diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index e9d655c49..16dee327e 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -35,9 +35,6 @@ jobs: run: source ci_scripts/ci_prepare_env.sh && setup_github_actions_environment - - run: | - xcversion installed - - name: SwiftLint run: bundle exec fastlane linting diff --git a/.gitignore b/.gitignore index 36e2b2501..8a80e27f8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## User settings -xcuserdata/* +xcuserdata/ +*.xcuserdata/* /OpenEdX.xcodeproj/xcuserdata/ /OpenEdX.xcworkspace/xcuserdata/ /OpenEdX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -25,6 +26,15 @@ DerivedData/ *.perspectivev3 !default.perspectivev3 +*.xcodeproj/* +**/xcuserdata/ +**/*.xcuserdata/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings + ## Obj-C/Swift specific *.hmap @@ -100,4 +110,9 @@ iOSInjectionProject/ xcode-frameworks vendor/ -.bundle/ \ No newline at end of file +.bundle/ + +venv/ +Podfile.lock +config_settings.yaml +default_config/ \ No newline at end of file diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index fba945920..acde4b3e3 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -26,7 +26,11 @@ 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 /* SocialAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */; }; + BADB3F552AD6DFC3004D5CFA /* SocialAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB3F542AD6DFC3004D5CFA /* SocialAuthViewModel.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 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -75,6 +79,10 @@ 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 = ""; }; 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 = ""; }; 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 = ""; }; + BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthView.swift; sourceTree = ""; }; + BADB3F542AD6DFC3004D5CFA /* SocialAuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthViewModel.swift; sourceTree = ""; }; + E03261632AE64676002CA7EB /* StartupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupViewModel.swift; sourceTree = ""; }; + E03261652AE64AF4002CA7EB /* StartupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupView.swift; sourceTree = ""; }; E78971D8E6ED2116BBF9FD66 /* Pods-App-Authorization.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.release.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.release.xcconfig"; sourceTree = ""; }; F52826C68AEA1CF4769389EA /* Pods-App-Authorization.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.releasestage.xcconfig"; sourceTree = ""; }; F5802BBA113276950ABCD9B3 /* Pods-App-Authorization.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.releaseprod.xcconfig"; sourceTree = ""; }; @@ -139,6 +147,8 @@ 071009CC28D1E24000344290 /* Presentation */ = { isa = PBXGroup; children = ( + BA8B3A302AD5485100D25EF5 /* SocialAuth */, + E03261622AE6464A002CA7EB /* Startup */, 020C31BD290AADA700D6DEA2 /* Base */, 071009C528D1D9FA00344290 /* Login */, 07169462296D93E000E3DED6 /* Registration */, @@ -258,6 +268,24 @@ path = ../Pods; sourceTree = ""; }; + BA8B3A302AD5485100D25EF5 /* SocialAuth */ = { + isa = PBXGroup; + children = ( + BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */, + BADB3F542AD6DFC3004D5CFA /* SocialAuthViewModel.swift */, + ); + path = SocialAuth; + sourceTree = ""; + }; + E03261622AE6464A002CA7EB /* Startup */ = { + isa = PBXGroup; + children = ( + E03261632AE64676002CA7EB /* StartupViewModel.swift */, + E03261652AE64AF4002CA7EB /* StartupView.swift */, + ); + path = Startup; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -467,16 +495,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BADB3F552AD6DFC3004D5CFA /* SocialAuthViewModel.swift in Sources */, 02066B442906D72400F4307E /* SignUpView.swift in Sources */, 0770DE7128D0C0E7006D8A5D /* Strings.swift in Sources */, 025F40E229D360E20064C183 /* ResetPasswordViewModel.swift in Sources */, 02066B462906D72F00F4307E /* SignUpViewModel.swift in Sources */, + E03261642AE64676002CA7EB /* StartupViewModel.swift in Sources */, 02A2ACDB2A4B016100FBBBBB /* AuthorizationAnalytics.swift in Sources */, 025F40E029D1E2FC0064C183 /* ResetPasswordView.swift in Sources */, 020C31CB290BF49900D6DEA2 /* FieldsView.swift in Sources */, 0770DE4E28D0A677006D8A5D /* SignInView.swift in Sources */, 02F3BFE5292533720051930C /* AuthorizationRouter.swift in Sources */, + E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */, 071009C728D1DA4F00344290 /* SignInViewModel.swift in Sources */, + BA8B3A322AD5487300D25EF5 /* SocialAuthView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift index 6c7a60393..d73799435 100644 --- a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift +++ b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift @@ -7,17 +7,31 @@ import Foundation -public enum LoginMethod: String { - case password = "Password" +public enum AuthMethod: Equatable { + case password + case socailAuth(SocialAuthMethod) + + public var analyticsValue: String { + switch self { + case .password: + "Password" + case .socailAuth(let socialAuthMethod): + socialAuthMethod.rawValue + } + } +} + +public enum SocialAuthMethod: String { case facebook = "Facebook" case google = "Google" case microsoft = "Microsoft" + case apple = "Apple" } //sourcery: AutoMockable public protocol AuthorizationAnalytics { func setUserID(_ id: String) - func userLogin(method: LoginMethod) + func userLogin(method: AuthMethod) func signUpClicked() func createAccountClicked() func registrationSuccess() @@ -28,7 +42,7 @@ public protocol AuthorizationAnalytics { #if DEBUG class AuthorizationAnalyticsMock: AuthorizationAnalytics { public func setUserID(_ id: String) {} - public func userLogin(method: LoginMethod) {} + public func userLogin(method: AuthMethod) {} public func signUpClicked() {} public func createAccountClicked() {} public func registrationSuccess() {} diff --git a/Authorization/Authorization/Presentation/AuthorizationRouter.swift b/Authorization/Authorization/Presentation/AuthorizationRouter.swift index 925e0a358..2d41d7683 100644 --- a/Authorization/Authorization/Presentation/AuthorizationRouter.swift +++ b/Authorization/Authorization/Presentation/AuthorizationRouter.swift @@ -9,13 +9,14 @@ import Foundation import Core //sourcery: AutoMockable -public protocol AuthorizationRouter: BaseRouter {} +public protocol AuthorizationRouter: BaseRouter { + func showUpdateRequiredView(showAccountLink: Bool) +} // Mark - For testing and SwiftUI preview #if DEBUG public class AuthorizationRouterMock: BaseRouterMock, AuthorizationRouter { - public override init() {} - + public func showUpdateRequiredView(showAccountLink: Bool) {} } #endif diff --git a/Authorization/Authorization/Presentation/Base/FieldsView.swift b/Authorization/Authorization/Presentation/Base/FieldsView.swift index 1a71a2483..6abe1858e 100644 --- a/Authorization/Authorization/Presentation/Base/FieldsView.swift +++ b/Authorization/Authorization/Presentation/Base/FieldsView.swift @@ -7,17 +7,19 @@ import SwiftUI import Core +import Theme struct FieldsView: View { let fields: [FieldConfiguration] let router: BaseRouter - let configuration: Config + let config: ConfigProtocol let cssInjector: CSSInjector let proxy: GeometryProxy @Environment(\.colorScheme) var colorScheme @State private var text: String = "" - + @State private var sendMarketing: Bool = true + var body: some View { ForEach(0.. some View { + if fieldConfig.field.isHonorCode, + let eulaURL = config.agreement.eulaURL, + let tosURL = config.agreement.tosURL, + let policy = config.agreement.privacyPolicyURL { + let text = AuthLocalization.SignUp.agreement( + "\(config.platformName)", + eulaURL, + "\(config.platformName)", + tosURL, + "\(config.platformName)", + policy + ) + let checkBox = fields.first(where: { $0.field.type == .checkbox }) + checkBox.flatMap { _ in + CheckBoxView( + checked: $sendMarketing, + text: AuthLocalization.SignUp.marketingEmailTitle("\(config.platformName)"), + font: Theme.Fonts.labelSmall + ) + .padding(.vertical, 10) + .onAppear { + checkBox?.text = "\(sendMarketing)" + } + .onChange(of: sendMarketing) { newValue in + checkBox?.text = "\(newValue)" + } + } + Text(.init(text)) + .tint(Theme.Colors.accentColor) + .foregroundStyle(Theme.Colors.textSecondary) + .font(Theme.Fonts.labelSmall) + .padding(.vertical, 3) + .id(UUID()) + .environment(\.openURL, OpenURLAction(handler: handleURL)) + Divider() + } else { + HTMLFormattedText( + cssInjector.injectCSS( + colorScheme: colorScheme, + html: fieldConfig.field.label, + type: .discovery, + fontSize: 90, screenWidth: proxy.size.width) + ) + .id(UUID()) + .padding(.horizontal, -6) + } + } + + private func handleURL(_ url: URL) -> OpenURLAction.Result { + router.showWebBrowser(title: url.host ?? "", url: url) + return .handled + } } #if DEBUG @@ -107,7 +154,7 @@ struct FieldsView_Previews: PreviewProvider { FieldsView( fields: fields, router: AuthorizationRouterMock(), - configuration: ConfigMock(), + config: ConfigMock(), cssInjector: CSSInjectorMock(), proxy: proxy ) diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index fd98fde7c..fbd015c14 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -7,12 +7,16 @@ import SwiftUI import Core +import Theme +import Swinject public struct SignInView: View { @State private var email: String = "" @State private var password: String = "" + @Environment (\.isHorizontal) private var isHorizontal + @ObservedObject private var viewModel: SignInViewModel @@ -23,16 +27,33 @@ public struct SignInView: View { public var body: some View { ZStack(alignment: .top) { VStack { - CoreAssets.authBackground.swiftUIImage + ThemeAssets.authBackground.swiftUIImage .resizable() .edgesIgnoringSafeArea(.top) + .accessibilityIdentifier("auth_bg_image") }.frame(maxWidth: .infinity, maxHeight: 200) + if viewModel.config.features.startupScreenEnabled { + VStack { + Button(action: { viewModel.router.back() }, label: { + CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) + .backButtonStyle(color: Theme.Colors.loginNavigationText) + }) + .foregroundColor(Theme.Colors.styledButtonText) + .padding(.leading, isHorizontal ? 48 : 0) + .padding(.top, 11) + .accessibilityIdentifier("back_button") + + }.frame(maxWidth: .infinity, alignment: .topLeading) + .padding(.top, isHorizontal ? 20 : 0) + } VStack(alignment: .center) { - CoreAssets.appLogo.swiftUIImage + ThemeAssets.appLogo.swiftUIImage .resizable() .frame(maxWidth: 189, maxHeight: 54) - .padding(.vertical, 40) + .padding(.top, isHorizontal ? 20 : 40) + .padding(.bottom, isHorizontal ? 10 : 40) + .accessibilityIdentifier("logo_image") ScrollView { VStack { @@ -41,15 +62,18 @@ public struct SignInView: View { .font(Theme.Fonts.displaySmall) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 4) + .accessibilityIdentifier("signin_text") Text(AuthLocalization.SignIn.welcomeBack) .font(Theme.Fonts.titleSmall) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 20) + .accessibilityIdentifier("welcome_back_text") - Text(AuthLocalization.SignIn.email) + Text(AuthLocalization.SignIn.emailOrUsername) .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.textPrimary) - TextField(AuthLocalization.SignIn.email, text: $email) + .accessibilityIdentifier("username_text") + TextField(AuthLocalization.SignIn.emailOrUsername, text: $email) .keyboardType(.emailAddress) .textContentType(.emailAddress) .autocapitalization(.none) @@ -64,11 +88,13 @@ public struct SignInView: View { .stroke(lineWidth: 1) .fill(Theme.Colors.textInputStroke) ) + .accessibilityIdentifier("username_textfield") Text(AuthLocalization.SignIn.password) .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.textPrimary) .padding(.top, 18) + .accessibilityIdentifier("password_text") SecureField(AuthLocalization.SignIn.password, text: $password) .padding(.all, 14) .background( @@ -80,40 +106,59 @@ public struct SignInView: View { .stroke(lineWidth: 1) .fill(Theme.Colors.textInputStroke) ) - + .accessibilityIdentifier("password_textfield") HStack { - Button(AuthLocalization.SignIn.registerBtn) { - viewModel.trackSignUpClicked() - viewModel.router.showRegisterScreen() - }.foregroundColor(Theme.Colors.accentColor) - - Spacer() - + if !viewModel.config.features.startupScreenEnabled { + Button(CoreLocalization.SignIn.registerBtn) { + viewModel.router.showRegisterScreen(sourceScreen: viewModel.sourceScreen) + } + .foregroundColor(Theme.Colors.accentColor) + .accessibilityIdentifier("register_button") + + Spacer() + } + Button(AuthLocalization.SignIn.forgotPassBtn) { viewModel.trackForgotPasswordClicked() viewModel.router.showForgotPasswordScreen() - }.foregroundColor(Theme.Colors.accentColor) + } + .foregroundColor(Theme.Colors.accentColor) + .padding(.top, 0) + .accessibilityIdentifier("forgot_password_button") } - .padding(.top, 10) + if viewModel.isShowProgress { HStack(alignment: .center) { ProgressBar(size: 40, lineWidth: 8) .padding(20) + .accessibilityIdentifier("progressbar") }.frame(maxWidth: .infinity) } else { - StyledButton(AuthLocalization.SignIn.logInBtn) { + StyledButton(CoreLocalization.SignIn.logInBtn) { Task { await viewModel.login(username: email, password: password) } - }.frame(maxWidth: .infinity) - .padding(.top, 40) + } + .frame(maxWidth: .infinity) + .padding(.top, 40) + .accessibilityIdentifier("signin_button") } } + if viewModel.socialAuthEnabled { + SocialAuthView( + viewModel: .init( + config: viewModel.config + ) { result in + Task { await viewModel.login(with: result) } + } + ) + } + agreements Spacer() } .padding(.horizontal, 24) .padding(.top, 50) - }.roundedBackground(Theme.Colors.background) + }.roundedBackground(Theme.Colors.loginBackground) .scrollAvoidKeyboard(dismissKeyboardByTap: true) } @@ -123,7 +168,7 @@ public struct SignInView: View { VStack { Text(viewModel.alertMessage ?? "") .shadowCardStyle(bgColor: Theme.Colors.accentColor, - textColor: .white) + textColor: Theme.Colors.white) .padding(.top, 80) Spacer() @@ -141,6 +186,7 @@ public struct SignInView: View { VStack { Spacer() SnackBarView(message: viewModel.errorMessage) + .accessibilityLabel("error_snackbar") }.transition(.move(edge: .bottom)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { @@ -150,10 +196,37 @@ public struct SignInView: View { } } .hideNavigationBar() - .navigationBarBackButtonHidden(true) - .navigationBarHidden(true) + .ignoresSafeArea(.all, edges: .horizontal) .background(Theme.Colors.background.ignoresSafeArea(.all)) } + + @ViewBuilder + private var agreements: some View { + if let eulaURL = viewModel.config.agreement.eulaURL, + let tosURL = viewModel.config.agreement.tosURL, + let policy = viewModel.config.agreement.privacyPolicyURL { + let text = AuthLocalization.SignIn.agreement( + "\(viewModel.config.platformName)", + eulaURL, + "\(viewModel.config.platformName)", + tosURL, + "\(viewModel.config.platformName)", + policy + ) + Text(.init(text)) + .tint(Theme.Colors.accentColor) + .foregroundStyle(Theme.Colors.textSecondary) + .font(Theme.Fonts.labelSmall) + .padding(.top, viewModel.socialAuthEnabled ? 0 : 15) + .padding(.bottom, 15) + .environment(\.openURL, OpenURLAction(handler: handleURL)) + } + } + + private func handleURL(_ url: URL) -> OpenURLAction.Result { + viewModel.router.showWebBrowser(title: url.host ?? "", url: url) + return .handled + } } #if DEBUG @@ -162,8 +235,10 @@ struct SignInView_Previews: PreviewProvider { let vm = SignInViewModel( interactor: AuthInteractor.mock, router: AuthorizationRouterMock(), + config: ConfigMock(), analytics: AuthorizationAnalyticsMock(), - validator: Validator() + validator: Validator(), + sourceScreen: .default ) SignInView(viewModel: vm) diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index 6d8ebfdee..3edd28bcf 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -9,12 +9,18 @@ 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 + let sourceScreen: LogistrationSourceScreen + var errorMessage: String? { didSet { withAnimation { @@ -31,30 +37,41 @@ public class SignInViewModel: ObservableObject { } let router: AuthorizationRouter - + let config: ConfigProtocol private let interactor: AuthInteractorProtocol private let analytics: AuthorizationAnalytics private let validator: Validator - + public init( interactor: AuthInteractorProtocol, router: AuthorizationRouter, + config: ConfigProtocol, analytics: AuthorizationAnalytics, - validator: Validator + validator: Validator, + sourceScreen: LogistrationSourceScreen ) { self.interactor = interactor self.router = router + self.config = config self.analytics = analytics self.validator = validator + self.sourceScreen = sourceScreen } - + + var socialAuthEnabled: Bool { + config.appleSignIn.enabled || + config.facebook.enabled || + config.microsoft.enabled || + config.google.enabled + } + @MainActor func login(username: String, password: String) async { - guard validator.isValidEmail(username) else { - errorMessage = AuthLocalization.Error.invalidEmailAddress + guard validator.isValidUsername(username) else { + errorMessage = AuthLocalization.Error.invalidEmailAddressOrUsername return } - guard validator.isValidPassword(password) else { + guard !password.isEmpty else { errorMessage = AuthLocalization.Error.invalidPasswordLenght return } @@ -64,27 +81,69 @@ public class SignInViewModel: ObservableObject { let user = try await interactor.login(username: username, password: password) analytics.setUserID("\(user.id)") analytics.userLogin(method: .password) - router.showMainScreen() + router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) } catch let error { - isShowProgress = false - 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 login(with result: Result) async { + switch result { + case .success(let result): + await socialLogin( + externalToken: result.response.token, + backend: result.backend, + authMethod: result.authMethod + ) + case .failure(let error): + errorMessage = error.localizedDescription + } + } + + @MainActor + private func socialLogin( + externalToken: String, + backend: String, + authMethod: AuthMethod + ) async { + isShowProgress = true + do { + let user = try await interactor.login(externalToken: externalToken, backend: backend) + analytics.setUserID("\(user.id)") + analytics.userLogin(method: authMethod) + router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + } catch let error { + failure(error, authMethod: authMethod) + } + } + + @MainActor + private func failure(_ error: Error, authMethod: AuthMethod? = nil) { + isShowProgress = false + if let validationError = error.validationError, + let value = validationError.data?["error_description"] as? String { + if authMethod != .password, validationError.statusCode == 400, let authMethod = authMethod { + errorMessage = AuthLocalization.Error.accountNotRegistered( + authMethod.analyticsValue, + config.platformName + ) + } else if validationError.statusCode == 403 { + errorMessage = AuthLocalization.Error.disabledAccount } 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() } + } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 2ce5f263c..3109c367a 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -7,12 +7,15 @@ import SwiftUI import Core +import Theme public struct SignUpView: View { @State private var disclosureGroupOpen: Bool = false + @Environment (\.isHorizontal) private var isHorizontal + @ObservedObject private var viewModel: SignUpViewModel @@ -26,24 +29,29 @@ public struct SignUpView: View { public var body: some View { ZStack(alignment: .top) { VStack { - CoreAssets.authBackground.swiftUIImage + ThemeAssets.authBackground.swiftUIImage .resizable() .edgesIgnoringSafeArea(.top) - }.frame(maxWidth: .infinity, maxHeight: 200) + } + .frame(maxWidth: .infinity, maxHeight: 200) + .accessibilityIdentifier("auth_bg_image") // MARK: - Page name VStack(alignment: .center) { ZStack { HStack { - Text(AuthLocalization.SignIn.registerBtn) - .titleSettings(color: .white) + Text(CoreLocalization.SignIn.registerBtn) + .titleSettings(color: Theme.Colors.loginNavigationText) + .accessibilityIdentifier("register_text") } VStack { Button(action: { viewModel.router.back() }, label: { CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .backButtonStyle(color: .white) + .backButtonStyle(color: Theme.Colors.loginNavigationText) }) .foregroundColor(Theme.Colors.styledButtonText) + .padding(.leading, isHorizontal ? 48 : 0) + .accessibilityIdentifier("back_button") }.frame(minWidth: 0, maxWidth: .infinity, @@ -59,59 +67,106 @@ public struct SignUpView: View { .font(Theme.Fonts.displaySmall) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 4) + .accessibilityIdentifier("signup_text") Text(AuthLocalization.SignUp.subtitle) .font(Theme.Fonts.titleSmall) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 20) - - let requiredFields = viewModel.fields.filter {$0.field.required} - let nonRequiredFields = viewModel.fields.filter {!$0.field.required} - - FieldsView(fields: requiredFields, - router: viewModel.router, - configuration: viewModel.config, - cssInjector: viewModel.cssInjector, - proxy: proxy) - + .accessibilityIdentifier("signup_subtitle_text") + + if viewModel.thirdPartyAuthSuccess { + Text(AuthLocalization.SignUp.successSigninLabel) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("social_auth_success_text") + Text(AuthLocalization.SignUp.successSigninSublabel) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textSecondary) + .padding(.bottom, 20) + .accessibilityIdentifier("social_auth_success_subtext_text") + } + + let requiredFields = viewModel.requiredFields + let optionalFields = viewModel.optionalFields + + FieldsView( + fields: requiredFields, + router: viewModel.router, + config: viewModel.config, + cssInjector: viewModel.cssInjector, + proxy: proxy + ) + if !viewModel.isShowProgress { - DisclosureGroup(isExpanded: $disclosureGroupOpen, content: { - FieldsView(fields: nonRequiredFields, + DisclosureGroup(isExpanded: $disclosureGroupOpen) { + FieldsView( + fields: optionalFields, router: viewModel.router, - configuration: viewModel.config, + config: viewModel.config, cssInjector: viewModel.cssInjector, - proxy: proxy).padding(.horizontal, 1) - }, label: { + proxy: proxy + ) + .padding(.horizontal, 1) + } label: { Text(disclosureGroupOpen ? AuthLocalization.SignUp.hideFields : AuthLocalization.SignUp.showFields) - }) + } + .accessibilityLabel("optional_fields_text") + .padding(.top, 10) } - + + FieldsView( + fields: viewModel.agreementsFields, + router: viewModel.router, + config: viewModel.config, + cssInjector: viewModel.cssInjector, + proxy: proxy + ) + .transaction { transaction in + transaction.animation = nil + } + if viewModel.isShowProgress { HStack(alignment: .center) { ProgressBar(size: 40, lineWidth: 8) .padding(20) + .accessibilityLabel("progressbar") }.frame(maxWidth: .infinity) } else { StyledButton(AuthLocalization.SignUp.createAccountBtn) { + viewModel.thirdPartyAuthSuccess = false Task { await viewModel.registerUser() } viewModel.trackCreateAccountClicked() } - .padding(.top, 40) - .padding(.bottom, 80) + .padding(.top, 30) .frame(maxWidth: .infinity) + .accessibilityLabel("signup_button") + } + if viewModel.socialAuthEnabled, + !requiredFields.isEmpty { + SocialAuthView( + authType: .register, + viewModel: .init( + config: viewModel.config + ) { result in + Task { await viewModel.register(with: result) } + } + ) + .padding(.bottom, 30) } Spacer() } .padding(.horizontal, 24) .padding(.top, 24) - }.roundedBackground(Theme.Colors.background) - .onRightSwipeGesture { - viewModel.router.back() - } + } + .roundedBackground(Theme.Colors.background) + .onRightSwipeGesture { + viewModel.router.back() + } .scrollAvoidKeyboard(dismissKeyboardByTap: true) .onChange(of: viewModel.scrollTo, perform: { index in withAnimation { @@ -127,6 +182,7 @@ public struct SignUpView: View { VStack { Spacer() SnackBarView(message: viewModel.errorMessage) + .accessibilityLabel("error_snackbar") }.transition(.move(edge: .bottom)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { @@ -135,6 +191,7 @@ public struct SignUpView: View { } } } + .ignoresSafeArea(.all, edges: .horizontal) .background(Theme.Colors.background.ignoresSafeArea(.all)) .hideNavigationBar() } @@ -150,7 +207,8 @@ struct SignUpView_Previews: PreviewProvider { analytics: AuthorizationAnalyticsMock(), config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: Validator() + validator: Validator(), + sourceScreen: .default ) SignUpView(viewModel: vm) diff --git a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift index a2142684f..023298e68 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift @@ -8,12 +8,19 @@ import Foundation import Core import SwiftUI +import AuthenticationServices +import FacebookLogin +import GoogleSignIn +import MSAL public class SignUpViewModel: ObservableObject { @Published var isShowProgress = false @Published var scrollTo: Int? @Published var showError: Bool = false + @Published var thirdPartyAuthSuccess: Bool = false + let sourceScreen: LogistrationSourceScreen + var errorMessage: String? { didSet { withAnimation { @@ -23,22 +30,39 @@ public class SignUpViewModel: ObservableObject { } @Published var fields: [FieldConfiguration] = [] - + var requiredFields: [FieldConfiguration] { + fields.filter { + $0.field.required && + !$0.field.isHonorCode && + $0.field.type != .checkbox + } + } + var agreementsFields: [FieldConfiguration] { + fields.filter { + $0.field.isHonorCode || + $0.field.type == .checkbox + } + } + var optionalFields: [FieldConfiguration] { + fields.filter { !$0.field.required } + } + let router: AuthorizationRouter - let config: Config + let config: ConfigProtocol let cssInjector: CSSInjector private let interactor: AuthInteractorProtocol private let analytics: AuthorizationAnalytics private let validator: Validator - + public init( interactor: AuthInteractorProtocol, router: AuthorizationRouter, analytics: AuthorizationAnalytics, - config: Config, + config: ConfigProtocol, cssInjector: CSSInjector, - validator: Validator + validator: Validator, + sourceScreen: LogistrationSourceScreen ) { self.interactor = interactor self.router = router @@ -46,9 +70,23 @@ public class SignUpViewModel: ObservableObject { self.config = config self.cssInjector = cssInjector self.validator = validator + self.sourceScreen = sourceScreen } - + + var socialAuthEnabled: Bool { + let socialLoginEnabled = config.appleSignIn.enabled || + config.facebook.enabled || + config.microsoft.enabled || + config.google.enabled + return socialLoginEnabled && !thirdPartyAuthSuccess && !isShowProgress + } + private func showErrors(errors: [String: String]) -> Bool { + if thirdPartyAuthSuccess, !errors.map({ $0.value }).filter({ !$0.isEmpty }).isEmpty { + scrollTo = 1 + return true + } + var containsError = false errors.forEach { key, value in if let index = fields.firstIndex(where: { $0.field.name == key }) { @@ -71,29 +109,32 @@ public class SignUpViewModel: ObservableObject { isShowProgress = false if error.isInternetError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else if error.isUpdateRequeiredError { + router.showUpdateRequiredView(showAccountLink: false) } else { errorMessage = CoreLocalization.Error.unknownError } } } - + + private var externalToken: String? + private var backend: String? + @MainActor func registerUser() async { do { - var validateFields: [String: String] = [:] - fields.forEach({ - validateFields[$0.field.name] = $0.text - }) - validateFields["honor_code"] = "true" - validateFields["terms_of_service"] = "true" + let validateFields = configureFields() let errors = try await interactor.validateRegistrationFields(fields: validateFields) guard !showErrors(errors: errors) else { return } isShowProgress = true - let user = try await interactor.registerUser(fields: validateFields) + let user = try await interactor.registerUser( + fields: validateFields, + isSocial: externalToken != nil + ) analytics.setUserID("\(user.id)") analytics.registrationSuccess() isShowProgress = false - router.showMainScreen() + router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) } catch let error { isShowProgress = false @@ -106,7 +147,66 @@ public class SignUpViewModel: ObservableObject { } } } - + + private func configureFields() -> [String: String] { + var validateFields: [String: String] = [:] + fields.forEach { validateFields[$0.field.name] = $0.text } + validateFields["honor_code"] = "true" + validateFields["terms_of_service"] = "true" + if let externalToken = externalToken, let backend = backend { + validateFields["access_token"] = externalToken + validateFields["provider"] = backend + validateFields["client_id"] = config.oAuthClientId + if validateFields.contains(where: {$0.key == "password"}) { + validateFields.removeValue(forKey: "password") + } + fields.removeAll { $0.field.type == .password } + } + return validateFields + } + + @MainActor + func register(with result: Result) async { + switch result { + case .success(let result): + await loginOrRegister( + result.response, + backend: result.backend, + authMethod: result.authMethod + ) + case .failure(let error): + errorMessage = error.localizedDescription + } + } + + @MainActor + private func loginOrRegister( + _ response: SocialAuthResponse, + backend: String, + authMethod: AuthMethod + ) async { + do { + isShowProgress = true + let user = try await interactor.login(externalToken: response.token, backend: backend) + analytics.setUserID("\(user.id)") + analytics.userLogin(method: authMethod) + isShowProgress = false + router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + } catch { + update(fullName: response.name, email: response.email) + self.externalToken = response.token + self.backend = backend + thirdPartyAuthSuccess = true + isShowProgress = false + await registerUser() + } + } + + private func update(fullName: String?, email: String?) { + fields.first(where: { $0.field.type == .email })?.text = email ?? "" + fields.first(where: { $0.field.name == "name" })?.text = fullName ?? "" + } + func trackCreateAccountClicked() { analytics.createAccountClicked() } diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift index 17f7466c0..3cec56f4a 100644 --- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift +++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme public struct ResetPasswordView: View { @@ -14,6 +15,8 @@ public struct ResetPasswordView: View { @State private var isRecovered: Bool = false + @Environment (\.isHorizontal) private var isHorizontal + @ObservedObject private var viewModel: ResetPasswordViewModel @@ -24,18 +27,20 @@ public struct ResetPasswordView: View { public var body: some View { ZStack(alignment: .top) { VStack { - CoreAssets.authBackground.swiftUIImage + ThemeAssets.authBackground.swiftUIImage .resizable() .edgesIgnoringSafeArea(.top) - }.frame(maxWidth: .infinity, maxHeight: 200) + } + .frame(maxWidth: .infinity, maxHeight: 200) + .accessibilityIdentifier("auth_bg_image") VStack(alignment: .center) { NavigationBar(title: AuthLocalization.Forgot.title, - titleColor: .white, - leftButtonColor: .white, + titleColor: Theme.Colors.loginNavigationText, + leftButtonColor: Theme.Colors.loginNavigationText, leftButtonAction: { viewModel.router.back() - }) + }).padding(.leading, isHorizontal ? 48 : 0) ScrollView { VStack { @@ -47,22 +52,26 @@ public struct ResetPasswordView: View { .frame(width: 100, height: 100) .padding(.bottom, 40) .padding(.top, 100) + .accessibilityIdentifier("check_email_image") Text(AuthLocalization.Forgot.checkTitle) .font(Theme.Fonts.titleLarge) .multilineTextAlignment(.center) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 4) + .accessibilityIdentifier("recover_title_text") Text(AuthLocalization.Forgot.checkDescription + email) .font(Theme.Fonts.bodyMedium) .multilineTextAlignment(.center) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 20) - StyledButton(AuthLocalization.SignIn.logInBtn) { + .accessibilityIdentifier("recover_description_text") + StyledButton(CoreLocalization.SignIn.logInBtn) { viewModel.router.backToRoot(animated: true) } .padding(.top, 30) .frame(maxWidth: .infinity) + .accessibilityIdentifier("signin_button") } } @@ -72,13 +81,16 @@ public struct ResetPasswordView: View { .font(Theme.Fonts.displaySmall) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 4) + .accessibilityIdentifier("forgot_title_text") Text(AuthLocalization.Forgot.description) .font(Theme.Fonts.titleSmall) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 20) + .accessibilityIdentifier("forgot_description_text") Text(AuthLocalization.SignIn.email) .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("email_text") TextField(AuthLocalization.SignIn.email, text: $email) .keyboardType(.emailAddress) .textContentType(.emailAddress) @@ -94,10 +106,12 @@ public struct ResetPasswordView: View { .stroke(lineWidth: 1) .fill(Theme.Colors.textInputStroke) ) + .accessibilityIdentifier("email_textfield") if viewModel.isShowProgress { HStack(alignment: .center) { ProgressBar(size: 40, lineWidth: 8) .padding(20) + .accessibilityIdentifier("progressbar") }.frame(maxWidth: .infinity) } else { StyledButton(AuthLocalization.Forgot.request) { @@ -107,6 +121,7 @@ public struct ResetPasswordView: View { } .padding(.top, 30) .frame(maxWidth: .infinity) + .accessibilityIdentifier("reset_password_button") } } } @@ -123,8 +138,9 @@ public struct ResetPasswordView: View { VStack { Text(viewModel.alertMessage ?? "") .shadowCardStyle(bgColor: Theme.Colors.accentColor, - textColor: .white) + textColor: Theme.Colors.white) .padding(.top, 80) + .accessibilityIdentifier("show_alert_text") Spacer() } @@ -141,6 +157,7 @@ public struct ResetPasswordView: View { VStack { Spacer() SnackBarView(message: viewModel.errorMessage) + .accessibilityIdentifier("show_error_snackbar") }.transition(.move(edge: .bottom)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { @@ -149,7 +166,10 @@ public struct ResetPasswordView: View { } } } + .ignoresSafeArea(.all, edges: .horizontal) + .background(Theme.Colors.background.ignoresSafeArea(.all)) + .hideNavigationBar() } } diff --git a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift new file mode 100644 index 000000000..f5190cf4e --- /dev/null +++ b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift @@ -0,0 +1,118 @@ +// +// SocialAuthView.swift +// Authorization +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import SwiftUI +import Core + +struct SocialAuthView: View { + + // MARK: - Properties + + @StateObject var viewModel: SocialAuthViewModel + + init( + authType: SocialAuthType = .signIn, + viewModel: SocialAuthViewModel + ) { + self._viewModel = .init(wrappedValue: viewModel) + self.authType = authType + } + + enum SocialAuthType { + case signIn + case register + } + var authType: SocialAuthType = .signIn + + private var title: String { + switch authType { + case .signIn: + AuthLocalization.signInWith + case .register: + AuthLocalization.registerWith + } + } + + // MARK: - Views + + var body: some View { + VStack(spacing: 10) { + headerView + buttonsView + } + .padding(.bottom, 20) + } + + private var headerView: some View { + HStack { + Text("\(AuthLocalization.or) \(title.lowercased()):") + .padding(.vertical, 20) + .font(.system(size: 17, weight: .medium)) + .accessibilityIdentifier("social_auth_title_text") + Spacer() + } + } + + private var buttonsView: some View { + Group { + if viewModel.googleEnabled { + SocialAuthButton( + image: CoreAssets.iconGoogleWhite.swiftUIImage, + title: "\(title) \(AuthLocalization.google)", + textColor: .black, + backgroundColor: CoreAssets.googleButtonColor.swiftUIColor, + action: { Task { await viewModel.signInWithGoogle() } } + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(title) \(AuthLocalization.google)") + .accessibilityIdentifier("social_auth_google_button") + } + if viewModel.faceboolEnabled { + SocialAuthButton( + image: CoreAssets.iconFacebookWhite.swiftUIImage, + title: "\(title) \(AuthLocalization.facebook)", + backgroundColor: CoreAssets.facebookButtonColor.swiftUIColor, + action: { Task { await viewModel.signInWithFacebook() } } + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(title) \(AuthLocalization.facebook)") + .accessibilityIdentifier("social_auth_facebook_button") + } + if viewModel.microsoftEnabled { + SocialAuthButton( + image: CoreAssets.iconMicrosoftWhite.swiftUIImage, + title: "\(title) \(AuthLocalization.microsoft)", + backgroundColor: CoreAssets.microsoftButtonColor.swiftUIColor, + action: { Task { await viewModel.signInWithMicrosoft() } } + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(title) \(AuthLocalization.microsoft)") + .accessibilityIdentifier("social_auth_microsoft_button") + } + if viewModel.appleSignInEnabled { + SocialAuthButton( + image: CoreAssets.iconApple.swiftUIImage, + title: "\(title) \(AuthLocalization.apple)", + backgroundColor: CoreAssets.appleButtonColor.swiftUIColor, + action: viewModel.signInWithApple + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(title) \(AuthLocalization.apple)") + .accessibilityIdentifier("social_auth_apple_button") + } + } + } +} + +#if DEBUG +struct SocialSignView_Previews: PreviewProvider { + static var previews: some View { + let vm = SocialAuthViewModel(config: ConfigMock(), completion: { _ in }) + SocialAuthView(viewModel: vm).padding() + } +} +#endif diff --git a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift new file mode 100644 index 000000000..24e8ca5b8 --- /dev/null +++ b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift @@ -0,0 +1,157 @@ +// +// SocialAuthViewModel.swift +// Authorization +// +// Created by Eugene Yatsenko on 11.10.2023. +// + +import SwiftUI +import Core +import AuthenticationServices +import FacebookLogin +import GoogleSignIn +import MSAL +import Swinject + +enum SocialAuthDetails { + case apple(SocialAuthResponse) + case facebook(SocialAuthResponse) + case google(SocialAuthResponse) + case microsoft(SocialAuthResponse) + + var backend: String { + switch self { + case .apple: + "apple-id" + case .facebook: + "facebook" + case .google: + "google-oauth2" + case .microsoft: + "azuread-oauth2" + } + } + + var authMethod: AuthMethod { + switch self { + case .apple: + .socailAuth(.apple) + case .facebook: + .socailAuth(.facebook) + case .google: + .socailAuth(.google) + case .microsoft: + .socailAuth(.microsoft) + } + } + + var response: SocialAuthResponse { + switch self { + case .apple(let response), + .facebook(let response), + .google(let response), + .microsoft(let response): + return response + } + } +} + +final public class SocialAuthViewModel: ObservableObject { + + // MARK: - Properties + + private var completion: ((Result) -> Void) + private let config: ConfigProtocol + + init( + config: ConfigProtocol, + completion: @escaping (Result) -> Void + ) { + self.config = config + self.completion = completion + } + + private lazy var appleAuthProvider: AppleAuthProvider = .init(config: config) + private lazy var googleAuthProvider: GoogleAuthProvider = .init() + private lazy var facebookAuthProvider: FacebookAuthProvider = .init() + private lazy var microsoftAuthProvider: MicrosoftAuthProvider = .init() + + private var topViewController: UIViewController? { + UIApplication.topViewController() + } + + // MARK: - Public Properties + + var faceboolEnabled: Bool { + config.facebook.enabled + } + + var googleEnabled: Bool { + config.google.enabled + } + + var microsoftEnabled: Bool { + config.microsoft.enabled + } + + var appleSignInEnabled: Bool { + if faceboolEnabled || + googleEnabled || + microsoftEnabled { + /// Apps that use a third-party or social login service (such as Facebook Login, Google Sign-In...) + /// to set up or authenticate the user's primary account with the app + /// must also offer Sign in with Apple as an equivalent option + return true + } + return config.appleSignIn.enabled + } + + // MARK: - Public Intens + + func signInWithApple() { + appleAuthProvider.request { [weak self] result in + guard let self else { return } + result.success { self.success(with: .apple($0)) } + result.failure(self.failure) + } + } + + @MainActor + func signInWithGoogle() async { + guard let vc = topViewController else { + return + } + let result = await googleAuthProvider.signIn(withPresenting: vc) + result.success { success(with: .google($0)) } + result.failure(failure) + } + + @MainActor + func signInWithFacebook() async { + guard let vc = topViewController else { + return + } + let result = await facebookAuthProvider.signIn(withPresenting: vc) + result.success { success(with: .facebook($0)) } + result.failure(failure) + } + + @MainActor + func signInWithMicrosoft() async { + guard let vc = topViewController else { + return + } + let result = await microsoftAuthProvider.signIn(withPresenting: vc) + result.success { success(with: .microsoft($0)) } + result.failure(failure) + } + + private func success(with social: SocialAuthDetails) { + completion(.success(social)) + } + + private func failure(_ error: Error) { + completion(.failure(error)) + } + +} diff --git a/Authorization/Authorization/Presentation/Startup/StartupView.swift b/Authorization/Authorization/Presentation/Startup/StartupView.swift new file mode 100644 index 000000000..37dbfb2c7 --- /dev/null +++ b/Authorization/Authorization/Presentation/Startup/StartupView.swift @@ -0,0 +1,141 @@ +// +// StartupView.swift +// Authorization +// +// Created by SaeedBashir on 10/23/23. +// + +import Foundation +import SwiftUI +import Core +import Theme + +public struct StartupView: View { + + @State private var searchQuery: String = "" + + @Environment(\.isHorizontal) private var isHorizontal + + @ObservedObject + private var viewModel: StartupViewModel + + public init(viewModel: StartupViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + ZStack(alignment: .top) { + VStack(alignment: .leading) { + ThemeAssets.appLogo.swiftUIImage + .resizable() + .frame(maxWidth: 189, maxHeight: 54) + .padding(.top, isHorizontal ? 20 : 40) + .padding(.bottom, isHorizontal ? 0 : 20) + .padding(.horizontal, isHorizontal ? 10 : 24) + .colorMultiply(Theme.Colors.accentColor) + .accessibilityIdentifier("logo_image") + + VStack { + VStack(alignment: .leading) { + Text(AuthLocalization.Startup.infoMessage) + .font(Theme.Fonts.titleLarge) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.bottom, isHorizontal ? 10 : 20 ) + .accessibilityIdentifier("heading_text") + + Text(AuthLocalization.Startup.searchTitle) + .font(Theme.Fonts.bodyLarge) + .bold() + .foregroundColor(Theme.Colors.textPrimary) + .padding(.top, isHorizontal ? 0 : 24) + .accessibilityIdentifier("search_title_text") + + HStack(spacing: 11) { + Image(systemName: "magnifyingglass") + .padding(.leading, 16) + .padding(.top, 1) + TextField(AuthLocalization.Startup.searchPlaceholder, text: $searchQuery, onCommit: { + if searchQuery.isEmpty { return } + viewModel.router.showDiscoveryScreen( + searchQuery: searchQuery, + sourceScreen: .startup + ) + }) + .autocapitalization(.none) + .autocorrectionDisabled() + .frame(minHeight: 50) + .submitLabel(.search) + .accessibilityIdentifier("explore_courses_textfield") + + }.overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill(Theme.Colors.textInputStroke) + ) + .background( + Theme.Shapes.textInputShape + .fill(Theme.Colors.textInputBackground) + ) + + Button { + viewModel.router.showDiscoveryScreen ( + searchQuery: searchQuery, + sourceScreen: .startup + ) + } label: { + Text(AuthLocalization.Startup.exploreAllCourses) + .underline() + .foregroundColor(Theme.Colors.accentColor) + .font(Theme.Fonts.bodyLarge) + } + .padding(.top, isHorizontal ? 0 : 5) + .accessibilityIdentifier("explore_courses_button") + Spacer() + } + .padding(.horizontal, isHorizontal ? 10 : 24) + + LogistrationBottomView { buttonAction in + switch buttonAction { + case .signIn: + viewModel.router.showLoginScreen(sourceScreen: .startup) + case .register: + viewModel.router.showRegisterScreen(sourceScreen: .startup) + } + } + } + .padding(.top, 10) + .padding(.bottom, 2) + } + .onDisappear { + searchQuery = "" + } + } + .hideNavigationBar() + .padding(.all, isHorizontal ? 1 : 0) + .background(Theme.Colors.background.ignoresSafeArea(.all)) + .ignoresSafeArea(.keyboard, edges: .bottom) + .onTapGesture { + UIApplication.shared.endEditing() + } + } +} + +#if DEBUG +struct StartupView_Previews: PreviewProvider { + static var previews: some View { + let vm = StartupViewModel( + router: AuthorizationRouterMock() + ) + + StartupView(viewModel: vm) + .preferredColorScheme(.light) + .previewDisplayName("StartupView Light") + .loadFonts() + + StartupView(viewModel: vm) + .preferredColorScheme(.dark) + .previewDisplayName("StartupView Dark") + .loadFonts() + } +} +#endif diff --git a/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift new file mode 100644 index 000000000..1549940a1 --- /dev/null +++ b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift @@ -0,0 +1,20 @@ +// +// StartupViewModel.swift +// Authorization +// +// Created by SaeedBashir on 10/23/23. +// + +import Foundation +import Core + +public class StartupViewModel: ObservableObject { + let router: AuthorizationRouter + @Published var searchQuery: String? + + public init( + router: AuthorizationRouter + ) { + self.router = router + } +} diff --git a/Authorization/Authorization/SwiftGen/Strings.swift b/Authorization/Authorization/SwiftGen/Strings.swift index 9d7d92b4e..2a28a0db4 100644 --- a/Authorization/Authorization/SwiftGen/Strings.swift +++ b/Authorization/Authorization/SwiftGen/Strings.swift @@ -10,9 +10,31 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum AuthLocalization { + /// Apple + public static let apple = AuthLocalization.tr("Localizable", "APPLE", fallback: "Apple") + /// Facebook + public static let facebook = AuthLocalization.tr("Localizable", "FACEBOOK", fallback: "Facebook") + /// Google + public static let google = AuthLocalization.tr("Localizable", "GOOGLE", fallback: "Google") + /// Microsoft + public static let microsoft = AuthLocalization.tr("Localizable", "MICROSOFT", fallback: "Microsoft") + /// Or + public static let or = AuthLocalization.tr("Localizable", "OR", fallback: "Or") + /// Register with + public static let registerWith = AuthLocalization.tr("Localizable", "REGISTER_WITH", fallback: "Register with") + /// Sign in with + public static let signInWith = AuthLocalization.tr("Localizable", "SIGN_IN_WITH", fallback: "Sign in with") public enum Error { + /// This %@ account is not linked with any %@ account. Please register. + public static func accountNotRegistered(_ p1: Any, _ p2: Any) -> String { + return AuthLocalization.tr("Localizable", "ERROR.ACCOUNT_NOT_REGISTERED", String(describing: p1), String(describing: p2), fallback: "This %@ account is not linked with any %@ account. Please register.") + } + /// Your account is disabled. Please contact customer support for assistance. + public static let disabledAccount = AuthLocalization.tr("Localizable", "ERROR.DISABLED_ACCOUNT", fallback: "Your account is disabled. Please contact customer support for assistance.") /// Invalid email address public static let invalidEmailAddress = AuthLocalization.tr("Localizable", "ERROR.INVALID_EMAIL_ADDRESS", fallback: "Invalid email address") + /// Invalid email or username + public static let invalidEmailAddressOrUsername = AuthLocalization.tr("Localizable", "ERROR.INVALID_EMAIL_ADDRESS_OR_USERNAME", fallback: "Invalid email or username") /// Invalid password lenght public static let invalidPasswordLenght = AuthLocalization.tr("Localizable", "ERROR.INVALID_PASSWORD_LENGHT", fallback: "Invalid password lenght") } @@ -29,12 +51,17 @@ public enum AuthLocalization { public static let title = AuthLocalization.tr("Localizable", "FORGOT.TITLE", fallback: "Forgot password") } public enum SignIn { + /// By signing in to this app, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data in + /// accordance with the [Privacy Policy.](%@) + public static func agreement(_ p1: Any, _ p2: Any, _ p3: Any, _ p4: Any, _ p5: Any, _ p6: Any) -> String { + return AuthLocalization.tr("Localizable", "SIGN_IN.AGREEMENT", String(describing: p1), String(describing: p2), String(describing: p3), String(describing: p4), String(describing: p5), String(describing: p6), fallback: "By signing in to this app, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data in\naccordance with the [Privacy Policy.](%@)") + } /// Email public static let email = AuthLocalization.tr("Localizable", "SIGN_IN.EMAIL", fallback: "Email") + /// Email or username + public static let emailOrUsername = AuthLocalization.tr("Localizable", "SIGN_IN.EMAIL_OR_USERNAME", fallback: "Email or username") /// Forgot password? public static let forgotPassBtn = AuthLocalization.tr("Localizable", "SIGN_IN.FORGOT_PASS_BTN", fallback: "Forgot password?") - /// Sign in - public static let logInBtn = AuthLocalization.tr("Localizable", "SIGN_IN.LOG_IN_BTN", fallback: "Sign in") /// Localizable.strings /// Authorization /// @@ -42,23 +69,43 @@ public enum AuthLocalization { public static let logInTitle = AuthLocalization.tr("Localizable", "SIGN_IN.LOG_IN_TITLE", fallback: "Sign in") /// Password public static let password = AuthLocalization.tr("Localizable", "SIGN_IN.PASSWORD", fallback: "Password") - /// Register - public static let registerBtn = AuthLocalization.tr("Localizable", "SIGN_IN.REGISTER_BTN", fallback: "Register") /// Welcome back! Please authorize to continue. public static let welcomeBack = AuthLocalization.tr("Localizable", "SIGN_IN.WELCOME_BACK", fallback: "Welcome back! Please authorize to continue.") } public enum SignUp { + /// By creating an account, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data inaccordance with the [Privacy Policy.](%@) + public static func agreement(_ p1: Any, _ p2: Any, _ p3: Any, _ p4: Any, _ p5: Any, _ p6: Any) -> String { + return AuthLocalization.tr("Localizable", "SIGN_UP.AGREEMENT", String(describing: p1), String(describing: p2), String(describing: p3), String(describing: p4), String(describing: p5), String(describing: p6), fallback: "By creating an account, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data inaccordance with the [Privacy Policy.](%@)") + } /// Create account public static let createAccountBtn = AuthLocalization.tr("Localizable", "SIGN_UP.CREATE_ACCOUNT_BTN", fallback: "Create account") /// Hide optional Fields public static let hideFields = AuthLocalization.tr("Localizable", "SIGN_UP.HIDE_FIELDS", fallback: "Hide optional Fields") + /// I agree that %@ may send me marketing messages. + public static func marketingEmailTitle(_ p1: Any) -> String { + return AuthLocalization.tr("Localizable", "SIGN_UP.MARKETING_EMAIL_TITLE", String(describing: p1), fallback: "I agree that %@ may send me marketing messages.") + } /// Show optional Fields public static let showFields = AuthLocalization.tr("Localizable", "SIGN_UP.SHOW_FIELDS", fallback: "Show optional Fields") /// Create new account. public static let subtitle = AuthLocalization.tr("Localizable", "SIGN_UP.SUBTITLE", fallback: "Create new account.") + /// You've successfully signed in. + public static let successSigninLabel = AuthLocalization.tr("Localizable", "SIGN_UP.SUCCESS_SIGNIN_LABEL", fallback: "You've successfully signed in.") + /// We just need a little more information before you start learning. + public static let successSigninSublabel = AuthLocalization.tr("Localizable", "SIGN_UP.SUCCESS_SIGNIN_SUBLABEL", fallback: "We just need a little more information before you start learning.") /// Sign up public static let title = AuthLocalization.tr("Localizable", "SIGN_UP.TITLE", fallback: "Sign up") } + public enum Startup { + /// Explore all courses + public static let exploreAllCourses = AuthLocalization.tr("Localizable", "STARTUP.EXPLORE_ALL_COURSES", fallback: "Explore all courses") + /// Courses and programs from the world's best universities in your pocket. + public static let infoMessage = AuthLocalization.tr("Localizable", "STARTUP.INFO_MESSAGE", fallback: "Courses and programs from the world's best universities in your pocket.") + /// Search our 3000+ courses + public static let searchPlaceholder = AuthLocalization.tr("Localizable", "STARTUP.SEARCH_PLACEHOLDER", fallback: "Search our 3000+ courses") + /// What do you want to learn? + public static let searchTitle = AuthLocalization.tr("Localizable", "STARTUP.SEARCH_TITLE", fallback: "What do you want to learn?") + } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/Authorization/Authorization/en.lproj/Localizable.strings b/Authorization/Authorization/en.lproj/Localizable.strings index 88365042c..6133ff62f 100644 --- a/Authorization/Authorization/en.lproj/Localizable.strings +++ b/Authorization/Authorization/en.lproj/Localizable.strings @@ -9,23 +9,44 @@ "SIGN_IN.LOG_IN_TITLE" = "Sign in"; "SIGN_IN.WELCOME_BACK" = "Welcome back! Please authorize to continue."; "SIGN_IN.EMAIL" = "Email"; +"SIGN_IN.EMAIL_OR_USERNAME" = "Email or username"; "SIGN_IN.PASSWORD" = "Password"; -"SIGN_IN.REGISTER_BTN" = "Register"; "SIGN_IN.FORGOT_PASS_BTN" = "Forgot password?"; -"SIGN_IN.LOG_IN_BTN" = "Sign in"; +"SIGN_IN.AGREEMENT" = "By signing in to this app, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data in +accordance with the [Privacy Policy.](%@)"; "ERROR.INVALID_EMAIL_ADDRESS" = "Invalid email address"; "ERROR.INVALID_PASSWORD_LENGHT" = "Invalid password lenght"; +"ERROR.ACCOUNT_NOT_REGISTERED" = "This %@ account is not linked with any %@ account. Please register."; +"ERROR.INVALID_EMAIL_ADDRESS_OR_USERNAME" = "Invalid email or username"; +"ERROR.DISABLED_ACCOUNT" = "Your account is disabled. Please contact customer support for assistance."; "SIGN_UP.TITLE" = "Sign up"; "SIGN_UP.SUBTITLE" = "Create new account."; "SIGN_UP.CREATE_ACCOUNT_BTN" = "Create account"; "SIGN_UP.HIDE_FIELDS" = "Hide optional Fields"; "SIGN_UP.SHOW_FIELDS" = "Show optional Fields"; +"SIGN_UP.SUCCESS_SIGNIN_LABEL" = "You've successfully signed in."; +"SIGN_UP.SUCCESS_SIGNIN_SUBLABEL" = "We just need a little more information before you start learning."; +"SIGN_UP.AGREEMENT" = "By creating an account, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data inaccordance with the [Privacy Policy.](%@)"; +"SIGN_UP.MARKETING_EMAIL_TITLE" = "I agree that %@ may send me marketing messages."; "FORGOT.TITLE"= "Forgot password"; "FORGOT.DESCRIPTION" = "Please enter your log-in or recovery email address below and we will send you an email with instructions."; "FORGOT.REQUEST" = "Reset password"; "FORGOT.CHECK_TITLE" = "Check your email"; "FORGOT.CHECK_Description" = "We have sent a password recover instructions to your email "; + +"SIGN_IN_WITH" = "Sign in with"; +"REGISTER_WITH" = "Register with"; +"APPLE" = "Apple"; +"GOOGLE" = "Google"; +"FACEBOOK" = "Facebook"; +"MICROSOFT" = "Microsoft"; +"OR" = "Or"; + +"STARTUP.INFO_MESSAGE" = "Courses and programs from the world's best universities in your pocket."; +"STARTUP.SEARCH_TITLE" = "What do you want to learn?"; +"STARTUP.SEARCH_PLACEHOLDER" = "Search our 3000+ courses"; +"STARTUP.EXPLORE_ALL_COURSES" = "Explore all courses"; diff --git a/Authorization/Authorization/uk.lproj/Localizable.strings b/Authorization/Authorization/uk.lproj/Localizable.strings index cc06c89a0..3cd9772d4 100644 --- a/Authorization/Authorization/uk.lproj/Localizable.strings +++ b/Authorization/Authorization/uk.lproj/Localizable.strings @@ -10,21 +10,40 @@ "SIGN_IN.WELCOME_BACK" = "З поверненням! Авторизуйтесь, щоб продовжити."; "SIGN_IN.EMAIL" = "Пошта"; "SIGN_IN.PASSWORD" = "Пароль"; -"SIGN_IN.REGISTER_BTN" = "Реєстрація"; "SIGN_IN.FORGOT_PASS_BTN" = "Забули пароль?"; -"SIGN_IN.LOG_IN_BTN" = "Увійти"; +"SIGN_IN.AGREEMENT" = "By signing in to this app, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data in +accordance with the [Privacy Policy.](%@)"; "ERROR.INVALID_EMAIL_ADDRESS" = "невірна адреса електронної пошти"; "ERROR.INVALID_PASSWORD_LENGHT" = "Пароль занадто короткий або занадто довгий"; +"ERROR.ACCOUNT_NOT_REGISTERED" = "This %@ account is not linked with any %@ account. Please register."; +"ERROR.DISABLED_ACCOUNT" = "Your account is disabled. Please contact customer support for assistance."; "SIGN_UP.TITLE" = "Зареєструватись"; "SIGN_UP.SUBTITLE" = "Cтворити новий акаунт."; "SIGN_UP.CREATE_ACCOUNT_BTN" = "Створити акаунт"; "SIGN_UP.HIDE_FIELDS" = "Приховати необовʼязкові поля"; "SIGN_UP.SHOW_FIELDS" = "Показати необовʼязкові поля"; +"SIGN_UP.SUCCESS_SIGNIN_LABEL" = "You've successfully signed in."; +"SIGN_UP.SUCCESS_SIGNIN_SUBLABEL" = "We just need a little more information before you start learning."; +"SIGN_UP.AGREEMENT" = "By creating an account, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data inaccordance with the [Privacy Policy.](%@)"; +"SIGN_UP.MARKETING_EMAIL_TITLE" = "I agree that %@ may send me marketing messages."; "FORGOT.TITLE"= "Відновлення паролю"; "FORGOT.DESCRIPTION" = "Будь ласка, введіть свою адресу електронної пошти для входу або відновлення нижче, і ми надішлемо вам електронний лист з інструкціями."; "FORGOT.REQUEST" = "Відновити пароль"; "FORGOT.CHECK_TITLE" = "Перевірте свою електронну пошту"; "FORGOT.CHECK_Description" = "Ми надіслали інструкції щодо відновлення пароля на вашу електронну пошту "; + +"SIGN_IN_WITH" = "Sign in with"; +"REGISTER_WITH" = "Register with"; +"APPLE" = "Apple"; +"GOOGLE" = "Google"; +"FACEBOOK" = "Facebook"; +"MICROSOFT" = "Microsoft"; +"OR" = "Or"; + +"STARTUP.INFO_MESSAGE" = "Courses and programs from the world's best universities in your pocket."; +"STARTUP.SEARCH_TITLE" = "What do you want to learn?"; +"STARTUP.SEARCH_PLACEHOLDER" = "Search our 3000+ courses"; +"STARTUP.EXPLORE_ALL_COURSES" = "Explore all courses"; diff --git a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift index ddb4ac259..fbbb73a12 100644 --- a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift +++ b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.8.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func login(externalToken: String, backend: String) throws -> User { + addInvocation(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) + let perform = methodPerformValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) as? (String, String) -> Void + perform?(`externalToken`, `backend`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) return Matcher.ComparisonResult(results) + case (.m_login__externalToken_externalTokenbackend_backend(let lhsExternaltoken, let lhsBackend), .m_login__externalToken_externalTokenbackend_backend(let rhsExternaltoken, let rhsBackend)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsExternaltoken, rhs: rhsExternaltoken, with: matcher), lhsExternaltoken, rhsExternaltoken, "externalToken")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) @@ -466,9 +515,9 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { perform?(`id`) } - open func userLogin(method: LoginMethod) { - addInvocation(.m_userLogin__method_method(Parameter.value(`method`))) - let perform = methodPerformValue(.m_userLogin__method_method(Parameter.value(`method`))) as? (LoginMethod) -> Void + open func userLogin(method: AuthMethod) { + addInvocation(.m_userLogin__method_method(Parameter.value(`method`))) + let perform = methodPerformValue(.m_userLogin__method_method(Parameter.value(`method`))) as? (AuthMethod) -> Void perform?(`method`) } @@ -505,7 +554,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { fileprivate enum MethodType { case m_setUserID__id(Parameter) - case m_userLogin__method_method(Parameter) + case m_userLogin__method_method(Parameter) case m_signUpClicked case m_createAccountClicked case m_registrationSuccess @@ -579,7 +628,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { fileprivate var method: MethodType public static func setUserID(_ id: Parameter) -> Verify { return Verify(method: .m_setUserID__id(`id`))} - public static func userLogin(method: Parameter) -> Verify { return Verify(method: .m_userLogin__method_method(`method`))} + public static func userLogin(method: Parameter) -> Verify { return Verify(method: .m_userLogin__method_method(`method`))} public static func signUpClicked() -> Verify { return Verify(method: .m_signUpClicked)} public static func createAccountClicked() -> Verify { return Verify(method: .m_createAccountClicked)} public static func registrationSuccess() -> Verify { return Verify(method: .m_registrationSuccess)} @@ -594,7 +643,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { public static func setUserID(_ id: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_setUserID__id(`id`), performs: perform) } - public static func userLogin(method: Parameter, perform: @escaping (LoginMethod) -> Void) -> Perform { + public static func userLogin(method: Parameter, perform: @escaping (AuthMethod) -> Void) -> Perform { return Perform(method: .m_userLogin__method_method(`method`), performs: perform) } public static func signUpClicked(perform: @escaping () -> Void) -> Perform { @@ -731,6 +780,12 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { + open func showUpdateRequiredView(showAccountLink: Bool) { + addInvocation(.m_showUpdateRequiredView__showAccountLink_showAccountLink(Parameter.value(`showAccountLink`))) + let perform = methodPerformValue(.m_showUpdateRequiredView__showAccountLink_showAccountLink(Parameter.value(`showAccountLink`))) as? (Bool) -> Void + perform?(`showAccountLink`) + } + open func backToRoot(animated: Bool) { addInvocation(.m_backToRoot__animated_animated(Parameter.value(`animated`))) let perform = methodPerformValue(.m_backToRoot__animated_animated(Parameter.value(`animated`))) as? (Bool) -> Void @@ -761,22 +816,28 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { perform?(`controllers`) } - open func showMainScreen() { - addInvocation(.m_showMainScreen) - let perform = methodPerformValue(.m_showMainScreen) as? () -> Void - perform?() + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } - open func showLoginScreen() { - addInvocation(.m_showLoginScreen) - let perform = methodPerformValue(.m_showLoginScreen) as? () -> Void + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void perform?() } - open func showRegisterScreen() { - addInvocation(.m_showRegisterScreen) - let perform = methodPerformValue(.m_showRegisterScreen) as? () -> Void - perform?() + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } open func showForgotPasswordScreen() { @@ -785,6 +846,18 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { perform?() } + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -811,15 +884,19 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { fileprivate enum MethodType { + case m_showUpdateRequiredView__showAccountLink_showAccountLink(Parameter) case m_backToRoot__animated_animated(Parameter) case m_back__animated_animated(Parameter) case m_backWithFade case m_dismiss__animated_animated(Parameter) case m_removeLastView__controllers_controllers(Parameter) - case m_showMainScreen - case m_showLoginScreen - case m_showRegisterScreen + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) @@ -827,6 +904,11 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { + case (.m_showUpdateRequiredView__showAccountLink_showAccountLink(let lhsShowaccountlink), .m_showUpdateRequiredView__showAccountLink_showAccountLink(let rhsShowaccountlink)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsShowaccountlink, rhs: rhsShowaccountlink, with: matcher), lhsShowaccountlink, rhsShowaccountlink, "showAccountLink")) + return Matcher.ComparisonResult(results) + case (.m_backToRoot__animated_animated(let lhsAnimated), .m_backToRoot__animated_animated(let rhsAnimated)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAnimated, rhs: rhsAnimated, with: matcher), lhsAnimated, rhsAnimated, "animated")) @@ -849,14 +931,37 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) return Matcher.ComparisonResult(results) - case (.m_showMainScreen, .m_showMainScreen): return .match + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) - case (.m_showLoginScreen, .m_showLoginScreen): return .match + case (.m_showStartupScreen, .m_showStartupScreen): return .match - case (.m_showRegisterScreen, .m_showRegisterScreen): return .match + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -896,15 +1001,19 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { func intValue() -> Int { switch self { + case let .m_showUpdateRequiredView__showAccountLink_showAccountLink(p0): return p0.intValue case let .m_backToRoot__animated_animated(p0): return p0.intValue case let .m_back__animated_animated(p0): return p0.intValue case .m_backWithFade: return 0 case let .m_dismiss__animated_animated(p0): return p0.intValue case let .m_removeLastView__controllers_controllers(p0): return p0.intValue - case .m_showMainScreen: return 0 - case .m_showLoginScreen: return 0 - case .m_showRegisterScreen: return 0 + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue @@ -913,15 +1022,19 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { } func assertionName() -> String { switch self { + case .m_showUpdateRequiredView__showAccountLink_showAccountLink: return ".showUpdateRequiredView(showAccountLink:)" case .m_backToRoot__animated_animated: return ".backToRoot(animated:)" case .m_back__animated_animated: return ".back(animated:)" case .m_backWithFade: return ".backWithFade()" case .m_dismiss__animated_animated: return ".dismiss(animated:)" case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" - case .m_showMainScreen: return ".showMainScreen()" - case .m_showLoginScreen: return ".showLoginScreen()" - case .m_showRegisterScreen: return ".showRegisterScreen()" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" @@ -944,15 +1057,19 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { public struct Verify { fileprivate var method: MethodType + public static func showUpdateRequiredView(showAccountLink: Parameter) -> Verify { return Verify(method: .m_showUpdateRequiredView__showAccountLink_showAccountLink(`showAccountLink`))} public static func backToRoot(animated: Parameter) -> Verify { return Verify(method: .m_backToRoot__animated_animated(`animated`))} public static func back(animated: Parameter) -> Verify { return Verify(method: .m_back__animated_animated(`animated`))} public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} - public static func showMainScreen() -> Verify { return Verify(method: .m_showMainScreen)} - public static func showLoginScreen() -> Verify { return Verify(method: .m_showLoginScreen)} - public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} @@ -963,6 +1080,9 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { fileprivate var method: MethodType var performs: Any + public static func showUpdateRequiredView(showAccountLink: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_showUpdateRequiredView__showAccountLink_showAccountLink(`showAccountLink`), performs: perform) + } public static func backToRoot(animated: Parameter, perform: @escaping (Bool) -> Void) -> Perform { return Perform(method: .m_backToRoot__animated_animated(`animated`), performs: perform) } @@ -978,18 +1098,27 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) } - public static func showMainScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showMainScreen, performs: perform) + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } - public static func showLoginScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showLoginScreen, performs: perform) + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) } - public static func showRegisterScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showRegisterScreen, performs: perform) + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showForgotPasswordScreen, performs: perform) } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1151,22 +1280,28 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`controllers`) } - open func showMainScreen() { - addInvocation(.m_showMainScreen) - let perform = methodPerformValue(.m_showMainScreen) as? () -> Void - perform?() + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } - open func showLoginScreen() { - addInvocation(.m_showLoginScreen) - let perform = methodPerformValue(.m_showLoginScreen) as? () -> Void + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void perform?() } - open func showRegisterScreen() { - addInvocation(.m_showRegisterScreen) - let perform = methodPerformValue(.m_showRegisterScreen) as? () -> Void - perform?() + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } open func showForgotPasswordScreen() { @@ -1175,6 +1310,18 @@ open class BaseRouterMock: BaseRouter, Mock { perform?() } + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -1206,10 +1353,13 @@ open class BaseRouterMock: BaseRouter, Mock { case m_backWithFade case m_dismiss__animated_animated(Parameter) case m_removeLastView__controllers_controllers(Parameter) - case m_showMainScreen - case m_showLoginScreen - case m_showRegisterScreen + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) @@ -1239,14 +1389,37 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) return Matcher.ComparisonResult(results) - case (.m_showMainScreen, .m_showMainScreen): return .match + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) - case (.m_showLoginScreen, .m_showLoginScreen): return .match + case (.m_showStartupScreen, .m_showStartupScreen): return .match - case (.m_showRegisterScreen, .m_showRegisterScreen): return .match + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -1291,10 +1464,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return 0 case let .m_dismiss__animated_animated(p0): return p0.intValue case let .m_removeLastView__controllers_controllers(p0): return p0.intValue - case .m_showMainScreen: return 0 - case .m_showLoginScreen: return 0 - case .m_showRegisterScreen: return 0 + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue @@ -1308,10 +1484,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return ".backWithFade()" case .m_dismiss__animated_animated: return ".dismiss(animated:)" case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" - case .m_showMainScreen: return ".showMainScreen()" - case .m_showLoginScreen: return ".showLoginScreen()" - case .m_showRegisterScreen: return ".showRegisterScreen()" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" @@ -1339,10 +1518,13 @@ open class BaseRouterMock: BaseRouter, Mock { public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} - public static func showMainScreen() -> Verify { return Verify(method: .m_showMainScreen)} - public static func showLoginScreen() -> Verify { return Verify(method: .m_showLoginScreen)} - public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} @@ -1368,18 +1550,27 @@ open class BaseRouterMock: BaseRouter, Mock { public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) } - public static func showMainScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showMainScreen, performs: perform) + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } - public static func showLoginScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showLoginScreen, performs: perform) + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) } - public static func showRegisterScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showRegisterScreen, performs: perform) + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showForgotPasswordScreen, performs: perform) } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1703,6 +1894,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var currentDownloadTask: DownloadDataTask? { + get { invocations.append(.p_currentDownloadTask_get); return __p_currentDownloadTask ?? optionalGivenGetterValue(.p_currentDownloadTask_get, "DownloadManagerProtocolMock - stub value for currentDownloadTask was not defined") } + } + private var __p_currentDownloadTask: (DownloadDataTask)? + @@ -1721,29 +1917,44 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func eventPublisher() -> AnyPublisher { + addInvocation(.m_eventPublisher) + let perform = methodPerformValue(.m_eventPublisher) as? () -> Void + perform?() + var __value: AnyPublisher do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void - } catch MockError.notStubed { - // do nothing + __value = try methodReturnValue(.m_eventPublisher).casted() } catch { - throw error + onFatalFailure("Stub return value not specified for eventPublisher(). Use given") + Failure("Stub return value not specified for eventPublisher(). Use given") } + return __value } - open func getDownloadsForCourse(_ courseId: String) -> [DownloadData] { - addInvocation(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) - let perform = methodPerformValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + open func getDownloadTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasks) + let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasks(). Use given") + Failure("Stub return value not specified for getDownloadTasks(). Use given") + } + return __value + } + + open func getDownloadTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void perform?(`courseId`) - var __value: [DownloadData] + var __value: [DownloadDataTask] do { - __value = try methodReturnValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))).casted() + __value = try methodReturnValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))).casted() } catch { - onFatalFailure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") - Failure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") + onFatalFailure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") } return __value } @@ -1761,12 +1972,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() + open func cancelDownloading(task: DownloadDataTask) throws { + addInvocation(.m_cancelDownloading__task_task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_cancelDownloading__task_task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + _ = try methodReturnValue(.m_cancelDownloading__task_task(Parameter.value(`task`))).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -1774,10 +1985,17 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func pauseDownloading() { - addInvocation(.m_pauseDownloading) - let perform = methodPerformValue(.m_pauseDownloading) as? () -> Void - perform?() + open func cancelDownloading(courseId: String) throws { + addInvocation(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } } open func deleteFile(blocks: [CourseBlock]) { @@ -1805,28 +2023,72 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + addInvocation(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + Failure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + } + return __value + } + + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + fileprivate enum MethodType { case m_publisher - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_getDownloadsForCourse__courseId(Parameter) + case m_eventPublisher + case m_getDownloadTasks + case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) - case m_resumeDownloading - case m_pauseDownloading + case m_cancelDownloading__task_task(Parameter) + case m_cancelDownloading__courseId_courseId(Parameter) case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_resumeDownloading + case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { case (.m_publisher, .m_publisher): return .match - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_eventPublisher, .m_eventPublisher): return .match - case (.m_getDownloadsForCourse__courseId(let lhsCourseid), .m_getDownloadsForCourse__courseId(let rhsCourseid)): + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match + + case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) return Matcher.ComparisonResult(results) @@ -1837,9 +2099,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.m_cancelDownloading__task_task(let lhsTask), .m_cancelDownloading__task_task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "task")) + return Matcher.ComparisonResult(results) - case (.m_pauseDownloading, .m_pauseDownloading): return .match + case (.m_cancelDownloading__courseId_courseId(let lhsCourseid), .m_cancelDownloading__courseId_courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + return Matcher.ComparisonResult(results) case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] @@ -1852,6 +2120,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } } @@ -1859,27 +2140,37 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { func intValue() -> Int { switch self { case .m_publisher: return 0 - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_getDownloadsForCourse__courseId(p0): return p0.intValue + case .m_eventPublisher: return 0 + case .m_getDownloadTasks: return 0 + case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue - case .m_resumeDownloading: return 0 - case .m_pauseDownloading: return 0 + case let .m_cancelDownloading__task_task(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_resumeDownloading: return 0 + case .p_currentDownloadTask_get: return 0 } } func assertionName() -> String { switch self { case .m_publisher: return ".publisher()" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_getDownloadsForCourse__courseId: return ".getDownloadsForCourse(_:)" + case .m_eventPublisher: return ".eventPublisher()" + case .m_getDownloadTasks: return ".getDownloadTasks()" + case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" - case .m_resumeDownloading: return ".resumeDownloading()" - case .m_pauseDownloading: return ".pauseDownloading()" + case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" + case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_resumeDownloading: return ".resumeDownloading()" + case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } } @@ -1892,16 +2183,28 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { super.init(products) } + public static func currentDownloadTask(getter defaultValue: DownloadDataTask?...) -> PropertyStub { + return Given(method: .p_currentDownloadTask_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willReturn: AnyPublisher...) -> MethodStub { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getDownloadsForCourse(_ courseId: Parameter, willReturn: [DownloadData]...) -> MethodStub { - return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func eventPublisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { let willReturn: [AnyPublisher] = [] let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -1909,10 +2212,24 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func getDownloadsForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadData]>) -> Void) -> MethodStub { - let willReturn: [[DownloadData]] = [] - let given: Given = { return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([DownloadData]).self) + public static func eventPublisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func getDownloadTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) willProduce(stubber) return given } @@ -1923,13 +2240,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) willProduce(stubber) return given } @@ -1943,6 +2257,36 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func cancelDownloading(task: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(task: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func resumeDownloading(willThrow: Error...) -> MethodStub { return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) } @@ -1959,14 +2303,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { fileprivate var method: MethodType public static func publisher() -> Verify { return Verify(method: .m_publisher)} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func getDownloadsForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadsForCourse__courseId(`courseId`))} + public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} + public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} - public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} - public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} + public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} + public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } public struct Perform { @@ -1976,20 +2325,23 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_publisher, performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func eventPublisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_eventPublisher, performs: perform) } - public static func getDownloadsForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getDownloadsForCourse__courseId(`courseId`), performs: perform) + public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadTasks, performs: perform) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadTasksForCourse__courseId(`courseId`), performs: perform) } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, perform: @escaping (String, [CourseBlock]) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + public static func cancelDownloading(task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__task_task(`task`), performs: perform) } - public static func pauseDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_pauseDownloading, performs: perform) + public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) @@ -2000,6 +2352,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) + } + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) + } } public func given(_ method: Given) { diff --git a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift index d478f0f68..e824ad975 100644 --- a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift @@ -29,17 +29,19 @@ final class SignInViewModelTests: XCTestCase { let analytics = AuthorizationAnalyticsMock() let viewModel = SignInViewModel( interactor: interactor, - router: router, + router: router, + config: ConfigMock(), analytics: analytics, - validator: validator + validator: validator, + sourceScreen: .default ) - await viewModel.login(username: "email", password: "") + await viewModel.login(username: "", password: "") Verify(interactor, 0, .login(username: .any, password: .any)) - Verify(router, 0, .showMainScreen()) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) - XCTAssertEqual(viewModel.errorMessage, AuthLocalization.Error.invalidEmailAddress) + XCTAssertEqual(viewModel.errorMessage, AuthLocalization.Error.invalidEmailAddressOrUsername) XCTAssertEqual(viewModel.isShowProgress, false) } @@ -51,13 +53,15 @@ final class SignInViewModelTests: XCTestCase { let viewModel = SignInViewModel( interactor: interactor, router: router, + config: ConfigMock(), analytics: analytics, - validator: validator + validator: validator, + sourceScreen: .default ) await viewModel.login(username: "edxUser@edx.com", password: "") Verify(interactor, 0, .login(username: .any, password: .any)) - Verify(router, 0, .showMainScreen()) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.errorMessage, AuthLocalization.Error.invalidPasswordLenght) XCTAssertEqual(viewModel.isShowProgress, false) @@ -71,8 +75,10 @@ final class SignInViewModelTests: XCTestCase { let viewModel = SignInViewModel( interactor: interactor, router: router, + config: ConfigMock(), analytics: analytics, - validator: validator + validator: validator, + sourceScreen: .default ) let user = User(id: 1, username: "username", email: "edxUser@edx.com", name: "Name", userAvatar: "") @@ -82,12 +88,78 @@ final class SignInViewModelTests: XCTestCase { Verify(interactor, 1, .login(username: .any, password: .any)) Verify(analytics, .userLogin(method: .any)) - Verify(router, 1, .showMainScreen()) + Verify(router, 1, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.errorMessage, nil) XCTAssertEqual(viewModel.isShowProgress, true) } - + + func testSocialLoginSuccess() async throws { + let interactor = AuthInteractorProtocolMock() + let router = AuthorizationRouterMock() + let validator = Validator() + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel( + interactor: interactor, + router: router, + config: ConfigMock(), + analytics: analytics, + validator: validator, + sourceScreen: .default + ) + + 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) + + Verify(interactor, 1, .login(externalToken: .any, backend: .any)) + Verify(analytics, .userLogin(method: .any)) + Verify(router, 1, .showMainOrWhatsNewScreen(sourceScreen: .any)) + + XCTAssertEqual(viewModel.errorMessage, nil) + XCTAssertEqual(viewModel.isShowProgress, true) + } + + func testSocialLoginErrorValidation() async throws { + let interactor = AuthInteractorProtocolMock() + let router = AuthorizationRouterMock() + let validator = Validator() + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel( + interactor: interactor, + router: router, + config: ConfigMock(), + analytics: analytics, + validator: validator, + sourceScreen: .default + ) + + let result: Result = .success( + .apple(.init(name: "name", email: "email", token: "239i2oi3jrf2jflkj23lf2f")) + ) + let validationErrorMessage = AuthLocalization.Error.accountNotRegistered( + AuthMethod.socailAuth(.apple).analyticsValue, + viewModel.config.platformName + ) + let validationError = CustomValidationError(statusCode: 400, data: ["error_description": validationErrorMessage]) + let error = AFError.responseValidationFailed(reason: AFError.ResponseValidationFailureReason.customValidationFailed(error: validationError)) + + Given(interactor, .login(externalToken: .any, backend: .any, willThrow: error)) + + await viewModel.login(with: result) + + Verify(interactor, 1, .login(externalToken: .any, backend: .any)) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) + + XCTAssertEqual(viewModel.errorMessage, validationErrorMessage) + XCTAssertEqual(viewModel.isShowProgress, false) + } + func testLoginErrorValidation() async throws { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() @@ -96,8 +168,10 @@ final class SignInViewModelTests: XCTestCase { let viewModel = SignInViewModel( interactor: interactor, router: router, + config: ConfigMock(), analytics: analytics, - validator: validator + validator: validator, + sourceScreen: .default ) let validationErrorMessage = "Some error" @@ -109,12 +183,12 @@ final class SignInViewModelTests: XCTestCase { await viewModel.login(username: "edxUser@edx.com", password: "password123") Verify(interactor, 1, .login(username: .any, password: .any)) - Verify(router, 0, .showMainScreen()) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.errorMessage, validationErrorMessage) XCTAssertEqual(viewModel.isShowProgress, false) } - + func testLoginErrorInvalidGrant() async throws { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() @@ -123,8 +197,10 @@ final class SignInViewModelTests: XCTestCase { let viewModel = SignInViewModel( interactor: interactor, router: router, + config: ConfigMock(), analytics: analytics, - validator: validator + validator: validator, + sourceScreen: .default ) Given(interactor, .login(username: .any, password: .any, willThrow: APIError.invalidGrant)) @@ -132,7 +208,7 @@ final class SignInViewModelTests: XCTestCase { await viewModel.login(username: "edxUser@edx.com", password: "password123") Verify(interactor, 1, .login(username: .any, password: .any)) - Verify(router, 0, .showMainScreen()) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.invalidCredentials) XCTAssertEqual(viewModel.isShowProgress, false) @@ -146,8 +222,10 @@ final class SignInViewModelTests: XCTestCase { let viewModel = SignInViewModel( interactor: interactor, router: router, + config: ConfigMock(), analytics: analytics, - validator: validator + validator: validator, + sourceScreen: .default ) Given(interactor, .login(username: .any, password: .any, willThrow: NSError())) @@ -155,7 +233,7 @@ final class SignInViewModelTests: XCTestCase { await viewModel.login(username: "edxUser@edx.com", password: "password123") Verify(interactor, 1, .login(username: .any, password: .any)) - Verify(router, 0, .showMainScreen()) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) XCTAssertEqual(viewModel.isShowProgress, false) @@ -169,8 +247,10 @@ final class SignInViewModelTests: XCTestCase { let viewModel = SignInViewModel( interactor: interactor, router: router, + config: ConfigMock(), analytics: analytics, - validator: validator + validator: validator, + sourceScreen: .default ) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -180,29 +260,12 @@ final class SignInViewModelTests: XCTestCase { await viewModel.login(username: "edxUser@edx.com", password: "password123") Verify(interactor, 1, .login(username: .any, password: .any)) - Verify(router, 0, .showMainScreen()) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) XCTAssertEqual(viewModel.isShowProgress, false) } - func testTrackSignUpClicked() { - let interactor = AuthInteractorProtocolMock() - let router = AuthorizationRouterMock() - let validator = Validator() - let analytics = AuthorizationAnalyticsMock() - let viewModel = SignInViewModel( - interactor: interactor, - router: router, - analytics: analytics, - validator: validator - ) - - viewModel.trackSignUpClicked() - - Verify(analytics, 1, .signUpClicked()) - } - func testTrackForgotPasswordClicked() { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() @@ -211,8 +274,10 @@ final class SignInViewModelTests: XCTestCase { let viewModel = SignInViewModel( interactor: interactor, router: router, + config: ConfigMock(), analytics: analytics, - validator: validator + validator: validator, + sourceScreen: .default ) viewModel.trackForgotPasswordClicked() diff --git a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift index 8699b79fc..ad180a925 100644 --- a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift @@ -33,7 +33,8 @@ final class SignUpViewModelTests: XCTestCase { analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: validator + validator: validator, + sourceScreen: .default ) let fields = [ @@ -64,7 +65,8 @@ final class SignUpViewModelTests: XCTestCase { analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: validator + validator: validator, + sourceScreen: .default ) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -90,7 +92,8 @@ final class SignUpViewModelTests: XCTestCase { analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: validator + validator: validator, + sourceScreen: .default ) Given(interactor, .getRegistrationFields(willThrow: NSError())) @@ -114,10 +117,11 @@ final class SignUpViewModelTests: XCTestCase { analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: validator + validator: validator, + sourceScreen: .default ) - Given(interactor, .registerUser(fields: .any, willReturn: .init(id: 1, + Given(interactor, .registerUser(fields: .any, isSocial: .any, willReturn: .init(id: 1, username: "Name", email: "mail", name: "name", @@ -127,8 +131,8 @@ final class SignUpViewModelTests: XCTestCase { await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 1, .registerUser(fields: .any)) - Verify(router, 1, .showMainScreen()) + Verify(interactor, 1, .registerUser(fields: .any, isSocial: .any)) + Verify(router, 1, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.isShowProgress, false) XCTAssertEqual(viewModel.showError, false) @@ -145,7 +149,8 @@ final class SignUpViewModelTests: XCTestCase { analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: validator + validator: validator, + sourceScreen: .default ) viewModel.fields = [ @@ -158,13 +163,13 @@ final class SignUpViewModelTests: XCTestCase { ] Given(interactor, .validateRegistrationFields(fields: .any, willReturn: ["email": "invalid email"])) - Given(interactor, .registerUser(fields: .any, willProduce: {_ in})) - + Given(interactor, .registerUser(fields: .any, isSocial: .any, willProduce: {_ in})) + await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 0, .registerUser(fields: .any)) - Verify(router, 0, .showMainScreen()) + Verify(interactor, 0, .registerUser(fields: .any, isSocial: .any)) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.isShowProgress, false) XCTAssertEqual(viewModel.showError, false) @@ -182,17 +187,18 @@ final class SignUpViewModelTests: XCTestCase { analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: validator + validator: validator, + sourceScreen: .default ) Given(interactor, .validateRegistrationFields(fields: .any, willReturn: [:])) - Given(interactor, .registerUser(fields: .any, willThrow: APIError.invalidGrant)) - + Given(interactor, .registerUser(fields: .any, isSocial: .any, willThrow: APIError.invalidGrant)) + await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 1, .registerUser(fields: .any)) - Verify(router, 0, .showMainScreen()) + Verify(interactor, 1, .registerUser(fields: .any, isSocial: .any)) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.isShowProgress, false) XCTAssertEqual(viewModel.showError, true) @@ -210,17 +216,18 @@ final class SignUpViewModelTests: XCTestCase { analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: validator + validator: validator, + sourceScreen: .default ) Given(interactor, .validateRegistrationFields(fields: .any, willReturn: [:])) - Given(interactor, .registerUser(fields: .any, willThrow: NSError())) + Given(interactor, .registerUser(fields: .any, isSocial: .any, willThrow: NSError())) await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 1, .registerUser(fields: .any)) - Verify(router, 0, .showMainScreen()) + Verify(interactor, 1, .registerUser(fields: .any, isSocial: .any)) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.isShowProgress, false) XCTAssertEqual(viewModel.showError, true) @@ -238,19 +245,20 @@ final class SignUpViewModelTests: XCTestCase { analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: validator + validator: validator, + sourceScreen: .default ) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) - Given(interactor, .registerUser(fields: .any, willThrow: noInternetError)) + Given(interactor, .registerUser(fields: .any, isSocial: .any, willThrow: noInternetError)) Given(interactor, .validateRegistrationFields(fields: .any, willReturn: [:])) await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 1, .registerUser(fields: .any)) - Verify(router, 0, .showMainScreen()) + Verify(interactor, 1, .registerUser(fields: .any, isSocial: .any)) + Verify(router, 0, .showMainOrWhatsNewScreen(sourceScreen: .any)) XCTAssertEqual(viewModel.isShowProgress, false) XCTAssertEqual(viewModel.showError, true) @@ -268,7 +276,8 @@ final class SignUpViewModelTests: XCTestCase { analytics: analytics, config: ConfigMock(), cssInjector: CSSInjectorMock(), - validator: validator + validator: validator, + sourceScreen: .default ) viewModel.trackCreateAccountClicked() diff --git a/Authorization/Mockfile b/Authorization/Mockfile index da0f47ddb..5e0805e28 100644 --- a/Authorization/Mockfile +++ b/Authorization/Mockfile @@ -1,5 +1,5 @@ -sourceryCommand: null -sourceryTemplate: null +sourceryCommand: mint run krzysztofzablocki/Sourcery@2.1.2 sourcery +sourceryTemplate: ../MockTemplate.swifttemplate unit.tests.mock: sources: include: diff --git a/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 72% rename from OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 0c67376eb..18d981003 100644 --- a/OpenEdX.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/Core/Core.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -1,5 +1,8 @@ - + + IDEDidComputeMac32BitWarning + + diff --git a/Core/Core.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Core/Core.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..7dde19a07 --- /dev/null +++ b/Core/Core.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "youtubeplayerkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SvenTiigi/YouTubePlayerKit", + "state" : { + "revision" : "1fe4c8b07a61d50c2fd276e1d9c8087583c7638a", + "version" : "1.5.3" + } + } + ], + "version" : 2 +} diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index eed739e2e..d788284c8 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */; }; 022C64E429AE0191000F532B /* TextWithUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64E329AE0191000F532B /* TextWithUrls.swift */; }; 0231CDBE2922422D00032416 /* CSSInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0231CDBD2922422D00032416 /* CSSInjector.swift */; }; + 0233D56F2AF13EB200BAC8BD /* StarRatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0233D56E2AF13EB200BAC8BD /* StarRatingView.swift */; }; + 0233D5712AF13EC800BAC8BD /* SelectMailClientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0233D5702AF13EC800BAC8BD /* SelectMailClientView.swift */; }; + 0233D5732AF13EEE00BAC8BD /* AppReviewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0233D5722AF13EEE00BAC8BD /* AppReviewButton.swift */; }; 0236961928F9A26900EEF206 /* AuthRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0236961828F9A26900EEF206 /* AuthRepository.swift */; }; 0236961B28F9A28B00EEF206 /* AuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0236961A28F9A28B00EEF206 /* AuthInteractor.swift */; }; 0236961D28F9A2D200EEF206 /* Data_AuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0236961C28F9A2D200EEF206 /* Data_AuthResponse.swift */; }; @@ -29,7 +32,6 @@ 0248C92329C075EF00DC8402 /* CourseBlockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248C92229C075EF00DC8402 /* CourseBlockModel.swift */; }; 024BE3DF29B2615500BCDEE2 /* CGColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024BE3DE29B2615500BCDEE2 /* CGColorExtension.swift */; }; 024D723529C8BB1A006D36ED /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024D723429C8BB1A006D36ED /* NavigationBar.swift */; }; - 024D865E28F02C6B0077E0A0 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024D865D28F02C6B0077E0A0 /* WebView.swift */; }; 024FCD0028EF1CD300232339 /* WebBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024FCCFF28EF1CD300232339 /* WebBrowser.swift */; }; 02512FF0299533DF0024D438 /* CoreDataHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02512FEF299533DE0024D438 /* CoreDataHandlerProtocol.swift */; }; 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0255D55729362839004DBC1A /* UploadBodyEncoding.swift */; }; @@ -52,7 +54,6 @@ 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3BA2909478B00392132 /* UIView+EnclosingScrollView.swift */; }; 027BD3BE2909478B00392132 /* UIResponder+CurrentResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3BB2909478B00392132 /* UIResponder+CurrentResponder.swift */; }; 027BD3C52909707700392132 /* Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3C42909707700392132 /* Shake.swift */; }; - 027DB33528D8C8FE002B6862 /* Data_MyCourse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33428D8C8FE002B6862 /* Data_MyCourse.swift */; }; 0282DA7328F98CC9003C3F07 /* WebUnitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0282DA7228F98CC9003C3F07 /* WebUnitView.swift */; }; 0283347D28D4D3DE00C828FC /* Data_Discovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283347C28D4D3DE00C828FC /* Data_Discovery.swift */; }; 0283348028D4DCD200C828FC /* ViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283347F28D4DCD200C828FC /* ViewExtension.swift */; }; @@ -60,24 +61,36 @@ 028CE96929858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */; }; 028F9F37293A44C700DE65D0 /* Data_ResetPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */; }; 028F9F39293A452B00DE65D0 /* ResetPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028F9F38293A452B00DE65D0 /* ResetPassword.swift */; }; - 0295B1DC297FF114003B0C65 /* SF-Pro.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0295B1DA297FF0E9003B0C65 /* SF-Pro.ttf */; }; 0295C885299B99DD00ABE571 /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */; }; + 02A463112AEA966C00331037 /* AppReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A463102AEA966C00331037 /* AppReviewView.swift */; }; 02A4833529B8A73400D33F33 /* CorePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */; }; 02A4833829B8A8F900D33F33 /* CoreDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833629B8A8F800D33F33 /* CoreDataModel.xcdatamodeld */; }; 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833929B8A9AB00D33F33 /* DownloadManager.swift */; }; 02A4833C29B8C57800D33F33 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833B29B8C57800D33F33 /* DownloadView.swift */; }; + 02AFCC182AEFDB24000360F0 /* ThirdPartyMailClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AFCC172AEFDB24000360F0 /* ThirdPartyMailClient.swift */; }; + 02AFCC1A2AEFDC18000360F0 /* ThirdPartyMailer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AFCC192AEFDC18000360F0 /* ThirdPartyMailer.swift */; }; 02B2B594295C5C7A00914876 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B2B593295C5C7A00914876 /* Thread.swift */; }; 02B3E3B32930198600A50475 /* AVPlayerViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */; }; 02B3F16E2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */; }; - 02C2DC0829B63D6200F4445D /* WebViewHTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C2DC0729B63D6200F4445D /* WebViewHTML.swift */; }; 02C917F029CDA99E00DBB8BD /* Data_Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */; }; 02CF46C829546AA200A698EE /* NoCachedDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CF46C729546AA200A698EE /* NoCachedDataError.swift */; }; + 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */; }; 02D800CC29348F460099CF16 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D800CB29348F460099CF16 /* ImagePicker.swift */; }; 02E225B0291D29EB0067769A /* UrlExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E225AF291D29EB0067769A /* UrlExtension.swift */; }; + 02E93F852AEBAEBC006C4750 /* AppReviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E93F842AEBAEBC006C4750 /* AppReviewViewModel.swift */; }; 02F164372902A9EB0090DDEF /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F164362902A9EB0090DDEF /* StringExtension.swift */; }; 02F6EF3B28D9B8EC00835477 /* CourseCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F6EF3A28D9B8EC00835477 /* CourseCellView.swift */; }; 02F6EF4A28D9F0A700835477 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F6EF4928D9F0A700835477 /* DateExtension.swift */; }; 02F98A7F28F81EE900DE94C0 /* Container+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F98A7E28F81EE900DE94C0 /* Container+App.swift */; }; + 0604C9AA2B22FACF00AD5DBF /* UIComponentsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0604C9A92B22FACF00AD5DBF /* UIComponentsConfig.swift */; }; + 064987932B4D69FF0071642A /* DragAndDropCssInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0649878A2B4D69FE0071642A /* DragAndDropCssInjection.swift */; }; + 064987942B4D69FF0071642A /* WebviewInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0649878B2B4D69FE0071642A /* WebviewInjection.swift */; }; + 064987952B4D69FF0071642A /* SurveyCssInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0649878C2B4D69FE0071642A /* SurveyCssInjection.swift */; }; + 064987962B4D69FF0071642A /* WebviewMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0649878D2B4D69FE0071642A /* WebviewMessage.swift */; }; + 064987972B4D69FF0071642A /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0649878E2B4D69FE0071642A /* WebView.swift */; }; + 064987982B4D69FF0071642A /* CSSInjectionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064987902B4D69FE0071642A /* CSSInjectionProtocol.swift */; }; + 064987992B4D69FF0071642A /* WebViewScriptInjectionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064987912B4D69FE0071642A /* WebViewScriptInjectionProtocol.swift */; }; + 0649879A2B4D69FF0071642A /* WebViewHTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064987922B4D69FE0071642A /* WebViewHTML.swift */; }; 070019A528F6F17900D5FC78 /* Data_Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070019A428F6F17900D5FC78 /* Data_Media.swift */; }; 070019AC28F6FD0100D5FC78 /* CourseDetailBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070019AB28F6FD0100D5FC78 /* CourseDetailBlock.swift */; }; 070019AE28F701B200D5FC78 /* Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070019AD28F701B200D5FC78 /* Certificate.swift */; }; @@ -109,11 +122,43 @@ 0770DE5B28D0B209006D8A5D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE5D28D0B209006D8A5D /* Localizable.strings */; }; 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE5E28D0B22C006D8A5D /* Strings.swift */; }; 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE6028D0B2CB006D8A5D /* Assets.swift */; }; - 0770DE7928D0C4A9006D8A5D /* RoundedCorners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE7828D0C4A9006D8A5D /* RoundedCorners.swift */; }; - 0770DE7B28D0C78C006D8A5D /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE7A28D0C78C006D8A5D /* Theme.swift */; }; 07DDFCBD29A780BB00572595 /* UINavigationController+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */; }; + 07E0939F2B308D2800F1E4B2 /* Data_Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */; }; + 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */; }; + A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; }; + BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA30427D2B20B299009B64B7 /* SocialAuthError.swift */; }; + BA4AFB442B6A5AF100A21367 /* CheckBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AFB432B6A5AF100A21367 /* CheckBoxView.swift */; }; + BA4AFB422B5A7A0900A21367 /* VideoDownloadQualityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */; }; + BA593F1C2AF8E498009ADB51 /* ScrollSlidingTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA593F1B2AF8E498009ADB51 /* ScrollSlidingTabBar.swift */; }; + BA593F1E2AF8E4A0009ADB51 /* FrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA593F1D2AF8E4A0009ADB51 /* FrameReader.swift */; }; + BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */; }; + BA8B3A2F2AD546A700D25EF5 /* DebugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */; }; + BA8FA6612AD5974300EA029A /* AppleAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA6602AD5974300EA029A /* AppleAuthProvider.swift */; }; + BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */; }; + BA8FA66A2AD59B5500EA029A /* GoogleAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */; }; + BA8FA66C2AD59BBC00EA029A /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */; }; + BA8FA66E2AD59E7D00EA029A /* FacebookAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */; }; + BA8FA6702AD59EA300EA029A /* MicrosoftAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */; }; + BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */; }; + BAD9CA2F2B289B3500DE790A /* ajaxHandler.js in Resources */ = {isa = PBXBuildFile; fileRef = BAD9CA2E2B289B3500DE790A /* ajaxHandler.js */; }; + BAD9CA332B28A8F300DE790A /* AjaxProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA322B28A8F300DE790A /* AjaxProvider.swift */; }; + BAD9CA422B2B140100DE790A /* AgreementConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */; }; + BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */; }; + BAF0D4CB2AD6AE14007AC334 /* FacebookLogin in Frameworks */ = {isa = PBXBuildFile; productRef = BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */; }; + BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */; }; + BAFB99842B0E282E007D09F9 /* MicrosoftConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */; }; + BAFB99902B14B377007D09F9 /* GoogleConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */; }; + BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */; }; C8C446EF233F81B9FABB77D2 /* Pods_App_Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */; }; CFC84952299F8B890055E497 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC84951299F8B890055E497 /* Debounce.swift */; }; + DBF6F2412B014ADA0098414B /* FirebaseConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF6F2402B014ADA0098414B /* FirebaseConfig.swift */; }; + DBF6F2462B01DAFE0098414B /* AgreementConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF6F2452B01DAFE0098414B /* AgreementConfig.swift */; }; + DBF6F24A2B0380E00098414B /* FeaturesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF6F2492B0380E00098414B /* FeaturesConfig.swift */; }; + E055A5392B18DC95008D9E5E /* Theme.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E055A5382B18DC95008D9E5E /* Theme.framework */; }; + E09179FD2B0F204E002AB695 /* ConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09179FC2B0F204D002AB695 /* ConfigTests.swift */; }; + E0D5861A2B2FF74C009B4BA7 /* DiscoveryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */; }; + E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */; }; + E0D586362B314CD3009B4BA7 /* LogistrationBottomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -137,6 +182,9 @@ 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; 022C64E329AE0191000F532B /* TextWithUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithUrls.swift; sourceTree = ""; }; 0231CDBD2922422D00032416 /* CSSInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSSInjector.swift; sourceTree = ""; }; + 0233D56E2AF13EB200BAC8BD /* StarRatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarRatingView.swift; sourceTree = ""; }; + 0233D5702AF13EC800BAC8BD /* SelectMailClientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectMailClientView.swift; sourceTree = ""; }; + 0233D5722AF13EEE00BAC8BD /* AppReviewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewButton.swift; sourceTree = ""; }; 0236961828F9A26900EEF206 /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRepository.swift; sourceTree = ""; }; 0236961A28F9A28B00EEF206 /* AuthInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthInteractor.swift; sourceTree = ""; }; 0236961C28F9A2D200EEF206 /* Data_AuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_AuthResponse.swift; sourceTree = ""; }; @@ -149,7 +197,6 @@ 0248C92229C075EF00DC8402 /* CourseBlockModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseBlockModel.swift; sourceTree = ""; }; 024BE3DE29B2615500BCDEE2 /* CGColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGColorExtension.swift; sourceTree = ""; }; 024D723429C8BB1A006D36ED /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; - 024D865D28F02C6B0077E0A0 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; 024FCCFF28EF1CD300232339 /* WebBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebBrowser.swift; sourceTree = ""; }; 02512FEF299533DE0024D438 /* CoreDataHandlerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandlerProtocol.swift; sourceTree = ""; }; 0255D55729362839004DBC1A /* UploadBodyEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadBodyEncoding.swift; sourceTree = ""; }; @@ -171,7 +218,6 @@ 027BD3BA2909478B00392132 /* UIView+EnclosingScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+EnclosingScrollView.swift"; sourceTree = ""; }; 027BD3BB2909478B00392132 /* UIResponder+CurrentResponder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIResponder+CurrentResponder.swift"; sourceTree = ""; }; 027BD3C42909707700392132 /* Shake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shake.swift; sourceTree = ""; }; - 027DB33428D8C8FE002B6862 /* Data_MyCourse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_MyCourse.swift; sourceTree = ""; }; 0282DA7228F98CC9003C3F07 /* WebUnitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebUnitView.swift; sourceTree = ""; }; 0283347C28D4D3DE00C828FC /* Data_Discovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Discovery.swift; sourceTree = ""; }; 0283347F28D4DCD200C828FC /* ViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtension.swift; sourceTree = ""; }; @@ -179,25 +225,37 @@ 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleKeyboardInputView.swift; sourceTree = ""; }; 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_ResetPassword.swift; sourceTree = ""; }; 028F9F38293A452B00DE65D0 /* ResetPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPassword.swift; sourceTree = ""; }; - 0295B1DA297FF0E9003B0C65 /* SF-Pro.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro.ttf"; sourceTree = ""; }; 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; + 02A463102AEA966C00331037 /* AppReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewView.swift; sourceTree = ""; }; 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePersistenceProtocol.swift; sourceTree = ""; }; 02A4833729B8A8F800D33F33 /* CoreDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataModel.xcdatamodel; sourceTree = ""; }; 02A4833929B8A9AB00D33F33 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; 02A4833B29B8C57800D33F33 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; + 02AFCC172AEFDB24000360F0 /* ThirdPartyMailClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyMailClient.swift; sourceTree = ""; }; + 02AFCC192AEFDC18000360F0 /* ThirdPartyMailer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyMailer.swift; sourceTree = ""; }; 02B2B593295C5C7A00914876 /* Thread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thread.swift; sourceTree = ""; }; 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerViewControllerExtension.swift; sourceTree = ""; }; 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollViewCompat.swift; sourceTree = ""; }; - 02C2DC0729B63D6200F4445D /* WebViewHTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewHTML.swift; sourceTree = ""; }; 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Dashboard.swift; sourceTree = ""; }; 02CF46C729546AA200A698EE /* NoCachedDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCachedDataError.swift; sourceTree = ""; }; + 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKStoreReviewControllerExtension.swift; sourceTree = ""; }; 02D800CB29348F460099CF16 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 02E225AF291D29EB0067769A /* UrlExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlExtension.swift; sourceTree = ""; }; + 02E93F842AEBAEBC006C4750 /* AppReviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewViewModel.swift; sourceTree = ""; }; 02ED50CB29A64B84008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02F164362902A9EB0090DDEF /* StringExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 02F6EF3A28D9B8EC00835477 /* CourseCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseCellView.swift; sourceTree = ""; }; 02F6EF4928D9F0A700835477 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; 02F98A7E28F81EE900DE94C0 /* Container+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Container+App.swift"; sourceTree = ""; }; + 0604C9A92B22FACF00AD5DBF /* UIComponentsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIComponentsConfig.swift; sourceTree = ""; }; + 0649878A2B4D69FE0071642A /* DragAndDropCssInjection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragAndDropCssInjection.swift; sourceTree = ""; }; + 0649878B2B4D69FE0071642A /* WebviewInjection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebviewInjection.swift; sourceTree = ""; }; + 0649878C2B4D69FE0071642A /* SurveyCssInjection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurveyCssInjection.swift; sourceTree = ""; }; + 0649878D2B4D69FE0071642A /* WebviewMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebviewMessage.swift; sourceTree = ""; }; + 0649878E2B4D69FE0071642A /* WebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; + 064987902B4D69FE0071642A /* CSSInjectionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSSInjectionProtocol.swift; sourceTree = ""; }; + 064987912B4D69FE0071642A /* WebViewScriptInjectionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewScriptInjectionProtocol.swift; sourceTree = ""; }; + 064987922B4D69FE0071642A /* WebViewHTML.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewHTML.swift; sourceTree = ""; }; 070019A428F6F17900D5FC78 /* Data_Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Media.swift; sourceTree = ""; }; 070019AB28F6FD0100D5FC78 /* CourseDetailBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetailBlock.swift; sourceTree = ""; }; 070019AD28F701B200D5FC78 /* Certificate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Certificate.swift; sourceTree = ""; }; @@ -231,18 +289,48 @@ 0770DE5C28D0B209006D8A5D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 0770DE5E28D0B22C006D8A5D /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 0770DE6028D0B2CB006D8A5D /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; - 0770DE7828D0C4A9006D8A5D /* RoundedCorners.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCorners.swift; sourceTree = ""; }; - 0770DE7A28D0C78C006D8A5D /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Animation.swift"; sourceTree = ""; }; + 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Certificate.swift; sourceTree = ""; }; 0E13E9173C9C4CFC19F8B6F2 /* Pods-App-Core.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugstage.xcconfig"; sourceTree = ""; }; + 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebviewCookiesUpdateProtocol.swift; sourceTree = ""; }; 1A154A95AF4EE85A4A1C083B /* Pods-App-Core.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releasedev.xcconfig"; sourceTree = ""; }; 2B7E6FE7843FC4CF2BFA712D /* Pods-App-Core.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debug.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debug.xcconfig"; sourceTree = ""; }; 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = ""; }; 60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = ""; }; 9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = ""; }; + A53A32342B233DEC005FE38A /* ThemeConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeConfig.swift; sourceTree = ""; }; + BA30427D2B20B299009B64B7 /* SocialAuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocialAuthError.swift; sourceTree = ""; }; + BA4AFB432B6A5AF100A21367 /* CheckBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxView.swift; sourceTree = ""; }; + BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQualityView.swift; sourceTree = ""; }; + BA593F1B2AF8E498009ADB51 /* ScrollSlidingTabBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollSlidingTabBar.swift; sourceTree = ""; }; + BA593F1D2AF8E4A0009ADB51 /* FrameReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrameReader.swift; sourceTree = ""; }; + BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthResponse.swift; sourceTree = ""; }; + BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLog.swift; sourceTree = ""; }; + BA8FA6602AD5974300EA029A /* AppleAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAuthProvider.swift; sourceTree = ""; }; + BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthButton.swift; sourceTree = ""; }; + BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthProvider.swift; sourceTree = ""; }; + BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthProvider.swift; sourceTree = ""; }; + BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftAuthProvider.swift; sourceTree = ""; }; + BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDisclosureGroup.swift; sourceTree = ""; }; + BAD9CA2E2B289B3500DE790A /* ajaxHandler.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = ajaxHandler.js; sourceTree = ""; }; + BAD9CA322B28A8F300DE790A /* AjaxProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AjaxProvider.swift; sourceTree = ""; }; + BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementConfigTests.swift; sourceTree = ""; }; + BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultExtension.swift; sourceTree = ""; }; + BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookConfig.swift; sourceTree = ""; }; + BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftConfig.swift; sourceTree = ""; }; + BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleConfig.swift; sourceTree = ""; }; + BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleSignInConfig.swift; sourceTree = ""; }; C7E5BCE79CE297B20777B27A /* Pods-App-Core.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugprod.xcconfig"; sourceTree = ""; }; CFC84951299F8B890055E497 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; + DBF6F2402B014ADA0098414B /* FirebaseConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseConfig.swift; sourceTree = ""; }; + DBF6F2452B01DAFE0098414B /* AgreementConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementConfig.swift; sourceTree = ""; }; + DBF6F2492B0380E00098414B /* FeaturesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturesConfig.swift; sourceTree = ""; }; + E055A5382B18DC95008D9E5E /* Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E09179FC2B0F204D002AB695 /* ConfigTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigTests.swift; sourceTree = ""; }; + E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryConfig.swift; sourceTree = ""; }; + E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawStringExtactable.swift; sourceTree = ""; }; + E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogistrationBottomView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -258,14 +346,27 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BAF0D4CB2AD6AE14007AC334 /* FacebookLogin in Frameworks */, 025EF2F62971740000B838AB /* YouTubePlayerKit in Frameworks */, C8C446EF233F81B9FABB77D2 /* Pods_App_Core.framework in Frameworks */, + BA8FA66C2AD59BBC00EA029A /* GoogleSignIn in Frameworks */, + E055A5392B18DC95008D9E5E /* Theme.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0233D56D2AF13EA400BAC8BD /* Elements */ = { + isa = PBXGroup; + children = ( + 0233D56E2AF13EB200BAC8BD /* StarRatingView.swift */, + 0233D5702AF13EC800BAC8BD /* SelectMailClientView.swift */, + 0233D5722AF13EEE00BAC8BD /* AppReviewButton.swift */, + ); + path = Elements; + sourceTree = ""; + }; 0236961728F9A21600EEF206 /* Repository */ = { isa = PBXGroup; children = ( @@ -341,16 +442,21 @@ 0727878228D31287002E9142 /* DispatchQueue+App.swift */, 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */, 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */, + BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */, + BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */, + 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */, + E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */, ); path = Extensions; sourceTree = ""; }; - 0295B1DB297FF0E9003B0C65 /* Fonts */ = { + 02AFCC162AEFDB0F000360F0 /* ThirdPartyMailer */ = { isa = PBXGroup; children = ( - 0295B1DA297FF0E9003B0C65 /* SF-Pro.ttf */, + 02AFCC172AEFDB24000360F0 /* ThirdPartyMailClient.swift */, + 02AFCC192AEFDC18000360F0 /* ThirdPartyMailer.swift */, ); - path = Fonts; + path = ThirdPartyMailer; sourceTree = ""; }; 02CF46C92954A42100A698EE /* Persistence */ = { @@ -363,12 +469,55 @@ path = Persistence; sourceTree = ""; }; + 02E93F862AEBAED4006C4750 /* AppReview */ = { + isa = PBXGroup; + children = ( + 0233D56D2AF13EA400BAC8BD /* Elements */, + 02AFCC162AEFDB0F000360F0 /* ThirdPartyMailer */, + 02A463102AEA966C00331037 /* AppReviewView.swift */, + 02E93F842AEBAEBC006C4750 /* AppReviewViewModel.swift */, + ); + path = AppReview; + sourceTree = ""; + }; + 064987882B4D69FE0071642A /* Webview */ = { + isa = PBXGroup; + children = ( + 064987892B4D69FE0071642A /* Models */, + 0649878E2B4D69FE0071642A /* WebView.swift */, + 0649878F2B4D69FE0071642A /* Protocols */, + 064987922B4D69FE0071642A /* WebViewHTML.swift */, + 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */, + ); + path = Webview; + sourceTree = ""; + }; + 064987892B4D69FE0071642A /* Models */ = { + isa = PBXGroup; + children = ( + 0649878A2B4D69FE0071642A /* DragAndDropCssInjection.swift */, + 0649878B2B4D69FE0071642A /* WebviewInjection.swift */, + 0649878C2B4D69FE0071642A /* SurveyCssInjection.swift */, + 0649878D2B4D69FE0071642A /* WebviewMessage.swift */, + ); + path = Models; + sourceTree = ""; + }; + 0649878F2B4D69FE0071642A /* Protocols */ = { + isa = PBXGroup; + children = ( + 064987902B4D69FE0071642A /* CSSInjectionProtocol.swift */, + 064987912B4D69FE0071642A /* WebViewScriptInjectionProtocol.swift */, + ); + path = Protocols; + sourceTree = ""; + }; 0727876E28D233EC002E9142 /* Configuration */ = { isa = PBXGroup; children = ( + DBF6F2422B014AF30098414B /* Config */, CFC84955299FAC4D0055E497 /* Combine */, 0770DE1828D0847D006D8A5D /* BaseRouter.swift */, - 0727876F28D23411002E9142 /* Config.swift */, 0231CDBD2922422D00032416 /* CSSInjector.swift */, 02280F5A294B4E6F0032823A /* Connectivity.swift */, ); @@ -394,13 +543,13 @@ 0727878428D31657002E9142 /* Data_User.swift */, 0283347C28D4D3DE00C828FC /* Data_Discovery.swift */, 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */, - 027DB33428D8C8FE002B6862 /* Data_MyCourse.swift */, 021D924728DC860C00ACC565 /* Data_UserProfile.swift */, 0259104929C4A5B6004B5A55 /* UserSettings.swift */, 070019A428F6F17900D5FC78 /* Data_Media.swift */, 0236961C28F9A2D200EEF206 /* Data_AuthResponse.swift */, 027BD3912907D88F00392132 /* Data_RegistrationFields.swift */, 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */, + 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */, ); path = Model; sourceTree = ""; @@ -436,6 +585,7 @@ children = ( 0770DE5328D0B00C006D8A5D /* swiftgen.yml */, 0770DE0A28D07831006D8A5D /* Core */, + E09179FA2B0F204D002AB695 /* CoreTests */, 0770DE0928D07831006D8A5D /* Products */, C9DFE47E699CFFA85A77AF2C /* Pods */, F1620A3A2C8B0699EAA61B57 /* Frameworks */, @@ -454,7 +604,7 @@ 0770DE0A28D07831006D8A5D /* Core */ = { isa = PBXGroup; children = ( - 0295B1DB297FF0E9003B0C65 /* Fonts */, + BA8FA65F2AD5973500EA029A /* Providers */, 027BD3A12909470F00392132 /* AvoidingHelpers */, 0770DE5528D0B142006D8A5D /* SwiftGen */, 0283347E28D4DCC100C828FC /* Extensions */, @@ -463,7 +613,6 @@ 0727876E28D233EC002E9142 /* Configuration */, 0770DE2828D0928B006D8A5D /* Network */, 0770DE7628D0C491006D8A5D /* View */, - 0770DE7A28D0C78C006D8A5D /* Theme.swift */, 0770DE5D28D0B209006D8A5D /* Localizable.strings */, 0770DE5128D0ADFF006D8A5D /* Assets.xcassets */, 071009CF28D1E3A600344290 /* Constants.swift */, @@ -509,7 +658,8 @@ 0770DE7728D0C49E006D8A5D /* Base */ = { isa = PBXGroup; children = ( - 0770DE7828D0C4A9006D8A5D /* RoundedCorners.swift */, + 064987882B4D69FE0071642A /* Webview */, + E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */, 02A4833B29B8C57800D33F33 /* DownloadView.swift */, 02D800CB29348F460099CF16 /* ImagePicker.swift */, 024D723429C8BB1A006D36ED /* NavigationBar.swift */, @@ -522,8 +672,6 @@ 024FCCFF28EF1CD300232339 /* WebBrowser.swift */, 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */, 023A4DD3299E66BD006C0E48 /* OfflineSnackBarView.swift */, - 024D865D28F02C6B0077E0A0 /* WebView.swift */, - 02C2DC0729B63D6200F4445D /* WebViewHTML.swift */, 021D925628DCF12900ACC565 /* AlertView.swift */, 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */, 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */, @@ -535,11 +683,65 @@ 020306CB2932C0C4000949EA /* PickerView.swift */, 027BD3C42909707700392132 /* Shake.swift */, 023A1135291432B200D0D354 /* RegistrationTextField.swift */, + BA4AFB432B6A5AF100A21367 /* CheckBoxView.swift */, 023A1137291432FD00D0D354 /* FieldConfiguration.swift */, + BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */, + BA593F1A2AF8E487009ADB51 /* ScrollSlidingTabBar */, + BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */, + BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */, + 02E93F862AEBAED4006C4750 /* AppReview */, ); path = Base; sourceTree = ""; }; + BA30427C2B20B235009B64B7 /* SocialAuth */ = { + isa = PBXGroup; + children = ( + BA30427E2B20B299009B64B7 /* Error */, + BA8FA6602AD5974300EA029A /* AppleAuthProvider.swift */, + BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */, + BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */, + BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */, + BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */, + ); + path = SocialAuth; + sourceTree = ""; + }; + BA30427E2B20B299009B64B7 /* Error */ = { + isa = PBXGroup; + children = ( + BA30427D2B20B299009B64B7 /* SocialAuthError.swift */, + ); + path = Error; + sourceTree = ""; + }; + BA593F1A2AF8E487009ADB51 /* ScrollSlidingTabBar */ = { + isa = PBXGroup; + children = ( + BA593F1D2AF8E4A0009ADB51 /* FrameReader.swift */, + BA593F1B2AF8E498009ADB51 /* ScrollSlidingTabBar.swift */, + ); + path = ScrollSlidingTabBar; + sourceTree = ""; + }; + BA8FA65F2AD5973500EA029A /* Providers */ = { + isa = PBXGroup; + children = ( + BA30427C2B20B235009B64B7 /* SocialAuth */, + BAD9CA3D2B29BB1A00DE790A /* Ajax */, + ); + path = Providers; + sourceTree = ""; + }; + BAD9CA3D2B29BB1A00DE790A /* Ajax */ = { + isa = PBXGroup; + children = ( + BAD9CA322B28A8F300DE790A /* AjaxProvider.swift */, + BAD9CA2E2B289B3500DE790A /* ajaxHandler.js */, + ); + path = Ajax; + sourceTree = ""; + }; C9DFE47E699CFFA85A77AF2C /* Pods */ = { isa = PBXGroup; children = ( @@ -564,9 +766,45 @@ path = Combine; sourceTree = ""; }; + DBF6F2422B014AF30098414B /* Config */ = { + isa = PBXGroup; + children = ( + 0604C9A92B22FACF00AD5DBF /* UIComponentsConfig.swift */, + 0727876F28D23411002E9142 /* Config.swift */, + DBF6F2402B014ADA0098414B /* FirebaseConfig.swift */, + DBF6F2492B0380E00098414B /* FeaturesConfig.swift */, + DBF6F2452B01DAFE0098414B /* AgreementConfig.swift */, + BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */, + BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */, + BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */, + BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */, + A53A32342B233DEC005FE38A /* ThemeConfig.swift */, + E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */, + ); + path = Config; + sourceTree = ""; + }; + E09179FA2B0F204D002AB695 /* CoreTests */ = { + isa = PBXGroup; + children = ( + E09179FB2B0F204D002AB695 /* Configuration */, + ); + path = CoreTests; + sourceTree = ""; + }; + E09179FB2B0F204D002AB695 /* Configuration */ = { + isa = PBXGroup; + children = ( + E09179FC2B0F204D002AB695 /* ConfigTests.swift */, + BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */, + ); + path = Configuration; + sourceTree = ""; + }; F1620A3A2C8B0699EAA61B57 /* Frameworks */ = { isa = PBXGroup; children = ( + E055A5382B18DC95008D9E5E /* Theme.framework */, 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */, ); name = Frameworks; @@ -621,6 +859,8 @@ name = Core; packageProductDependencies = ( 025EF2F52971740000B838AB /* YouTubePlayerKit */, + BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */, + BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */, ); productName = Core; productReference = 0770DE0828D07831006D8A5D /* Core.framework */; @@ -638,6 +878,7 @@ TargetAttributes = { 07169468296D996800E3DED6 = { CreatedOnToolsVersion = 14.2; + LastSwiftMigration = 1500; }; 0770DE0728D07831006D8A5D = { CreatedOnToolsVersion = 14.0; @@ -657,6 +898,8 @@ mainGroup = 0770DDFE28D07831006D8A5D; packageReferences = ( 025EF2F42971740000B838AB /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */, + BA8FA65E2AD574D700EA029A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, + BA8FA6712AD6ABA300EA029A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */, ); productRefGroup = 0770DE0928D07831006D8A5D /* Products */; projectDirPath = ""; @@ -681,9 +924,9 @@ buildActionMask = 2147483647; files = ( 0770DE5228D0ADFF006D8A5D /* Assets.xcassets in Resources */, - 0295B1DC297FF114003B0C65 /* SF-Pro.ttf in Resources */, 0770DE5B28D0B209006D8A5D /* Localizable.strings in Resources */, 0770DE5428D0B00C006D8A5D /* swiftgen.yml in Resources */, + BAD9CA2F2B289B3500DE790A /* ajaxHandler.js in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -738,6 +981,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BAD9CA422B2B140100DE790A /* AgreementConfigTests.swift in Sources */, + E09179FD2B0F204E002AB695 /* ConfigTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -746,28 +991,37 @@ buildActionMask = 2147483647; files = ( 0727878528D31657002E9142 /* Data_User.swift in Sources */, + 064987942B4D69FF0071642A /* WebviewInjection.swift in Sources */, 02F6EF4A28D9F0A700835477 /* DateExtension.swift in Sources */, + DBF6F2462B01DAFE0098414B /* AgreementConfig.swift in Sources */, 027BD3AF2909475000392132 /* DismissKeyboardTapHandler.swift in Sources */, + BAFB99902B14B377007D09F9 /* GoogleConfig.swift in Sources */, 02E225B0291D29EB0067769A /* UrlExtension.swift in Sources */, - 0770DE7928D0C4A9006D8A5D /* RoundedCorners.swift in Sources */, 02CF46C829546AA200A698EE /* NoCachedDataError.swift in Sources */, 0727877728D23847002E9142 /* DataLayer.swift in Sources */, 0241666B28F5A78B00082765 /* HTMLFormattedText.swift in Sources */, 02D800CC29348F460099CF16 /* ImagePicker.swift in Sources */, 027BD3B92909476200392132 /* KeyboardAvoidingModifier.swift in Sources */, + BA4AFB422B5A7A0900A21367 /* VideoDownloadQualityView.swift in Sources */, 0770DE2C28D092B3006D8A5D /* NetworkLogger.swift in Sources */, + 064987972B4D69FF0071642A /* WebView.swift in Sources */, 0770DE2A28D0929E006D8A5D /* HTTPTask.swift in Sources */, 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */, 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */, + BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */, 027BD3B32909475900392132 /* Publishers+KeyboardState.swift in Sources */, 0727877D28D25212002E9142 /* ProgressBar.swift in Sources */, 0236961F28F9A2F600EEF206 /* AuthEndpoint.swift in Sources */, 02B3E3B32930198600A50475 /* AVPlayerViewControllerExtension.swift in Sources */, + BAD9CA332B28A8F300DE790A /* AjaxProvider.swift in Sources */, 0295C885299B99DD00ABE571 /* RefreshableScrollView.swift in Sources */, 0282DA7328F98CC9003C3F07 /* WebUnitView.swift in Sources */, + E0D586362B314CD3009B4BA7 /* LogistrationBottomView.swift in Sources */, 0727878128D25EFD002E9142 /* SnackBarView.swift in Sources */, 021D924828DC860C00ACC565 /* Data_UserProfile.swift in Sources */, + BA593F1C2AF8E498009ADB51 /* ScrollSlidingTabBar.swift in Sources */, 070019AC28F6FD0100D5FC78 /* CourseDetailBlock.swift in Sources */, + BA593F1E2AF8E4A0009ADB51 /* FrameReader.swift in Sources */, 0727877028D23411002E9142 /* Config.swift in Sources */, CFC84952299F8B890055E497 /* Debounce.swift in Sources */, 0236F3B728F4351E0050F09B /* CourseButton.swift in Sources */, @@ -775,74 +1029,105 @@ 028F9F37293A44C700DE65D0 /* Data_ResetPassword.swift in Sources */, 022C64E429AE0191000F532B /* TextWithUrls.swift in Sources */, 0283348028D4DCD200C828FC /* ViewExtension.swift in Sources */, + BA8FA66A2AD59B5500EA029A /* GoogleAuthProvider.swift in Sources */, 02A4833529B8A73400D33F33 /* CorePersistenceProtocol.swift in Sources */, + 0233D5732AF13EEE00BAC8BD /* AppReviewButton.swift in Sources */, 02512FF0299533DF0024D438 /* CoreDataHandlerProtocol.swift in Sources */, 0260E58028FD792800BBBE18 /* WebUnitViewModel.swift in Sources */, 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */, 027BD3AE2909475000392132 /* KeyboardScrollerOptions.swift in Sources */, + BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */, + 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */, + 064987992B4D69FF0071642A /* WebViewScriptInjectionProtocol.swift in Sources */, 027BD3BE2909478B00392132 /* UIResponder+CurrentResponder.swift in Sources */, 070019AE28F701B200D5FC78 /* Certificate.swift in Sources */, + BA4AFB442B6A5AF100A21367 /* CheckBoxView.swift in Sources */, 076F297F2A1F80C800967E7D /* Pagination.swift in Sources */, + 02AFCC1A2AEFDC18000360F0 /* ThirdPartyMailer.swift in Sources */, 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */, 02C917F029CDA99E00DBB8BD /* Data_Dashboard.swift in Sources */, 024FCD0028EF1CD300232339 /* WebBrowser.swift in Sources */, 027BD3B52909475900392132 /* KeyboardStateObserver.swift in Sources */, 0283347D28D4D3DE00C828FC /* Data_Discovery.swift in Sources */, + 064987962B4D69FF0071642A /* WebviewMessage.swift in Sources */, 07DDFCBD29A780BB00572595 /* UINavigationController+Animation.swift in Sources */, 023A4DD4299E66BD006C0E48 /* OfflineSnackBarView.swift in Sources */, 021D925728DCF12900ACC565 /* AlertView.swift in Sources */, 027BD3A82909474200392132 /* KeyboardAvoidingViewController.swift in Sources */, - 0770DE7B28D0C78C006D8A5D /* Theme.swift in Sources */, + 02E93F852AEBAEBC006C4750 /* AppReviewViewModel.swift in Sources */, 0770DE2528D08FBA006D8A5D /* CoreStorage.swift in Sources */, + BA8FA6612AD5974300EA029A /* AppleAuthProvider.swift in Sources */, + BA8FA6702AD59EA300EA029A /* MicrosoftAuthProvider.swift in Sources */, + BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */, 020306CC2932C0C4000949EA /* PickerView.swift in Sources */, 027BD3C52909707700392132 /* Shake.swift in Sources */, 027BD39C2908810C00392132 /* RegisterUser.swift in Sources */, 071009C428D1C9D000344290 /* StyledButton.swift in Sources */, 07460FE1294B706200F70538 /* CollectionExtension.swift in Sources */, + 064987932B4D69FF0071642A /* DragAndDropCssInjection.swift in Sources */, 027BD3B42909475900392132 /* KeyboardState.swift in Sources */, 027BD3922907D88F00392132 /* Data_RegistrationFields.swift in Sources */, 07460FE3294B72D700F70538 /* Notification.swift in Sources */, 0727877F28D25B24002E9142 /* Alamofire+Error.swift in Sources */, 02A4833829B8A8F900D33F33 /* CoreDataModel.xcdatamodeld in Sources */, + 064987952B4D69FF0071642A /* SurveyCssInjection.swift in Sources */, 0259104A29C4A5B6004B5A55 /* UserSettings.swift in Sources */, 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */, 071009D028D1E3A600344290 /* Constants.swift in Sources */, 0770DE1928D0847D006D8A5D /* BaseRouter.swift in Sources */, + BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */, 0284DBFE28D48C5300830893 /* CourseItem.swift in Sources */, 0248C92329C075EF00DC8402 /* CourseBlockModel.swift in Sources */, + E0D5861A2B2FF74C009B4BA7 /* DiscoveryConfig.swift in Sources */, + DBF6F2412B014ADA0098414B /* FirebaseConfig.swift in Sources */, 072787B628D37A0E002E9142 /* Validator.swift in Sources */, 0236961D28F9A2D200EEF206 /* Data_AuthResponse.swift in Sources */, + 02AFCC182AEFDB24000360F0 /* ThirdPartyMailClient.swift in Sources */, + 0233D5712AF13EC800BAC8BD /* SelectMailClientView.swift in Sources */, + BAFB99842B0E282E007D09F9 /* MicrosoftConfig.swift in Sources */, 02B2B594295C5C7A00914876 /* Thread.swift in Sources */, - 027DB33528D8C8FE002B6862 /* Data_MyCourse.swift in Sources */, + E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */, 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */, + BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */, + 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */, 02A4833C29B8C57800D33F33 /* DownloadView.swift in Sources */, 027BD3AD2909475000392132 /* KeyboardScroller.swift in Sources */, 070019A528F6F17900D5FC78 /* Data_Media.swift in Sources */, 024D723529C8BB1A006D36ED /* NavigationBar.swift in Sources */, + BA8B3A2F2AD546A700D25EF5 /* DebugLog.swift in Sources */, 0727877928D23BE0002E9142 /* RequestInterceptor.swift in Sources */, + BA8FA66E2AD59E7D00EA029A /* FacebookAuthProvider.swift in Sources */, 0770DE2E28D09743006D8A5D /* API.swift in Sources */, 028CE96929858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift in Sources */, 027BD3A92909474200392132 /* KeyboardAvoidingViewControllerRepr.swift in Sources */, 02F98A7F28F81EE900DE94C0 /* Container+App.swift in Sources */, 02B3F16E2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift in Sources */, + 0649879A2B4D69FF0071642A /* WebViewHTML.swift in Sources */, 0727877B28D24A1D002E9142 /* HeadersRedirectHandler.swift in Sources */, 0236961B28F9A28B00EEF206 /* AuthInteractor.swift in Sources */, 0770DE3028D09793006D8A5D /* EndPointType.swift in Sources */, 020C31C9290AC3F700D6DEA2 /* PickerFields.swift in Sources */, 02F6EF3B28D9B8EC00835477 /* CourseCellView.swift in Sources */, 023A1138291432FD00D0D354 /* FieldConfiguration.swift in Sources */, - 024D865E28F02C6B0077E0A0 /* WebView.swift in Sources */, + DBF6F24A2B0380E00098414B /* FeaturesConfig.swift in Sources */, 02F164372902A9EB0090DDEF /* StringExtension.swift in Sources */, 0231CDBE2922422D00032416 /* CSSInjector.swift in Sources */, + 064987982B4D69FF0071642A /* CSSInjectionProtocol.swift in Sources */, + BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */, + BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */, 0236961928F9A26900EEF206 /* AuthRepository.swift in Sources */, 023A1136291432B200D0D354 /* RegistrationTextField.swift in Sources */, - 02C2DC0829B63D6200F4445D /* WebViewHTML.swift in Sources */, + 02A463112AEA966C00331037 /* AppReviewView.swift in Sources */, 025B36752A13B7D5001A640E /* UnitButtonView.swift in Sources */, 028F9F39293A452B00DE65D0 /* ResetPassword.swift in Sources */, + 0233D56F2AF13EB200BAC8BD /* StarRatingView.swift in Sources */, + 0604C9AA2B22FACF00AD5DBF /* UIComponentsConfig.swift in Sources */, 027BD3B82909476200392132 /* DismissKeyboardTapViewModifier.swift in Sources */, 024BE3DF29B2615500BCDEE2 /* CGColorExtension.swift in Sources */, 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */, + 07E0939F2B308D2800F1E4B2 /* Data_Certificate.swift in Sources */, 0727878928D31734002E9142 /* User.swift in Sources */, + A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */, 02280F5B294B4E6F0032823A /* Connectivity.swift in Sources */, 02066B482906F73400F4307E /* PickerMenu.swift in Sources */, ); @@ -974,6 +1259,7 @@ 02DD1C9B29E80CE400F35DCE /* DebugStage */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; @@ -986,6 +1272,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1086,6 +1373,7 @@ 02DD1C9E29E80CED00F35DCE /* ReleaseStage */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; @@ -1106,6 +1394,7 @@ 07169470296D996900E3DED6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; @@ -1118,6 +1407,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1126,6 +1416,7 @@ 07169471296D996900E3DED6 /* DebugProd */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; @@ -1138,6 +1429,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1146,6 +1438,7 @@ 07169472296D996900E3DED6 /* DebugDev */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; @@ -1158,6 +1451,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1166,6 +1460,7 @@ 07169473296D996900E3DED6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; @@ -1186,6 +1481,7 @@ 07169474296D996900E3DED6 /* ReleaseProd */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; @@ -1206,6 +1502,7 @@ 07169475296D996900E3DED6 /* ReleaseDev */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; @@ -1855,6 +2152,22 @@ minimumVersion = 1.5.0; }; }; + BA8FA65E2AD574D700EA029A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/google/GoogleSignIn-iOS"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.0.0; + }; + }; + BA8FA6712AD6ABA300EA029A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/facebook/facebook-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 14.1.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1863,6 +2176,16 @@ package = 025EF2F42971740000B838AB /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */; productName = YouTubePlayerKit; }; + BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */ = { + isa = XCSwiftPackageProductDependency; + package = BA8FA65E2AD574D700EA029A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */; + productName = GoogleSignIn; + }; + BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */ = { + isa = XCSwiftPackageProductDependency; + package = BA8FA6712AD6ABA300EA029A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */; + productName = FacebookLogin; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Core/Core.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Core/Core.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/Core/Core.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..7dde19a07 --- /dev/null +++ b/Core/Core.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "youtubeplayerkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SvenTiigi/YouTubePlayerKit", + "state" : { + "revision" : "1fe4c8b07a61d50c2fd276e1d9c8087583c7638a", + "version" : "1.5.3" + } + } + ], + "version" : 2 +} diff --git a/Core/Core/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json b/Core/Core/Assets.xcassets/Colors/AppleButtonColor.colorset/Contents.json similarity index 76% rename from Core/Core/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json rename to Core/Core/Assets.xcassets/Colors/AppleButtonColor.colorset/Contents.json index 096098758..be9d677bb 100644 --- a/Core/Core/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json +++ b/Core/Core/Assets.xcassets/Colors/AppleButtonColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xBB", - "green" : "0xA5", - "red" : "0x97" + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x9F", - "green" : "0x88", - "red" : "0x79" + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" } }, "idiom" : "universal" diff --git a/Core/Core/Assets.xcassets/Colors/FacebookButtonColor.colorset/Contents.json b/Core/Core/Assets.xcassets/Colors/FacebookButtonColor.colorset/Contents.json new file mode 100644 index 000000000..1b6540850 --- /dev/null +++ b/Core/Core/Assets.xcassets/Colors/FacebookButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xEA", + "green" : "0x73", + "red" : "0x38" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEA", + "green" : "0x73", + "red" : "0x38" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/GoogleButtonColor.colorset/Contents.json b/Core/Core/Assets.xcassets/Colors/GoogleButtonColor.colorset/Contents.json new file mode 100644 index 000000000..8cd6714c9 --- /dev/null +++ b/Core/Core/Assets.xcassets/Colors/GoogleButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xF2", + "green" : "0xF2", + "red" : "0xF2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF2", + "green" : "0xF2", + "red" : "0xF2" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/MicrosoftButtonColor.colorset/Contents.json b/Core/Core/Assets.xcassets/Colors/MicrosoftButtonColor.colorset/Contents.json new file mode 100644 index 000000000..45a391bd7 --- /dev/null +++ b/Core/Core/Assets.xcassets/Colors/MicrosoftButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x2E", + "red" : "0x2E" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x2E", + "red" : "0x2E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Auth/Contents.json b/Core/Core/Assets.xcassets/CourseDates/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Auth/Contents.json rename to Core/Core/Assets.xcassets/CourseDates/Contents.json diff --git a/Core/Core/Assets.xcassets/CourseDates/assignment_icon.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseDates/assignment_icon.imageset/Contents.json new file mode 100644 index 000000000..40941b157 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/assignment_icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "edit_square.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/CourseDates/assignment_icon.imageset/edit_square.svg b/Core/Core/Assets.xcassets/CourseDates/assignment_icon.imageset/edit_square.svg new file mode 100644 index 000000000..0fd888059 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/assignment_icon.imageset/edit_square.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/CourseDates/calendar_icon.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseDates/calendar_icon.imageset/Contents.json new file mode 100644 index 000000000..2a14fd4fa --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/calendar_icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "calender_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/CourseDates/calendar_icon.imageset/calender_icon.svg b/Core/Core/Assets.xcassets/CourseDates/calendar_icon.imageset/calender_icon.svg new file mode 100644 index 000000000..e0cdf5f31 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/calendar_icon.imageset/calender_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/CourseDates/certificate_icon.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseDates/certificate_icon.imageset/Contents.json new file mode 100644 index 000000000..47d94b155 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/certificate_icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "certificate_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/CourseDates/certificate_icon.imageset/certificate_icon.svg b/Core/Core/Assets.xcassets/CourseDates/certificate_icon.imageset/certificate_icon.svg new file mode 100644 index 000000000..532dd1baf --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/certificate_icon.imageset/certificate_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/CourseDates/lock_icon.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseDates/lock_icon.imageset/Contents.json new file mode 100644 index 000000000..f94ec1e8c --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/lock_icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "lock_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/CourseDates/lock_icon.imageset/lock_icon.svg b/Core/Core/Assets.xcassets/CourseDates/lock_icon.imageset/lock_icon.svg new file mode 100644 index 000000000..823dd2e69 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/lock_icon.imageset/lock_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/CourseDates/lock_with_watch_icon.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseDates/lock_with_watch_icon.imageset/Contents.json new file mode 100644 index 000000000..705033805 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/lock_with_watch_icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "locked.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/CourseDates/lock_with_watch_icon.imageset/locked.svg b/Core/Core/Assets.xcassets/CourseDates/lock_with_watch_icon.imageset/locked.svg new file mode 100644 index 000000000..bebb2111f --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/lock_with_watch_icon.imageset/locked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Core/Core/Assets.xcassets/CourseDates/school_cap_icon.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseDates/school_cap_icon.imageset/Contents.json new file mode 100644 index 000000000..f31579037 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/school_cap_icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "school_cap_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/CourseDates/school_cap_icon.imageset/school_cap_icon.svg b/Core/Core/Assets.xcassets/CourseDates/school_cap_icon.imageset/school_cap_icon.svg new file mode 100644 index 000000000..4636ce2fd --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseDates/school_cap_icon.imageset/school_cap_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/Discussions/send.imageset/Contents.json b/Core/Core/Assets.xcassets/Discussions/send.imageset/Contents.json index 66cc886a8..501261e9c 100644 --- a/Core/Core/Assets.xcassets/Discussions/send.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/Discussions/send.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Group 62.svg", + "filename" : "send.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "Group 62-2.svg", + "filename" : "send 1.svg", "idiom" : "universal" } ], diff --git a/Core/Core/Assets.xcassets/Discussions/send.imageset/Group 62-2.svg b/Core/Core/Assets.xcassets/Discussions/send.imageset/Group 62-2.svg deleted file mode 100644 index bafc12f2e..000000000 --- a/Core/Core/Assets.xcassets/Discussions/send.imageset/Group 62-2.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/Discussions/send.imageset/Group 62.svg b/Core/Core/Assets.xcassets/Discussions/send.imageset/Group 62.svg deleted file mode 100644 index 312dc8d1a..000000000 --- a/Core/Core/Assets.xcassets/Discussions/send.imageset/Group 62.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/Discussions/send.imageset/send 1.svg b/Core/Core/Assets.xcassets/Discussions/send.imageset/send 1.svg new file mode 100644 index 000000000..db8ab01ea --- /dev/null +++ b/Core/Core/Assets.xcassets/Discussions/send.imageset/send 1.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/Discussions/send.imageset/send.svg b/Core/Core/Assets.xcassets/Discussions/send.imageset/send.svg new file mode 100644 index 000000000..db8ab01ea --- /dev/null +++ b/Core/Core/Assets.xcassets/Discussions/send.imageset/send.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json b/Core/Core/Assets.xcassets/Profile/bg_delete.imageset/Contents.json similarity index 77% rename from Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json rename to Core/Core/Assets.xcassets/Profile/bg_delete.imageset/Contents.json index 2a3f6edf2..118426dbb 100644 --- a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/Profile/bg_delete.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Group-2.svg", + "filename" : "delete_bg_light.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "Group.svg", + "filename" : "bg_delete 1.svg", "idiom" : "universal" } ], diff --git a/Core/Core/Assets.xcassets/Profile/bg_delete.imageset/bg_delete 1.svg b/Core/Core/Assets.xcassets/Profile/bg_delete.imageset/bg_delete 1.svg new file mode 100644 index 000000000..92b146822 --- /dev/null +++ b/Core/Core/Assets.xcassets/Profile/bg_delete.imageset/bg_delete 1.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Core/Core/Assets.xcassets/Profile/bg_delete.imageset/delete_bg_light.svg b/Core/Core/Assets.xcassets/Profile/bg_delete.imageset/delete_bg_light.svg new file mode 100644 index 000000000..96007a9b5 --- /dev/null +++ b/Core/Core/Assets.xcassets/Profile/bg_delete.imageset/delete_bg_light.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Group-2.svg b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Group-2.svg deleted file mode 100644 index 60f0fafb9..000000000 --- a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Group-2.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Group.svg b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Group.svg deleted file mode 100644 index 09d83d54a..000000000 --- a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Group.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/Profile/delete_char.imageset/Contents.json b/Core/Core/Assets.xcassets/Profile/delete_char.imageset/Contents.json new file mode 100644 index 000000000..44ef99c1d --- /dev/null +++ b/Core/Core/Assets.xcassets/Profile/delete_char.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "delete_char.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/Profile/delete_char.imageset/delete_char.svg b/Core/Core/Assets.xcassets/Profile/delete_char.imageset/delete_char.svg new file mode 100644 index 000000000..d1422ad6a --- /dev/null +++ b/Core/Core/Assets.xcassets/Profile/delete_char.imageset/delete_char.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Core/Core/Assets.xcassets/Profile/delete_eyes.imageset/Contents.json b/Core/Core/Assets.xcassets/Profile/delete_eyes.imageset/Contents.json new file mode 100644 index 000000000..19c371f58 --- /dev/null +++ b/Core/Core/Assets.xcassets/Profile/delete_eyes.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "delete_eyes.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Profile/delete_eyes.imageset/delete_eyes.svg b/Core/Core/Assets.xcassets/Profile/delete_eyes.imageset/delete_eyes.svg new file mode 100644 index 000000000..af854563b --- /dev/null +++ b/Core/Core/Assets.xcassets/Profile/delete_eyes.imageset/delete_eyes.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Core/Core/Assets.xcassets/Colors/CardView/Contents.json b/Core/Core/Assets.xcassets/Socials/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/CardView/Contents.json rename to Core/Core/Assets.xcassets/Socials/Contents.json diff --git a/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/Contents.json b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/Contents.json new file mode 100644 index 000000000..f98d12c7c --- /dev/null +++ b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_apple@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_apple@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@2x.png b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@2x.png new file mode 100644 index 000000000..6ca46033a Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@2x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@3x.png b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@3x.png new file mode 100644 index 000000000..87995a5ef Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@3x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/Contents.json b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/Contents.json new file mode 100644 index 000000000..af6600d1a --- /dev/null +++ b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_facebook_white@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_facebook_white@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@2x.png b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@2x.png new file mode 100644 index 000000000..109a67604 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@2x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@3x.png b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@3x.png new file mode 100644 index 000000000..1623f36fd Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@3x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/Contents.json b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/Contents.json new file mode 100644 index 000000000..3c4d94ecc --- /dev/null +++ b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_google_white@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_google_white@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@2x.png b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@2x.png new file mode 100644 index 000000000..64c2157f9 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@2x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@3x.png b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@3x.png new file mode 100644 index 000000000..56b7b28d3 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@3x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/Contents.json b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/Contents.json new file mode 100644 index 000000000..be9b6142b --- /dev/null +++ b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_microsoft_white@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_microsoft_white@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@2x.png b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@2x.png new file mode 100644 index 000000000..bc596b581 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@2x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@3x.png b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@3x.png new file mode 100644 index 000000000..586c06636 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@3x.png differ diff --git a/Core/Core/Assets.xcassets/Auth/checkEmail.imageset/Contents.json b/Core/Core/Assets.xcassets/checkEmail.imageset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Auth/checkEmail.imageset/Contents.json rename to Core/Core/Assets.xcassets/checkEmail.imageset/Contents.json diff --git a/Core/Core/Assets.xcassets/Auth/checkEmail.imageset/_1-2.svg b/Core/Core/Assets.xcassets/checkEmail.imageset/_1-2.svg similarity index 100% rename from Core/Core/Assets.xcassets/Auth/checkEmail.imageset/_1-2.svg rename to Core/Core/Assets.xcassets/checkEmail.imageset/_1-2.svg diff --git a/Core/Core/Assets.xcassets/Auth/checkEmail.imageset/_1.svg b/Core/Core/Assets.xcassets/checkEmail.imageset/_1.svg similarity index 100% rename from Core/Core/Assets.xcassets/Auth/checkEmail.imageset/_1.svg rename to Core/Core/Assets.xcassets/checkEmail.imageset/_1.svg diff --git a/Core/Core/Assets.xcassets/favorite.imageset/Contents.json b/Core/Core/Assets.xcassets/favorite.imageset/Contents.json new file mode 100644 index 000000000..b1b3b208a --- /dev/null +++ b/Core/Core/Assets.xcassets/favorite.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "favorite.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/favorite.imageset/favorite.svg b/Core/Core/Assets.xcassets/favorite.imageset/favorite.svg new file mode 100644 index 000000000..70056997f --- /dev/null +++ b/Core/Core/Assets.xcassets/favorite.imageset/favorite.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Core/Core/Assets.xcassets/Colors/Contents.json b/Core/Core/Assets.xcassets/mailClients/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/Contents.json rename to Core/Core/Assets.xcassets/mailClients/Contents.json diff --git a/Core/Core/Assets.xcassets/mailClients/airmail.imageset/Contents.json b/Core/Core/Assets.xcassets/mailClients/airmail.imageset/Contents.json new file mode 100644 index 000000000..e0ebe7ad4 --- /dev/null +++ b/Core/Core/Assets.xcassets/mailClients/airmail.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "preview.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/mailClients/airmail.imageset/preview.jpg b/Core/Core/Assets.xcassets/mailClients/airmail.imageset/preview.jpg new file mode 100644 index 000000000..a355fff10 Binary files /dev/null and b/Core/Core/Assets.xcassets/mailClients/airmail.imageset/preview.jpg differ diff --git a/Core/Core/Assets.xcassets/mailClients/defaultMail.imageset/Contents.json b/Core/Core/Assets.xcassets/mailClients/defaultMail.imageset/Contents.json new file mode 100644 index 000000000..fed5fed62 --- /dev/null +++ b/Core/Core/Assets.xcassets/mailClients/defaultMail.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Mail_(iOS).svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/mailClients/defaultMail.imageset/Mail_(iOS).svg b/Core/Core/Assets.xcassets/mailClients/defaultMail.imageset/Mail_(iOS).svg new file mode 100644 index 000000000..8f18726b8 --- /dev/null +++ b/Core/Core/Assets.xcassets/mailClients/defaultMail.imageset/Mail_(iOS).svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/mailClients/fastmail.imageset/Contents.json b/Core/Core/Assets.xcassets/mailClients/fastmail.imageset/Contents.json new file mode 100644 index 000000000..77fd4f1b9 --- /dev/null +++ b/Core/Core/Assets.xcassets/mailClients/fastmail.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "fastmail.jpeg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/mailClients/fastmail.imageset/fastmail.jpeg b/Core/Core/Assets.xcassets/mailClients/fastmail.imageset/fastmail.jpeg new file mode 100644 index 000000000..c689b851e Binary files /dev/null and b/Core/Core/Assets.xcassets/mailClients/fastmail.imageset/fastmail.jpeg differ diff --git a/Core/Core/Assets.xcassets/mailClients/googlegmail.imageset/Contents.json b/Core/Core/Assets.xcassets/mailClients/googlegmail.imageset/Contents.json new file mode 100644 index 000000000..cc1243259 --- /dev/null +++ b/Core/Core/Assets.xcassets/mailClients/googlegmail.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "gmail-2015-07-30.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/mailClients/googlegmail.imageset/gmail-2015-07-30.png b/Core/Core/Assets.xcassets/mailClients/googlegmail.imageset/gmail-2015-07-30.png new file mode 100644 index 000000000..5bdabd7be Binary files /dev/null and b/Core/Core/Assets.xcassets/mailClients/googlegmail.imageset/gmail-2015-07-30.png differ diff --git a/Core/Core/Assets.xcassets/mailClients/ms-outlook.imageset/Contents.json b/Core/Core/Assets.xcassets/mailClients/ms-outlook.imageset/Contents.json new file mode 100644 index 000000000..6bd18085b --- /dev/null +++ b/Core/Core/Assets.xcassets/mailClients/ms-outlook.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "microsoft-outlook-2015-02-09.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/mailClients/ms-outlook.imageset/microsoft-outlook-2015-02-09.png b/Core/Core/Assets.xcassets/mailClients/ms-outlook.imageset/microsoft-outlook-2015-02-09.png new file mode 100644 index 000000000..fdbdb4616 Binary files /dev/null and b/Core/Core/Assets.xcassets/mailClients/ms-outlook.imageset/microsoft-outlook-2015-02-09.png differ diff --git a/Core/Core/Assets.xcassets/mailClients/proton.imageset/Contents.json b/Core/Core/Assets.xcassets/mailClients/proton.imageset/Contents.json new file mode 100644 index 000000000..16a2516ab --- /dev/null +++ b/Core/Core/Assets.xcassets/mailClients/proton.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "unnamed.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/mailClients/proton.imageset/unnamed.png b/Core/Core/Assets.xcassets/mailClients/proton.imageset/unnamed.png new file mode 100644 index 000000000..88e80fb09 Binary files /dev/null and b/Core/Core/Assets.xcassets/mailClients/proton.imageset/unnamed.png differ diff --git a/Core/Core/Assets.xcassets/mailClients/readdle-spark.imageset/Contents.json b/Core/Core/Assets.xcassets/mailClients/readdle-spark.imageset/Contents.json new file mode 100644 index 000000000..d692cdf89 --- /dev/null +++ b/Core/Core/Assets.xcassets/mailClients/readdle-spark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "spark-mail-smart-email-inbox-2023-01-05.png.jpeg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/mailClients/readdle-spark.imageset/spark-mail-smart-email-inbox-2023-01-05.png.jpeg b/Core/Core/Assets.xcassets/mailClients/readdle-spark.imageset/spark-mail-smart-email-inbox-2023-01-05.png.jpeg new file mode 100644 index 000000000..ec879878a Binary files /dev/null and b/Core/Core/Assets.xcassets/mailClients/readdle-spark.imageset/spark-mail-smart-email-inbox-2023-01-05.png.jpeg differ diff --git a/Core/Core/Assets.xcassets/mailClients/ymail.imageset/Contents.json b/Core/Core/Assets.xcassets/mailClients/ymail.imageset/Contents.json new file mode 100644 index 000000000..61ceb1f24 --- /dev/null +++ b/Core/Core/Assets.xcassets/mailClients/ymail.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "image-2.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/mailClients/ymail.imageset/image-2.png b/Core/Core/Assets.xcassets/mailClients/ymail.imageset/image-2.png new file mode 100644 index 000000000..065c53e60 Binary files /dev/null and b/Core/Core/Assets.xcassets/mailClients/ymail.imageset/image-2.png differ diff --git a/Core/Core/Assets.xcassets/star.imageset/Contents.json b/Core/Core/Assets.xcassets/star.imageset/Contents.json new file mode 100644 index 000000000..fb87640df --- /dev/null +++ b/Core/Core/Assets.xcassets/star.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "star.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/star.imageset/star.svg b/Core/Core/Assets.xcassets/star.imageset/star.svg new file mode 100644 index 000000000..1d79798d1 --- /dev/null +++ b/Core/Core/Assets.xcassets/star.imageset/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/star_outline.imageset/Contents.json b/Core/Core/Assets.xcassets/star_outline.imageset/Contents.json new file mode 100644 index 000000000..dde435c5d --- /dev/null +++ b/Core/Core/Assets.xcassets/star_outline.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "star_outline.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/star_outline.imageset/star_outline.svg b/Core/Core/Assets.xcassets/star_outline.imageset/star_outline.svg new file mode 100644 index 000000000..aa8b674a5 --- /dev/null +++ b/Core/Core/Assets.xcassets/star_outline.imageset/star_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/warning_filled.imageset/Contents.json b/Core/Core/Assets.xcassets/warning_filled.imageset/Contents.json new file mode 100644 index 000000000..1f83277d1 --- /dev/null +++ b/Core/Core/Assets.xcassets/warning_filled.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "warning_filled.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/warning_filled.imageset/warning_filled.svg b/Core/Core/Assets.xcassets/warning_filled.imageset/warning_filled.svg new file mode 100644 index 000000000..3ff3fdfec --- /dev/null +++ b/Core/Core/Assets.xcassets/warning_filled.imageset/warning_filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Core/Configuration/BaseRouter.swift b/Core/Core/Configuration/BaseRouter.swift index c86b90f62..f18897272 100644 --- a/Core/Core/Configuration/BaseRouter.swift +++ b/Core/Core/Configuration/BaseRouter.swift @@ -21,14 +21,20 @@ public protocol BaseRouter { func removeLastView(controllers: Int) - func showMainScreen() - - func showLoginScreen() - - func showRegisterScreen() + func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) + + func showStartupScreen() + + func showLoginScreen(sourceScreen: LogistrationSourceScreen) + + func showRegisterScreen(sourceScreen: LogistrationSourceScreen) func showForgotPasswordScreen() - + + func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) + + func showWebBrowser(title: String, url: URL) + func presentAlert( alertTitle: String, alertMessage: String, @@ -73,14 +79,18 @@ open class BaseRouterMock: BaseRouter { public func dismiss(animated: Bool) {} - public func showMainScreen() {} - - public func showLoginScreen() {} + public func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) {} + + public func showStartupScreen() {} - public func showRegisterScreen() {} + public func showLoginScreen(sourceScreen: LogistrationSourceScreen) {} + + public func showRegisterScreen(sourceScreen: LogistrationSourceScreen) {} public func showForgotPasswordScreen() {} + public func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) {} + public func backToRoot(animated: Bool) {} public func back(animated: Bool) {} @@ -88,7 +98,9 @@ open class BaseRouterMock: BaseRouter { public func backWithFade() {} public func removeLastView(controllers: Int) {} - + + public func showWebBrowser(title: String, url: URL) {} + public func presentAlert( alertTitle: String, alertMessage: String, diff --git a/Core/Core/Configuration/CSSInjector.swift b/Core/Core/Configuration/CSSInjector.swift index 9e7faf0ec..59beef4cd 100644 --- a/Core/Core/Configuration/CSSInjector.swift +++ b/Core/Core/Configuration/CSSInjector.swift @@ -7,16 +7,14 @@ import Foundation import SwiftUI +import Theme public class CSSInjector { public let baseURL: URL - public init(baseURL: String) { - guard let url = URL(string: baseURL) else { - fatalError("Ivalid baseURL") - } - self.baseURL = url + public init(config: ConfigProtocol) { + self.baseURL = config.baseURL } public enum CssType { @@ -115,6 +113,10 @@ public class CSSInjector { let style = """ - +
""" @@ -147,7 +149,7 @@ public class CSSInjector { #if DEBUG public class CSSInjectorMock: CSSInjector { public convenience init() { - self.init(baseURL: "https://google.com/") + self.init(config: ConfigMock()) } } #endif diff --git a/Core/Core/Configuration/Config.swift b/Core/Core/Configuration/Config.swift deleted file mode 100644 index 7f49fac57..000000000 --- a/Core/Core/Configuration/Config.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Config.swift -// Core -// -// Created by Vladimir Chekyrta on 14.09.2022. -// - -import Foundation - -public class Config { - - public let baseURL: URL - public let oAuthClientId: String - - public lazy var termsOfUse: URL? = { - URL(string: "\(baseURL.description)/tos") - }() - - public lazy var privacyPolicy: URL? = { - URL(string: "\(baseURL.description)/privacy") - }() - - public let feedbackEmail = "support@example.com" - - public init(baseURL: String, oAuthClientId: String) { - guard let url = URL(string: baseURL) else { - fatalError("Ivalid baseURL") - } - self.baseURL = url - self.oAuthClientId = oAuthClientId - } -} - -// Mark - For testing and SwiftUI preview -#if DEBUG -public class ConfigMock: Config { - public convenience init() { - self.init(baseURL: "https://google.com/", oAuthClientId: "client_id") - } -} -#endif diff --git a/Core/Core/Configuration/Config/AgreementConfig.swift b/Core/Core/Configuration/Config/AgreementConfig.swift new file mode 100644 index 000000000..6dc4d518e --- /dev/null +++ b/Core/Core/Configuration/Config/AgreementConfig.swift @@ -0,0 +1,78 @@ +// +// AgreementConfig.swift +// Core +// +// Created by Muhammad Umer on 11/13/23. +// + +import Foundation + +private enum AgreementKeys: String, RawStringExtractable { + case privacyPolicyURL = "PRIVACY_POLICY_URL" + case tosURL = "TOS_URL" + case cookiePolicyURL = "COOKIE_POLICY_URL" + case dataSellContentURL = "DATA_SELL_CONSENT_URL" + case eulaURL = "EULA_URL" + case supportedLanguages = "SUPPORTED_LANGUAGES" +} + +public class AgreementConfig: NSObject { + public var privacyPolicyURL: URL? + public var tosURL: URL? + public var cookiePolicyURL: URL? + public var dataSellContentURL: URL? + public var eulaURL: URL? + public var supportedLanguages: [String]? + + init(dictionary: [String: AnyObject]) { + supportedLanguages = dictionary[AgreementKeys.supportedLanguages] as? [String] + cookiePolicyURL = (dictionary[AgreementKeys.cookiePolicyURL] as? String).flatMap(URL.init) + dataSellContentURL = (dictionary[AgreementKeys.dataSellContentURL] as? String).flatMap(URL.init) + eulaURL = (dictionary[AgreementKeys.eulaURL] as? String).flatMap(URL.init) + + super.init() + + if let tosURL = dictionary[AgreementKeys.tosURL.rawValue] as? String { + self.tosURL = URL(string: completePath(url: tosURL)) + } + + if let privacyPolicyURL = dictionary[AgreementKeys.privacyPolicyURL.rawValue] as? String { + self.privacyPolicyURL = URL(string: completePath(url: privacyPolicyURL)) + } + } + + private func completePath(url: String) -> String { + let langCode: String + if #available(iOS 16, *) { + langCode = Locale.current.language.languageCode?.identifier ?? "" + } else { + langCode = Locale.current.languageCode ?? "" + } + + if let supportedLanguages = supportedLanguages, + !supportedLanguages.contains(langCode) { + return url + } + + let URL = URL(string: url) + let host = URL?.host ?? "" + let components = url.components(separatedBy: host) + + if components.count != 2 { + return url + } + + if let firstComponent = components.first, let lastComponent = components.last { + return "\(firstComponent)\(host)/\(langCode)\(lastComponent)" + } + + return url + } +} + +private let key = "AGREEMENT_URLS" +extension Config { + public var agreement: AgreementConfig { + return AgreementConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/AppleSignInConfig.swift b/Core/Core/Configuration/Config/AppleSignInConfig.swift new file mode 100644 index 000000000..8e5817e90 --- /dev/null +++ b/Core/Core/Configuration/Config/AppleSignInConfig.swift @@ -0,0 +1,28 @@ +// +// SocialLoginConfig.swift +// Core +// +// Created by Eugene Yatsenko on 27.11.2023. +// + +import Foundation + +private enum AppleSignInKeys: String { + case enabled = "ENABLED" +} + +public class AppleSignInConfig: NSObject { + public var enabled: Bool + + init(dictionary: [String: Any]) { + enabled = dictionary[AppleSignInKeys.enabled.rawValue] as? Bool ?? false + super.init() + } +} + +private let key = "APPLE_SIGNIN" +extension Config { + public var appleSignIn: AppleSignInConfig { + AppleSignInConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift new file mode 100644 index 000000000..481f67b9c --- /dev/null +++ b/Core/Core/Configuration/Config/Config.swift @@ -0,0 +1,200 @@ +// +// Config.swift +// Core +// +// Created by Muhammad Umer on 11/11/2023. +// + +import Foundation + +public protocol ConfigProtocol { + var baseURL: URL { get } + var oAuthClientId: String { get } + var tokenType: TokenType { get } + var feedbackEmail: String { get } + var appStoreLink: String { get } + var faq: URL? { get } + var platformName: String { get } + var agreement: AgreementConfig { get } + var firebase: FirebaseConfig { get } + var facebook: FacebookConfig { get } + var microsoft: MicrosoftConfig { get } + var google: GoogleConfig { get } + var appleSignIn: AppleSignInConfig { get } + var features: FeaturesConfig { get } + var theme: ThemeConfig { get } + var uiComponents: UIComponentsConfig { get } + var discovery: DiscoveryConfig { get } + var program: DiscoveryConfig { get } + var URIScheme: String { get } +} + +public enum TokenType: String { + case jwt = "JWT" + case bearer = "BEARER" +} + +private enum ConfigKeys: String { + case baseURL = "API_HOST_URL" + case oAuthClientID = "OAUTH_CLIENT_ID" + case tokenType = "TOKEN_TYPE" + case feedbackEmailAddress = "FEEDBACK_EMAIL_ADDRESS" + case environmentDisplayName = "ENVIRONMENT_DISPLAY_NAME" + case platformName = "PLATFORM_NAME" + case organizationCode = "ORGANIZATION_CODE" + case appstoreID = "APP_STORE_ID" + case faq = "FAQ_URL" + case URIScheme = "URI_SCHEME" +} + +public class Config { + let configFileName = "config" + + internal var properties: [String: Any] = [:] + + internal init(properties: [String: Any]) { + self.properties = properties + } + + public convenience init() { + self.init(properties: [:]) + loadAndParseConfig() + } + + private func loadAndParseConfig() { + guard let path = Bundle.main.path(forResource: configFileName, ofType: "plist"), + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let dict = try? PropertyListSerialization.propertyList( + from: data, + options: [], + format: nil) as? [String: Any] + else { return } + + properties = dict + } + + internal subscript(key: String) -> Any? { + return properties[key] + } + + func dict(for key: String) -> [String: Any]? { + return properties[key] as? [String: Any] + } + + func value(for key: String) -> T? { + return properties[key] as? T + } + + func value(for key: String) -> Any? { + return properties[key] + } + + func value(for key: String, dict: [String: Any]) -> String? { + return dict[key] as? String ?? nil + } + + func string(for key: String) -> String? { + return value(for: key) as? String ?? nil + } + + func string(for key: String, dict: [String: Any]) -> String? { + return value(for: key, dict: dict) + } + + func bool(for key: String) -> Bool { + return value(for: key) as? Bool ?? false + } +} + +extension Config: ConfigProtocol { + public var baseURL: URL { + guard let urlString = string(for: ConfigKeys.baseURL.rawValue), + let url = URL(string: urlString) else { + fatalError("Unable to find base url in config.") + } + return url + } + + public var oAuthClientId: String { + guard let clientID = string(for: ConfigKeys.oAuthClientID.rawValue) else { + fatalError("Unable to find OAuth ClientID in config.") + } + return clientID + } + + public var tokenType: TokenType { + guard let tokenTypeValue = string(for: ConfigKeys.tokenType.rawValue), + let tokenType = TokenType(rawValue: tokenTypeValue) + else { return .jwt } + return tokenType + } + + public var feedbackEmail: String { + return string(for: ConfigKeys.feedbackEmailAddress.rawValue) ?? "" + } + + public var platformName: String { + return string(for: ConfigKeys.platformName.rawValue) ?? "" + } + + private var appStoreId: String { + return string(for: ConfigKeys.appstoreID.rawValue) ?? "0000000000" + } + + public var appStoreLink: String { + "itms-apps://itunes.apple.com/app/id\(appStoreId)?mt=8" + } + + public var faq: URL? { + guard let urlString = string(for: ConfigKeys.faq.rawValue), + let url = URL(string: urlString) else { + return nil + } + return url + } + + public var URIScheme: String { + return string(for: ConfigKeys.URIScheme.rawValue) ?? "" + } +} + +// Mark - For testing and SwiftUI preview +#if DEBUG +public class ConfigMock: Config { + private let config: [String: Any] = [ + "API_HOST_URL": "https://www.example.com", + "OAUTH_CLIENT_ID": "oauth_client_id", + "FEEDBACK_EMAIL_ADDRESS": "example@mail.com", + "PLATFORM_NAME": "OpenEdx", + "TOKEN_TYPE": "JWT", + "WHATS_NEW_ENABLED": false, + "AGREEMENT_URLS": [ + "PRIVACY_POLICY_URL": "https://www.example.com/privacy", + "TOS_URL": "https://www.example.com/tos", + "DATA_SELL_CONSENT_URL": "https://www.example.com/sell", + "COOKIE_POLICY_URL": "https://www.example.com/cookie", + "SUPPORTED_LANGUAGES": ["es"] + ], + "GOOGLE": [ + "ENABLED": true, + "CLIENT_ID": "clientId" + ], + "FACEBOOK": [ + "ENABLED": true, + "FACEBOOK_APP_ID": "facebookAppId", + "CLIENT_TOKEN": "client_token" + ], + "MICROSOFT": [ + "ENABLED": true, + "APP_ID": "appId" + ], + "APPLE_SIGNIN": [ + "ENABLED": true + ] + ] + + public init() { + super.init(properties: config) + } +} +#endif diff --git a/Core/Core/Configuration/Config/DiscoveryConfig.swift b/Core/Core/Configuration/Config/DiscoveryConfig.swift new file mode 100644 index 000000000..884800441 --- /dev/null +++ b/Core/Core/Configuration/Config/DiscoveryConfig.swift @@ -0,0 +1,64 @@ +// +// DiscoveryConfig.swift +// Core +// +// Created by SaeedBashir on 12/18/23. +// + +import Foundation + +public enum DiscoveryConfigType: String { + case native + case webview + case none +} + +private enum DiscoveryKeys: String, RawStringExtractable { + case discoveryType = "TYPE" + case webview = "WEBVIEW" + case baseURL = "BASE_URL" + case courseDetailTemplate = "COURSE_DETAIL_TEMPLATE" + case programDetailTemplate = "PROGRAM_DETAIL_TEMPLATE" +} + +public class DiscoveryWebviewConfig: NSObject { + public let baseURL: String? + public let courseDetailTemplate: String? + public let programDetailTemplate: String? + + init(dictionary: [String: AnyObject]) { + baseURL = dictionary[DiscoveryKeys.baseURL] as? String + courseDetailTemplate = dictionary[DiscoveryKeys.courseDetailTemplate] as? String + programDetailTemplate = dictionary[DiscoveryKeys.programDetailTemplate] as? String + } +} + +public class DiscoveryConfig: NSObject { + public let type: DiscoveryConfigType + public let webview: DiscoveryWebviewConfig + + init(dictionary: [String: AnyObject]) { + type = (dictionary[DiscoveryKeys.discoveryType] as? String).flatMap { + DiscoveryConfigType(rawValue: $0) + } ?? .none + webview = DiscoveryWebviewConfig(dictionary: dictionary[DiscoveryKeys.webview] as? [String: AnyObject] ?? [:]) + } + + public var enabled: Bool { + return type != .none + } +} + +private let key = "DISCOVERY" +extension Config { + public var discovery: DiscoveryConfig { + DiscoveryConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} + +private let programKey = "PROGRAM" +extension Config { + public var program: DiscoveryConfig { + DiscoveryConfig(dictionary: self[programKey] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/FacebookConfig.swift b/Core/Core/Configuration/Config/FacebookConfig.swift new file mode 100644 index 000000000..e8d7f1ad0 --- /dev/null +++ b/Core/Core/Configuration/Config/FacebookConfig.swift @@ -0,0 +1,38 @@ +// +// FacebookConfig.swift +// Core +// +// Created by Eugene Yatsenko on 22.11.2023. +// + +import Foundation + +private enum FacebookKeys: String { + case enabled = "ENABLED" + case appID = "FACEBOOK_APP_ID" + case clientToken = "CLIENT_TOKEN" +} + +public final class FacebookConfig: NSObject { + public var enabled: Bool = false + private(set) var appID: String? + private(set) var clientToken: String? + + private var requiredKeysAvailable: Bool { + return appID != nil && clientToken != nil + } + + init(dictionary: [String: AnyObject]) { + appID = dictionary[FacebookKeys.appID.rawValue] as? String + clientToken = dictionary[FacebookKeys.clientToken.rawValue] as? String + super.init() + enabled = requiredKeysAvailable && dictionary[FacebookKeys.enabled.rawValue] as? Bool == true + } +} + +private let key = "FACEBOOK" +extension Config { + public var facebook: FacebookConfig { + FacebookConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/FeaturesConfig.swift b/Core/Core/Configuration/Config/FeaturesConfig.swift new file mode 100644 index 000000000..b07cd3c6d --- /dev/null +++ b/Core/Core/Configuration/Config/FeaturesConfig.swift @@ -0,0 +1,30 @@ +// +// FeaturesConfig.swift +// Core +// +// Created by Muhammad Umer on 11/14/23. +// + +import Foundation + +private enum FeaturesKeys: String { + case whatNewEnabled = "WHATS_NEW_ENABLED" + case startupScreenEnabled = "PRE_LOGIN_EXPERIENCE_ENABLED" +} + +public class FeaturesConfig: NSObject { + public var whatNewEnabled: Bool + public var startupScreenEnabled: Bool + + init(dictionary: [String: Any]) { + whatNewEnabled = dictionary[FeaturesKeys.whatNewEnabled.rawValue] as? Bool ?? false + startupScreenEnabled = dictionary[FeaturesKeys.startupScreenEnabled.rawValue] as? Bool ?? false + super.init() + } +} + +extension Config { + public var features: FeaturesConfig { + return FeaturesConfig(dictionary: properties) + } +} diff --git a/Core/Core/Configuration/Config/FirebaseConfig.swift b/Core/Core/Configuration/Config/FirebaseConfig.swift new file mode 100644 index 000000000..ba4a6f020 --- /dev/null +++ b/Core/Core/Configuration/Config/FirebaseConfig.swift @@ -0,0 +1,105 @@ +// +// FirebaseConfig.swift +// Core +// +// Created by Muhammad Umer on 11/12/23. +// + +import Foundation +import FirebaseCore + +private enum FirebaseKeys: String { + case enabled = "ENABLED" + case analyticsSource = "ANALYTICS_SOURCE" + case cloudMessagingEnabled = "CLOUD_MESSAGING_ENABLED" + case apiKey = "API_KEY" + case bundleID = "BUNDLE_ID" + case clientID = "CLIENT_ID" + case databaseURL = "DATABASE_URL" + case gcmSenderID = "GCM_SENDER_ID" + case googleAppID = "GOOGLE_APP_ID" + case projectID = "PROJECT_ID" + case reversedClientID = "REVERSED_CLIENT_ID" + case storageBucket = "STORAGE_BUCKET" +} + +enum AnalyticsSource: String { + case firebase + case segment + case none +} + +public final class FirebaseConfig: NSObject { + public var enabled: Bool = false + public var cloudMessagingEnabled: Bool = false + public let apiKey: String? + public let bundleID: String? + public let clientID: String? + public let databaseURL: String? + public let gcmSenderID: String? + public let googleAppID: String? + public let projectID: String? + public let reversedClientID: String? + public let storageBucket: String? + + private let analyticsSource: AnalyticsSource + + public var requiredKeysAvailable: Bool { + return apiKey != nil && clientID != nil && googleAppID != nil && gcmSenderID != nil + } + + init(dictionary: [String: AnyObject]) { + apiKey = dictionary[FirebaseKeys.apiKey.rawValue] as? String + clientID = dictionary[FirebaseKeys.clientID.rawValue] as? String + googleAppID = dictionary[FirebaseKeys.googleAppID.rawValue] as? String + gcmSenderID = dictionary[FirebaseKeys.gcmSenderID.rawValue] as? String + bundleID = dictionary[FirebaseKeys.bundleID.rawValue] as? String + databaseURL = dictionary[FirebaseKeys.databaseURL.rawValue] as? String + projectID = dictionary[FirebaseKeys.projectID.rawValue] as? String + reversedClientID = dictionary[FirebaseKeys.reversedClientID.rawValue] as? String + storageBucket = dictionary[FirebaseKeys.storageBucket.rawValue] as? String + + let analyticsSource = dictionary[FirebaseKeys.analyticsSource.rawValue] as? String + self.analyticsSource = AnalyticsSource(rawValue: analyticsSource ?? AnalyticsSource.none.rawValue) ?? .none + + super.init() + + enabled = requiredKeysAvailable && dictionary[FirebaseKeys.enabled.rawValue] as? Bool == true + let cloudMessagingEnabled = dictionary[FirebaseKeys.cloudMessagingEnabled.rawValue] as? Bool ?? false + self.cloudMessagingEnabled = enabled && cloudMessagingEnabled + } + + public var isAnalyticsSourceSegment: Bool { + return analyticsSource == AnalyticsSource.segment + } + + public var isAnalyticsSourceFirebase: Bool { + return analyticsSource == AnalyticsSource.firebase + } + + public var firebaseOptions: FirebaseOptions? { + if enabled, + requiredKeysAvailable, + let bundleID = bundleID, + let googleAppID = googleAppID, + let gcmSenderID = gcmSenderID { + let firebaseOptions = FirebaseOptions(googleAppID: googleAppID, + gcmSenderID: gcmSenderID) + firebaseOptions.apiKey = apiKey + firebaseOptions.projectID = projectID + firebaseOptions.bundleID = bundleID + firebaseOptions.clientID = clientID + firebaseOptions.storageBucket = storageBucket + firebaseOptions.databaseURL = databaseURL + } + + return nil + } +} + +private let firebaseKey = "FIREBASE" +extension Config { + public var firebase: FirebaseConfig { + FirebaseConfig(dictionary: self[firebaseKey] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/GoogleConfig.swift b/Core/Core/Configuration/Config/GoogleConfig.swift new file mode 100644 index 000000000..76eeef217 --- /dev/null +++ b/Core/Core/Configuration/Config/GoogleConfig.swift @@ -0,0 +1,37 @@ +// +// GoogleConfig.swift +// Core +// +// Created by Eugene Yatsenko on 27.11.2023. +// + +import Foundation + +private enum GoogleKeys: String { + case enabled = "ENABLED" + case googlePlusKey = "GOOGLE_PLUS_KEY" + case clientID = "CLIENT_ID" +} + +public final class GoogleConfig: NSObject { + public var enabled: Bool = false + private(set) var googlePlusKey: String? + private(set) var clientID: String? + + private var requiredKeysAvailable: Bool { + return clientID != nil + } + + init(dictionary: [String: AnyObject]) { + clientID = dictionary[GoogleKeys.clientID.rawValue] as? String + super.init() + enabled = requiredKeysAvailable && dictionary[GoogleKeys.enabled.rawValue] as? Bool == true + } +} + +private let key = "GOOGLE" +extension Config { + public var google: GoogleConfig { + GoogleConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/MicrosoftConfig.swift b/Core/Core/Configuration/Config/MicrosoftConfig.swift new file mode 100644 index 000000000..4175a53e2 --- /dev/null +++ b/Core/Core/Configuration/Config/MicrosoftConfig.swift @@ -0,0 +1,35 @@ +// +// MicrosoftConfig.swift +// Core +// +// Created by Eugene Yatsenko on 22.11.2023. +// + +import Foundation + +private enum MicrosoftKeys: String { + case enabled = "ENABLED" + case appID = "APP_ID" +} + +public final class MicrosoftConfig: NSObject { + public var enabled: Bool = false + private(set) var appID: String? + + private var requiredKeysAvailable: Bool { + return appID != nil + } + + init(dictionary: [String: AnyObject]) { + appID = dictionary[MicrosoftKeys.appID.rawValue] as? String + super.init() + enabled = requiredKeysAvailable && dictionary[MicrosoftKeys.enabled.rawValue] as? Bool == true + } +} + +private let key = "MICROSOFT" +extension Config { + public var microsoft: MicrosoftConfig { + MicrosoftConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/ThemeConfig.swift b/Core/Core/Configuration/Config/ThemeConfig.swift new file mode 100644 index 000000000..239cd69fe --- /dev/null +++ b/Core/Core/Configuration/Config/ThemeConfig.swift @@ -0,0 +1,28 @@ +// +// ThemeConfig.swift +// Core +// +// Created by Anton Yarmolenka on 01/12/2023. +// + +import Foundation + +private enum ThemeKeys: String { + case isRoundedCorners = "ROUNDED_CORNERS_STYLE" +} + +public final class ThemeConfig: NSObject { + public var isRoundedCorners: Bool + + init(dictionary: [String: AnyObject]) { + isRoundedCorners = dictionary[ThemeKeys.isRoundedCorners.rawValue] as? Bool != false + super.init() + } +} + +private let ThemeKey = "THEME" +extension Config { + public var theme: ThemeConfig { + ThemeConfig(dictionary: self[ThemeKey] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/UIComponentsConfig.swift b/Core/Core/Configuration/Config/UIComponentsConfig.swift new file mode 100644 index 000000000..03a1ca4bb --- /dev/null +++ b/Core/Core/Configuration/Config/UIComponentsConfig.swift @@ -0,0 +1,37 @@ +// +// UIComponentsConfig.swift +// Core +// +// Created by Vadim Kuznetsov on 5.12.23. +// + +import Foundation + +private enum Keys: String, RawStringExtractable { + case courseNestedListEnabled = "COURSE_NESTED_LIST_ENABLED" + case courseTopTabBarEnabled = "COURSE_TOP_TAB_BAR_ENABLED" + case courseBannerEnabled = "COURSE_BANNER_ENABLED" + case courseUnitProgressEnabled = "COURSE_UNIT_PROGRESS_ENABLED" +} + +public class UIComponentsConfig: NSObject { + public var courseNestedListEnabled: Bool + public var courseBannerEnabled: Bool + public var courseUnitProgressEnabled: Bool + public var courseTopTabBarEnabled: Bool + + init(dictionary: [String: Any]) { + courseNestedListEnabled = dictionary[Keys.courseNestedListEnabled] as? Bool ?? false + courseBannerEnabled = dictionary[Keys.courseBannerEnabled] as? Bool ?? false + courseUnitProgressEnabled = dictionary[Keys.courseUnitProgressEnabled] as? Bool ?? false + courseTopTabBarEnabled = dictionary[Keys.courseTopTabBarEnabled] as? Bool ?? false + super.init() + } +} + +private let key = "UI_COMPONENTS" +extension Config { + public var uiComponents: UIComponentsConfig { + return UIComponentsConfig(dictionary: properties[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Data/CoreStorage.swift b/Core/Core/Data/CoreStorage.swift index 4ff71e963..b3a8faca0 100644 --- a/Core/Core/Data/CoreStorage.swift +++ b/Core/Core/Data/CoreStorage.swift @@ -10,8 +10,29 @@ import Foundation public protocol CoreStorage { var accessToken: String? {get set} var refreshToken: String? {get set} + var appleSignFullName: String? {get set} + var appleSignEmail: String? {get set} var cookiesDate: String? {get set} + var reviewLastShownVersion: String? {get set} + var lastReviewDate: Date? {get set} var user: DataLayer.User? {get set} var userSettings: UserSettings? {get set} func clear() } + +#if DEBUG +public class CoreStorageMock: CoreStorage { + public var accessToken: String? + public var refreshToken: String? + public var appleSignFullName: String? + public var appleSignEmail: String? + public var cookiesDate: String? + public var reviewLastShownVersion: String? + public var lastReviewDate: Date? + public var user: DataLayer.User? + public var userSettings: UserSettings? + public func clear() {} + + public init() {} +} +#endif diff --git a/Core/Core/Data/Model/Data_Certificate.swift b/Core/Core/Data/Model/Data_Certificate.swift new file mode 100644 index 000000000..0b6475ed1 --- /dev/null +++ b/Core/Core/Data/Model/Data_Certificate.swift @@ -0,0 +1,24 @@ +// +// Data_Certificate.swift +// Core +// +// Created by Vladimir Chekyrta on 18.12.2023. +// + +import Foundation + +public extension DataLayer { + struct Certificate: Codable { + public let url: String? + + public init(url: String?) { + self.url = url + } + } +} + +public extension DataLayer.Certificate { + var domain: Certificate { + return Certificate(url: url ?? "") + } +} diff --git a/Core/Core/Data/Model/Data_Dashboard.swift b/Core/Core/Data/Model/Data_Dashboard.swift index 4133670a9..603af54b2 100644 --- a/Core/Core/Data/Model/Data_Dashboard.swift +++ b/Core/Core/Data/Model/Data_Dashboard.swift @@ -8,32 +8,48 @@ import Foundation public extension DataLayer { + // MARK: - CourseEnrollments struct CourseEnrollments: Codable { + public let enrollments: Enrollments + + enum CodingKeys: String, CodingKey { + case enrollments + } + + public init(enrollments: Enrollments) { + self.enrollments = enrollments + } + } + + // MARK: - Enrollments + struct Enrollments: Codable { public let next: String? public let previous: String? - public let count: Int - public let numPages: Int - public let currentPage: Int - public let start: Int + public let count: Int? + public let numPages: Int? + public let currentPage: Int? + public let start: Int? public let results: [Result] - + enum CodingKeys: String, CodingKey { - case next = "next" - case previous = "previous" - case count = "count" + case next + case previous + case count case numPages = "num_pages" case currentPage = "current_page" - case start = "start" - case results = "results" + case start + case results } - - public init(next: String?, - previous: String?, - count: Int, - numPages: Int, - currentPage: Int, - start: Int, - results: [Result]) { + + public init( + next: String?, + previous: String?, + count: Int?, + numPages: Int?, + currentPage: Int?, + start: Int?, + results: [Result] + ) { self.next = next self.previous = previous self.count = count @@ -43,57 +59,219 @@ public extension DataLayer { self.results = results } } - + // MARK: - Result struct Result: Codable { public let auditAccessExpires: String? public let created: String + public let mode: Mode public let isActive: Bool - public let course: Course - public let certificate: Certificate - + public let course: DashboardCourse + public let courseModes: [CourseMode] + enum CodingKeys: String, CodingKey { case auditAccessExpires = "audit_access_expires" - case created = "created" + case created + case mode case isActive = "is_active" - case course = "course" - case certificate = "certificate" + case course + case courseModes = "course_modes" } - - public init(auditAccessExpires: String?, created: String,// mode: Mode, - isActive: Bool, course: Course, certificate: Certificate) { + + public init( + auditAccessExpires: String?, + created: String, + mode: Mode, + isActive: Bool, + course: DashboardCourse, + courseModes: [CourseMode] + ) { self.auditAccessExpires = auditAccessExpires self.created = created + self.mode = mode self.isActive = isActive self.course = course - self.certificate = certificate + self.courseModes = courseModes + } + } + + // MARK: - Course + struct DashboardCourse: Codable { + public let id: String + public let name: String + public let number: String + public let org: String + public let start: String? + public let startDisplay: String + public let startType: StartType + public let end: String? + public let dynamicUpgradeDeadline: String? + public let subscriptionID: String + public let coursewareAccess: CoursewareAccess + public let media: Media + public let courseImage: String + public let courseAbout: String + public let courseSharingUtmParameters: CourseSharingUtmParameters + public let courseUpdates: String + public let courseHandouts: String + public let discussionURL: String + public let videoOutline: String? + public let isSelfPaced: Bool + + enum CodingKeys: String, CodingKey { + case id + case name + case number + case org + case start + case startDisplay = "start_display" + case startType = "start_type" + case end + case dynamicUpgradeDeadline = "dynamic_upgrade_deadline" + case subscriptionID = "subscription_id" + case coursewareAccess = "courseware_access" + case media + case courseImage = "course_image" + case courseAbout = "course_about" + case courseSharingUtmParameters = "course_sharing_utm_parameters" + case courseUpdates = "course_updates" + case courseHandouts = "course_handouts" + case discussionURL = "discussion_url" + case videoOutline = "video_outline" + case isSelfPaced = "is_self_paced" + } + + public init( + id: String, + name: String, + number: String, + org: String, + start: String?, + startDisplay: String, + startType: StartType, + end: String?, + dynamicUpgradeDeadline: String?, + subscriptionID: String, + coursewareAccess: CoursewareAccess, + media: Media, + courseImage: String, + courseAbout: String, + courseSharingUtmParameters: CourseSharingUtmParameters, + courseUpdates: String, + courseHandouts: String, + discussionURL: String, + videoOutline: String?, + isSelfPaced: Bool + ) { + self.id = id + self.name = name + self.number = number + self.org = org + self.start = start + self.startDisplay = startDisplay + self.startType = startType + self.end = end + self.dynamicUpgradeDeadline = dynamicUpgradeDeadline + self.subscriptionID = subscriptionID + self.coursewareAccess = coursewareAccess + self.media = media + self.courseImage = courseImage + self.courseAbout = courseAbout + self.courseSharingUtmParameters = courseSharingUtmParameters + self.courseUpdates = courseUpdates + self.courseHandouts = courseHandouts + self.discussionURL = discussionURL + self.videoOutline = videoOutline + self.isSelfPaced = isSelfPaced + } + } + + // MARK: - CourseMode + struct CourseMode: Codable { + public let slug: Mode? + public let sku: String? + public let androidSku: String? + public let iosSku: String? + + enum CodingKeys: String, CodingKey { + case slug + case sku + case androidSku = "android_sku" + case iosSku = "ios_sku" + } + + public init(slug: Mode?, sku: String?, androidSku: String?, iosSku: String?) { + self.slug = slug + self.sku = sku + self.androidSku = androidSku + self.iosSku = iosSku + } + } + + enum Mode: String, Codable { + case audit + case honor + case verified + case unknown + + public init(from decoder: Decoder) throws { + let rawValue = try decoder.singleValueContainer().decode(RawValue.self) + self = Mode(rawValue: rawValue) ?? .unknown + } + } + + // MARK: - CourseSharingUtmParameters + struct CourseSharingUtmParameters: Codable { + public let facebook: String + public let twitter: String + } + + // MARK: - CoursewareAccess + struct CoursewareAccess: Codable { + public let hasAccess: Bool + public let errorCode: String? + public let developerMessage: String? + public let userMessage: String? + public let additionalContextUserMessage: String? + public let userFragment: String? + + enum CodingKeys: String, CodingKey { + case hasAccess = "has_access" + case errorCode = "error_code" + case developerMessage = "developer_message" + case userMessage = "user_message" + case additionalContextUserMessage = "additional_context_user_message" + case userFragment = "user_fragment" } } } public extension DataLayer.CourseEnrollments { func domain(baseURL: String) -> [CourseItem] { - - return results.map { course in - let imageURL = baseURL + (course.course.media.courseImage?.url?.addingPercentEncoding( - withAllowedCharacters: .urlQueryAllowed) ?? "") + return enrollments.results.map { result in + let course = result.course + + let imageUrl = course.media.courseImage?.url ?? "" + let encodedUrl = imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let fullImageURL = baseURL + encodedUrl + return CourseItem( - name: course.course.name, - org: course.course.org, + name: course.name, + org: course.org, shortDescription: "", - imageURL: imageURL, + imageURL: fullImageURL, isActive: true, - courseStart: course.course.start != nil ? Date(iso8601: course.course.start!) : nil, - courseEnd: course.course.end != nil ? Date(iso8601: course.course.end!) : nil, - enrollmentStart: course.course.enrollmentStart != nil - ? Date(iso8601: course.course.enrollmentStart!) + courseStart: course.start != nil ? Date(iso8601: course.start!) : nil, + courseEnd: course.end != nil ? Date(iso8601: course.end!) : nil, + enrollmentStart: course.start != nil + ? Date(iso8601: course.start!) : nil, - enrollmentEnd: course.course.enrollmentEnd != nil - ? Date(iso8601: course.course.enrollmentEnd!) + enrollmentEnd: course.end != nil + ? Date(iso8601: course.end!) : nil, - courseID: course.course.id, - numPages: numPages, - coursesCount: count + courseID: course.id, + numPages: enrollments.numPages ?? 1, + coursesCount: enrollments.count ?? 0 ) } } diff --git a/Core/Core/Data/Model/Data_Discovery.swift b/Core/Core/Data/Model/Data_Discovery.swift index 6bbc014b5..cb8dd9be8 100644 --- a/Core/Core/Data/Model/Data_Discovery.swift +++ b/Core/Core/Data/Model/Data_Discovery.swift @@ -90,8 +90,15 @@ public extension DataLayer { } enum StartType: String, Codable { - case empty case timestamp + case string + case empty + case unknown + + public init(from decoder: Decoder) throws { + let rawValue = try decoder.singleValueContainer().decode(RawValue.self) + self = StartType(rawValue: rawValue) ?? .unknown + } } } diff --git a/Core/Core/Data/Model/Data_MyCourse.swift b/Core/Core/Data/Model/Data_MyCourse.swift deleted file mode 100644 index f11e27bd7..000000000 --- a/Core/Core/Data/Model/Data_MyCourse.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// Data_Dashboard.swift -// Core -// -// Created by  Stepanok Ivan on 19.09.2022. -// - -import Foundation - -// MARK: "/api/mobile/v1/users/\(username)/course_enrollments/" - -public extension DataLayer { - - struct MyCourse: Codable { - public let auditAccessExpires: String? - public let created: String - public let mode: String - public let isActive: Bool - public let course: DashboardCourse - public let certificate: Certificate? - - enum CodingKeys: String, CodingKey { - case auditAccessExpires = "audit_access_expires" - case created - case mode - case isActive = "is_active" - case course - case certificate = "certificate" - } - } - - // MARK: - Certificate - struct Certificate: Codable { - public let url: String? - - public init(url: String?) { - self.url = url - } - } - - // MARK: - Course - struct DashboardCourse: Codable { - public let id: String - public let name: String - public let number: String - public let org: String - public let start: String? - public let startDisplay: String? - public let startType: String? - public let end: String? - public let dynamicUpgradeDeadline: String? - public let subscriptionID: String - public let coursewareAccess: CoursewareAccess - public let media: DashboardMedia - public let courseImage: String - public let courseAbout: String - public let courseSharingUtmParameters: CourseSharingUtmParameters - public let courseUpdates: String - public let courseHandouts: String - public let discussionURL: String - public let videoOutline: String? - public let isSelfPaced: Bool - - enum CodingKeys: String, CodingKey { - case id - case name - case number - case org - case start - case startDisplay = "start_display" - case startType = "start_type" - case end - case dynamicUpgradeDeadline = "dynamic_upgrade_deadline" - case subscriptionID = "subscription_id" - case coursewareAccess = "courseware_access" - case media - case courseImage = "course_image" - case courseAbout = "course_about" - case courseSharingUtmParameters = "course_sharing_utm_parameters" - case courseUpdates = "course_updates" - case courseHandouts = "course_handouts" - case discussionURL = "discussion_url" - case videoOutline = "video_outline" - case isSelfPaced = "is_self_paced" - } - } - - // MARK: - CourseSharingUtmParameters - struct CourseSharingUtmParameters: Codable { - public let facebook: String - public let twitter: String - } - - // MARK: - CoursewareAccess - struct CoursewareAccess: Codable { - public let hasAccess: Bool - public let errorCode: String? - public let developerMessage: String? - public let userMessage: String? - public let additionalContextUserMessage: String? - public let userFragment: String? - - enum CodingKeys: String, CodingKey { - case hasAccess = "has_access" - case errorCode = "error_code" - case developerMessage = "developer_message" - case userMessage = "user_message" - case additionalContextUserMessage = "additional_context_user_message" - case userFragment = "user_fragment" - } - } - - // MARK: - Media - struct DashboardMedia: Codable { - public let courseImage: CourseImage - - enum CodingKeys: String, CodingKey { - case courseImage = "course_image" - } - } - - // MARK: - CourseImage - struct CourseImage: Codable { - public let url: String - public let name: String - - enum CodingKeys: String, CodingKey { - case url = "uri" - case name = "name" - } - } -} - -public extension DataLayer.Certificate { - var domain: Certificate { - return Certificate(url: url ?? "") - } -} - -public extension DataLayer.MyCourse { - func domain(baseURL: String) -> CourseItem { - let imageURL = baseURL + (course.media.courseImage.url.addingPercentEncoding( - withAllowedCharacters: .urlQueryAllowed) ?? "") - return CourseItem( - name: course.name, - org: course.org, - shortDescription: course.courseAbout, - imageURL: imageURL, - isActive: isActive, - courseStart: course.start != nil ? Date(iso8601: course.start!) : nil, - courseEnd: course.end != nil ? Date(iso8601: course.end!) : nil, - enrollmentStart: nil, - enrollmentEnd: nil, - courseID: course.id, - numPages: 1, - coursesCount: 0 - ) - } -} diff --git a/Core/Core/Data/Model/Data_UserProfile.swift b/Core/Core/Data/Model/Data_UserProfile.swift index d0207ea95..d3541cfd2 100644 --- a/Core/Core/Data/Model/Data_UserProfile.swift +++ b/Core/Core/Data/Model/Data_UserProfile.swift @@ -12,7 +12,7 @@ import Foundation // MARK: - UserProfile public extension DataLayer { struct UserProfile: Codable { - public let id: Int + public let id: Int? public let accountPrivacy: AccountPrivacy? public let profileImage: ProfileImage? public let username: String? @@ -70,12 +70,13 @@ public extension DataLayer { public enum AccountPrivacy: String, Codable { case privateAccess = "private" case allUsers = "all_users" + case allUsersBig = "ALL_USERS" public var boolValue: Bool { switch self { case .privateAccess: return false - case .allUsers: + case .allUsers, .allUsersBig: return true } } diff --git a/Core/Core/Data/Model/UserSettings.swift b/Core/Core/Data/Model/UserSettings.swift index e709fca00..ab113a74e 100644 --- a/Core/Core/Data/Model/UserSettings.swift +++ b/Core/Core/Data/Model/UserSettings.swift @@ -7,19 +7,32 @@ import Foundation -public struct UserSettings: Codable { +public struct UserSettings: Codable, Hashable { public var wifiOnly: Bool - public var downloadQuality: VideoQuality - - public init(wifiOnly: Bool, downloadQuality: VideoQuality) { + public var streamingQuality: StreamingQuality + public var downloadQuality: DownloadQuality + + public init( + wifiOnly: Bool, + streamingQuality: StreamingQuality, + downloadQuality: DownloadQuality + ) { self.wifiOnly = wifiOnly + self.streamingQuality = streamingQuality self.downloadQuality = downloadQuality } } -public enum VideoQuality: Codable { +public enum StreamingQuality: Codable { case auto case low case medium case high } + +public enum DownloadQuality: Codable, CaseIterable { + case auto + case low_360 + case medium_540 + case high_720 +} diff --git a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index 3dc8a8e03..cd49e4370 100644 --- a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -1,8 +1,10 @@ - + + + diff --git a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift index f250fc49c..3a577ec50 100644 --- a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift +++ b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift @@ -10,14 +10,15 @@ import Combine public protocol CorePersistenceProtocol { func publisher() -> AnyPublisher - func getAllDownloadData() -> [DownloadData] - func addToDownloadQueue(blocks: [CourseBlock]) - func getNextBlockForDownloading() -> DownloadData? - func getDownloadsForCourse(_ courseId: String) -> [DownloadData] - func downloadData(by blockId: String) -> DownloadData? + func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) + func getNextBlockForDownloading() -> DownloadDataTask? func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) - func deleteDownloadData(id: String) throws - func saveDownloadData(data: DownloadData) + func deleteDownloadDataTask(id: String) throws + func saveDownloadDataTask(data: DownloadDataTask) + func downloadDataTask(for blockId: String) -> DownloadDataTask? + func downloadDataTask(for blockId: String, completion: @escaping (DownloadDataTask?) -> Void) + func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void) + func getDownloadDataTasksForCourse(_ courseId: String, completion: @escaping ([DownloadDataTask]) -> Void) } public final class CoreBundle { diff --git a/Core/Core/Data/Repository/AuthRepository.swift b/Core/Core/Data/Repository/AuthRepository.swift index 1ed62fd68..e4adf93ea 100644 --- a/Core/Core/Data/Repository/AuthRepository.swift +++ b/Core/Core/Data/Repository/AuthRepository.swift @@ -9,9 +9,10 @@ import Foundation public protocol AuthRepositoryProtocol { func login(username: String, password: String) async throws -> User + func login(externalToken: String, backend: String) async throws -> User func getCookies(force: Bool) async throws func getRegistrationFields() async throws -> [PickerFields] - func registerUser(fields: [String: String]) async throws -> User + func registerUser(fields: [String: String], isSocial: Bool) async throws -> User func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] func resetPassword(email: String) async throws -> ResetPassword } @@ -20,9 +21,9 @@ public class AuthRepository: AuthRepositoryProtocol { private let api: API private var appStorage: CoreStorage - private let config: Config + private let config: ConfigProtocol - public init(api: API, appStorage: CoreStorage, config: Config) { + public init(api: API, appStorage: CoreStorage, config: ConfigProtocol) { self.api = api self.appStorage = appStorage self.config = config @@ -33,7 +34,8 @@ public class AuthRepository: AuthRepositoryProtocol { let endPoint = AuthEndpoint.getAccessToken( username: username, password: password, - clientId: config.oAuthClientId + clientId: config.oAuthClientId, + tokenType: config.tokenType.rawValue ) let authResponse = try await api.requestData(endPoint).mapResponse(DataLayer.AuthResponse.self) guard let accessToken = authResponse.accessToken, @@ -52,7 +54,32 @@ public class AuthRepository: AuthRepositoryProtocol { appStorage.user = user return user.domain } - + + public func login(externalToken: String, backend: String) async throws -> User { + let endPoint = AuthEndpoint.exchangeAccessToken( + externalToken: externalToken, + backend: backend, + clientId: config.oAuthClientId, + tokenType: config.tokenType.rawValue + ) + let authResponse = try await api.requestData(endPoint).mapResponse(DataLayer.AuthResponse.self) + guard let accessToken = authResponse.accessToken, + let refreshToken = authResponse.refreshToken else { + if let error = authResponse.error, error == DataLayer.AuthResponse.invalidGrant { + throw APIError.invalidGrant + } else { + throw APIError.unknown + } + } + + appStorage.accessToken = accessToken + appStorage.refreshToken = refreshToken + + let user = try await api.requestData(AuthEndpoint.getUserInfo).mapResponse(DataLayer.User.self) + appStorage.user = user + return user.domain + } + public func resetPassword(email: String) async throws -> ResetPassword { let response = try await api.requestData(AuthEndpoint.resetPassword(email: email)) .mapResponse(DataLayer.ResetPassword.self) @@ -80,11 +107,14 @@ public class AuthRepository: AuthRepositoryProtocol { } @discardableResult - public func registerUser(fields: [String: String]) async throws -> User { + public func registerUser(fields: [String: String], isSocial: Bool) async throws -> User { try await api.requestData(AuthEndpoint.registerUser(fields)) + if isSocial { + return try await login(externalToken: fields["access_token"] ?? "", backend: fields["provider"] ?? "") + } return try await login(username: fields["username"] ?? "", password: fields["password"] ?? "") } - + public func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] { let result = try await api.requestData(AuthEndpoint.validateRegistrationFields(fields)) if let fieldsResult = try JSONSerialization.jsonObject(with: result, options: []) as? [String: Any] { @@ -103,7 +133,11 @@ class AuthRepositoryMock: AuthRepositoryProtocol { func login(username: String, password: String) async throws -> User { User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") } - + + public func login(externalToken: String, backend: String) async throws -> User { + User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") + } + func resetPassword(email: String) async throws -> ResetPassword { ResetPassword(success: true, responseText: "Success reset") } @@ -143,7 +177,7 @@ class AuthRepositoryMock: AuthRepositoryProtocol { return fields } - func registerUser(fields: [String: String]) async throws -> User { + func registerUser(fields: [String: String], isSocial: Bool) async throws -> User { User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") } diff --git a/Core/Core/Domain/AuthInteractor.swift b/Core/Core/Domain/AuthInteractor.swift index 94e202364..45868cbc9 100644 --- a/Core/Core/Domain/AuthInteractor.swift +++ b/Core/Core/Domain/AuthInteractor.swift @@ -11,15 +11,16 @@ import Foundation public protocol AuthInteractorProtocol { @discardableResult func login(username: String, password: String) async throws -> User + @discardableResult + func login(externalToken: String, backend: String) async throws -> User func resetPassword(email: String) async throws -> ResetPassword func getCookies(force: Bool) async throws func getRegistrationFields() async throws -> [PickerFields] - func registerUser(fields: [String: String]) async throws -> User + func registerUser(fields: [String: String], isSocial: Bool) async throws -> User func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] } public class AuthInteractor: AuthInteractorProtocol { - private let repository: AuthRepositoryProtocol public init(repository: AuthRepositoryProtocol) { @@ -31,6 +32,11 @@ public class AuthInteractor: AuthInteractorProtocol { return try await repository.login(username: username, password: password) } + @discardableResult + public func login(externalToken: String, backend: String) async throws -> User { + return try await repository.login(externalToken: externalToken, backend: backend) + } + public func resetPassword(email: String) async throws -> ResetPassword { try await repository.resetPassword(email: email) } @@ -43,8 +49,8 @@ public class AuthInteractor: AuthInteractorProtocol { return try await repository.getRegistrationFields() } - public func registerUser(fields: [String: String]) async throws -> User { - return try await repository.registerUser(fields: fields) + public func registerUser(fields: [String: String], isSocial: Bool) async throws -> User { + return try await repository.registerUser(fields: fields, isSocial: isSocial) } public func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] { diff --git a/Core/Core/Domain/Model/CourseBlockModel.swift b/Core/Core/Domain/Model/CourseBlockModel.swift index c22907af4..24a74bcf4 100644 --- a/Core/Core/Domain/Model/CourseBlockModel.swift +++ b/Core/Core/Domain/Model/CourseBlockModel.swift @@ -8,6 +8,10 @@ import Foundation public struct CourseStructure: Equatable { + public static func == (lhs: CourseStructure, rhs: CourseStructure) -> Bool { + return lhs.id == rhs.id + } + public let id: String public let graded: Bool public let completion: Double @@ -15,7 +19,7 @@ public struct CourseStructure: Equatable { public let encodedVideo: String public let displayName: String public let topicID: String? - public let childs: [CourseChapter] + public var childs: [CourseChapter] public let media: DataLayer.CourseMedia //FIXME Domain model public let certificate: Certificate? @@ -42,20 +46,33 @@ public struct CourseStructure: Equatable { self.media = media self.certificate = certificate } - - public static func == (lhs: CourseStructure, rhs: CourseStructure) -> Bool { - return lhs.id == rhs.id + + public func totalVideosSizeInBytes(downloadQuality: DownloadQuality) -> Int { + childs.flatMap { + $0.childs.flatMap { $0.childs.flatMap { $0.childs.compactMap { $0 } } } + } + .filter { $0.isDownloadable } + .compactMap { $0.encodedVideo?.video(downloadQuality: downloadQuality)?.fileSize } + .reduce(.zero) { $0 + $1 } } - + + public func totalVideosSizeInMb(downloadQuality: DownloadQuality) -> Double { + Double(totalVideosSizeInBytes(downloadQuality: downloadQuality)) / 1024.0 / 1024.0 + } + + public func totalVideosSizeInGb(downloadQuality: DownloadQuality) -> Double { + Double(totalVideosSizeInBytes(downloadQuality: downloadQuality)) / 1024.0 / 1024.0 / 1024.0 + } + } -public struct CourseChapter { - +public struct CourseChapter: Identifiable { + public let blockId: String public let id: String public let displayName: String public let type: BlockType - public let childs: [CourseSequential] + public var childs: [CourseSequential] public init( blockId: String, @@ -72,15 +89,15 @@ public struct CourseChapter { } } -public struct CourseSequential { - +public struct CourseSequential: Identifiable { + public let blockId: String public let id: String public let displayName: String public let type: BlockType public let completion: Double - public let childs: [CourseVertical] - + public var childs: [CourseVertical] + public var isDownloadable: Bool { return childs.first(where: { $0.isDownloadable }) != nil } @@ -102,19 +119,23 @@ public struct CourseSequential { } } -public struct CourseVertical { +public struct CourseVertical: Identifiable, Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + public let blockId: String public let id: String public let courseId: String public let displayName: String public let type: BlockType public let completion: Double - public let childs: [CourseBlock] + public var childs: [CourseBlock] public var isDownloadable: Bool { return childs.first(where: { $0.isDownloadable }) != nil } - + public init( blockId: String, id: String, @@ -144,24 +165,33 @@ public struct SubtitleUrl: Equatable { } } -public struct CourseBlock: Equatable { +public struct CourseBlock: Hashable, Identifiable { + public static func == (lhs: CourseBlock, rhs: CourseBlock) -> Bool { + lhs.id == rhs.id && + lhs.blockId == rhs.blockId && + lhs.completion == rhs.completion + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + public let blockId: String public let id: String public let courseId: String public let topicId: String? public let graded: Bool - public let completion: Double + public var completion: Double public let type: BlockType public let displayName: String public let studentUrl: String public let subtitles: [SubtitleUrl]? - public let videoUrl: String? - public let youTubeUrl: String? - + public let encodedVideo: CourseBlockEncodedVideo? + public var isDownloadable: Bool { - return videoUrl != nil + encodedVideo?.isDownloadable ?? false } - + public init( blockId: String, id: String, @@ -173,8 +203,7 @@ public struct CourseBlock: Equatable { displayName: String, studentUrl: String, subtitles: [SubtitleUrl]? = nil, - videoUrl: String? = nil, - youTubeUrl: String? = nil + encodedVideo: CourseBlockEncodedVideo? ) { self.blockId = blockId self.id = id @@ -186,7 +215,109 @@ public struct CourseBlock: Equatable { self.displayName = displayName self.studentUrl = studentUrl self.subtitles = subtitles - self.videoUrl = videoUrl - self.youTubeUrl = youTubeUrl + self.encodedVideo = encodedVideo + } +} + +public struct CourseBlockEncodedVideo { + + public let fallback: CourseBlockVideo? + public let desktopMP4: CourseBlockVideo? + public let mobileHigh: CourseBlockVideo? + public let mobileLow: CourseBlockVideo? + public let hls: CourseBlockVideo? + public let youtube: CourseBlockVideo? + + public init( + fallback: CourseBlockVideo?, + youtube: CourseBlockVideo?, + desktopMP4: CourseBlockVideo?, + mobileHigh: CourseBlockVideo?, + mobileLow: CourseBlockVideo?, + hls: CourseBlockVideo? + ) { + self.fallback = fallback + self.youtube = youtube + self.desktopMP4 = desktopMP4 + self.mobileHigh = mobileHigh + self.mobileLow = mobileLow + self.hls = hls + } + + public var isDownloadable: Bool { + [hls, desktopMP4, mobileHigh, mobileLow, fallback] + .contains { $0?.isDownloadable == true } + } + + public func video(downloadQuality: DownloadQuality) -> CourseBlockVideo? { + switch downloadQuality { + case .auto: + [mobileLow, mobileHigh, desktopMP4, fallback, hls] + .first(where: { $0?.isDownloadable == true })? + .flatMap { $0 } + case .high_720: + [desktopMP4, mobileHigh, mobileLow, fallback, hls] + .first(where: { $0?.isDownloadable == true })? + .flatMap { $0 } + case .medium_540: + [mobileHigh, mobileLow, desktopMP4, fallback, hls] + .first(where: { $0?.isDownloadable == true })? + .flatMap { $0 } + case .low_360: + [mobileLow, mobileHigh, desktopMP4, fallback, hls] + .first(where: { $0?.isDownloadable == true })? + .flatMap { $0 } + } + } + + public func video(streamingQuality: StreamingQuality) -> CourseBlockVideo? { + switch streamingQuality { + case .auto: + [mobileLow, mobileHigh, desktopMP4, fallback, hls] + .compactMap { $0 } + .sorted(by: { ($0?.streamPriority ?? 0) < ($1?.streamPriority ?? 0) }) + .first? + .flatMap { $0 } + case .high: + [desktopMP4, mobileHigh, mobileLow, fallback, hls] + .compactMap { $0 } + .first? + .flatMap { $0 } + case .medium: + [mobileHigh, mobileLow, desktopMP4, fallback, hls] + .compactMap { $0 } + .first? + .flatMap { $0 } + case .low: + [mobileLow, mobileHigh, desktopMP4, fallback, hls] + .compactMap { $0 } + .first(where: { $0?.isDownloadable == true })? + .flatMap { $0 } + } + } + + public var youtubeVideoUrl: String? { + youtube?.url + } + +} + +public struct CourseBlockVideo: Equatable { + public let url: String? + public let fileSize: Int? + public let streamPriority: Int? + + public init(url: String?, fileSize: Int?, streamPriority: Int?) { + self.url = url + self.fileSize = fileSize + self.streamPriority = streamPriority + } + + public var isVideoURL: Bool { + [".mp4", ".m3u8"].contains(where: { url?.contains($0) == true }) + } + + public var isDownloadable: Bool { + [".mp4"].contains(where: { url?.contains($0) == true }) } } diff --git a/Core/Core/Domain/Model/CourseDetailBlock.swift b/Core/Core/Domain/Model/CourseDetailBlock.swift index 8230d44df..8f2f9db73 100644 --- a/Core/Core/Domain/Model/CourseDetailBlock.swift +++ b/Core/Core/Domain/Model/CourseDetailBlock.swift @@ -39,7 +39,9 @@ public enum BlockType: String { case chapter case video case problem + case survey case unknown + case dragAndDropV2 = "drag-and-drop-v2" public var image: Image { switch self { diff --git a/Core/Core/Domain/Model/PickerFields.swift b/Core/Core/Domain/Model/PickerFields.swift index ebfed8907..8791f10ef 100644 --- a/Core/Core/Domain/Model/PickerFields.swift +++ b/Core/Core/Domain/Model/PickerFields.swift @@ -26,7 +26,11 @@ public struct PickerFields { public let name: String public let instructions: String public let options: [Option] - + + public var isHonorCode: Bool { + name == "honor_code" + } + public struct Option { public let value: String public let name: String diff --git a/Core/Core/Domain/Model/UserProfile.swift b/Core/Core/Domain/Model/UserProfile.swift index 0d8ca7e2c..03b19990a 100644 --- a/Core/Core/Domain/Model/UserProfile.swift +++ b/Core/Core/Domain/Model/UserProfile.swift @@ -39,4 +39,16 @@ public struct UserProfile: Hashable { self.shortBiography = shortBiography self.isFullProfile = isFullProfile } + + public init() { + self.avatarUrl = "" + self.name = "" + self.username = "" + self.dateJoined = Date() + self.yearOfBirth = 0 + self.country = "" + self.spokenLanguage = "" + self.shortBiography = "" + self.isFullProfile = true + } } diff --git a/Core/Core/Extensions/CGColorExtension.swift b/Core/Core/Extensions/CGColorExtension.swift index 2454e026d..f1e871974 100644 --- a/Core/Core/Extensions/CGColorExtension.swift +++ b/Core/Core/Extensions/CGColorExtension.swift @@ -28,17 +28,6 @@ public extension CGColor { public extension Color { func uiColor() -> UIColor { - let scanner = Scanner(string: description.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)) - var hexNumber: UInt64 = 0 - var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0, a: CGFloat = 0.0 - - let result = scanner.scanHexInt64(&hexNumber) - if result { - r = CGFloat((hexNumber & 0xFF000000) >> 24) / 255 - g = CGFloat((hexNumber & 0x00FF0000) >> 16) / 255 - b = CGFloat((hexNumber & 0x0000FF00) >> 8) / 255 - a = CGFloat(hexNumber & 0x000000FF) / 255 - } - return UIColor(red: r, green: g, blue: b, alpha: a) + return UIColor(self) } } diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index fae6cd14c..36f93d5ee 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -9,12 +9,18 @@ import Foundation public extension Date { init(iso8601: String) { - let date: Date + let formats = ["yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"] + var date: Date var dateFormatter: DateFormatter? dateFormatter = DateFormatter() - dateFormatter?.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + dateFormatter?.locale = Locale(identifier: "en_US_POSIX") + + date = formats.compactMap { format in + dateFormatter?.dateFormat = format + return dateFormatter?.date(from: iso8601) + } + .first ?? Date() - date = dateFormatter?.date(from: iso8601) ?? Date() defer { dateFormatter = nil } @@ -25,7 +31,8 @@ public extension Date { let formatter = RelativeDateTimeFormatter() formatter.locale = .current formatter.unitsStyle = .full - if self.description == Date().description { + formatter.locale = Locale(identifier: "en_US_POSIX") + if description == Date().description { return CoreLocalization.Date.justNow } else { return formatter.localizedString(for: self, relativeTo: Date()) @@ -38,6 +45,7 @@ public extension Date { let components = calendar.dateComponents([.year, .month, .day], from: now) let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss,SSS" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") let dateString = "\(components.year!)-\(components.month!)-\(components.day!) \(subtitleTime)" guard let date = dateFormatter.date(from: dateString) else { self = now @@ -67,9 +75,23 @@ public enum DateStringStyle { case monthYear case lastPost case iso8601 + case shortWeekdayMonthDayYear } public extension Date { + + func secondsSinceMidnight() -> Double { + let calendar = Calendar.current + let components = calendar.dateComponents([.hour, .minute, .second], from: self) + + guard let hours = components.hour, let minutes = components.minute, let seconds = components.second else { + return 0.0 + } + + let totalSeconds = Double(hours) * 3600.0 + Double(minutes) * 60.0 + Double(seconds) + return totalSeconds + } + func dateToString(style: DateStringStyle) -> String { let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") @@ -87,6 +109,8 @@ public extension Date { dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy case .iso8601: dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + case .shortWeekdayMonthDayYear: + applyShortWeekdayMonthDayYear(dateFormatter: dateFormatter) } let date = dateFormatter.string(from: self) @@ -106,7 +130,7 @@ public extension Date { let days = Calendar.current.dateComponents([.day], from: self, to: Date()) if let day = days.day { if day < 2 { - return self.timeAgoDisplay() + return timeAgoDisplay() } else { return date } @@ -115,6 +139,58 @@ public extension Date { } case .iso8601: return date + case .shortWeekdayMonthDayYear: + return getShortWeekdayMonthDayYear(dateFormatterString: date) + } + } + + private func applyShortWeekdayMonthDayYear(dateFormatter: DateFormatter) { + if isCurrentYear() { + let days = Calendar.current.dateComponents([.day], from: self, to: Date()) + if let day = days.day, (-6 ... -2).contains(day) { + dateFormatter.dateFormat = "EEEE" + } else { + dateFormatter.dateFormat = "MMMM d" + } + } else { + dateFormatter.dateFormat = "MMMM d, yyyy" + } + } + + private func getShortWeekdayMonthDayYear(dateFormatterString: String) -> String { + let days = Calendar.current.dateComponents([.day], from: self, to: Date()) + + if let day = days.day { + guard isCurrentYear() else { + // It's past year or future year + return dateFormatterString + } + + switch day { + case -6...(-2): + return dateFormatterString + case 2...6: + return timeAgoDisplay() + case -1: + return CoreLocalization.CourseDates.tomorrow + case 1: + return CoreLocalization.CourseDates.yesterday + default: + if day > 6 || day < -6 { + return dateFormatterString + } else { + // It means, date is in hours past due or upcoming + return timeAgoDisplay() + } + } + } else { + return dateFormatterString } } + + func isCurrentYear() -> Bool { + let selfYear = Calendar.current.component(.year, from: self) + let runningYear = Calendar.current.component(.year, from: Date()) + return selfYear == runningYear + } } diff --git a/Core/Core/Extensions/DebugLog.swift b/Core/Core/Extensions/DebugLog.swift new file mode 100644 index 000000000..1ceb35482 --- /dev/null +++ b/Core/Core/Extensions/DebugLog.swift @@ -0,0 +1,25 @@ +// +// DebugLog.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation + +public func debugLog( + _ item: Any..., + filename: String = #file, + line: Int = #line, + funcname: String = #function +) { +#if DEBUG + print( + """ + 🕗 \(Date()) + 📄 \(filename.components(separatedBy: "/").last ?? "") \(line) \(funcname) + ℹ️ \(item) + """ + ) +#endif +} diff --git a/Core/Core/Extensions/Notification.swift b/Core/Core/Extensions/Notification.swift index fd12e5aab..4f47b478a 100644 --- a/Core/Core/Extensions/Notification.swift +++ b/Core/Core/Extensions/Notification.swift @@ -10,4 +10,9 @@ import Foundation public extension Notification.Name { static let onCourseEnrolled = Notification.Name("onCourseEnrolled") static let onTokenRefreshFailed = Notification.Name("onTokenRefreshFailed") + static let onActualVersionReceived = Notification.Name("onActualVersionReceived") + static let onAppUpgradeAccountSettingsTapped = Notification.Name("onAppUpgradeAccountSettingsTapped") + static let onNewVersionAvaliable = Notification.Name("onNewVersionAvaliable") + static let webviewReloadNotification = Notification.Name("webviewReloadNotification") + static let onBlockCompletion = Notification.Name.init("onBlockCompletion") } diff --git a/Core/Core/Extensions/RawStringExtactable.swift b/Core/Core/Extensions/RawStringExtactable.swift new file mode 100644 index 000000000..8ab42f2ed --- /dev/null +++ b/Core/Core/Extensions/RawStringExtactable.swift @@ -0,0 +1,27 @@ +// +// RawStringExtactable.swift +// Core +// +// Created by SaeedBashir on 12/18/23. +// + +import Foundation + +public protocol RawStringExtractable { + var rawValue: String { get } +} + +public protocol DictionaryExtractionExtension { + associatedtype Key + associatedtype Value + subscript(key: Key) -> Value? { get } +} + +extension Dictionary: DictionaryExtractionExtension {} + +public extension DictionaryExtractionExtension where Self.Key == String { + + subscript(key :RawStringExtractable) -> Value? { + return self[key.rawValue] + } +} diff --git a/Core/Core/Extensions/ResultExtension.swift b/Core/Core/Extensions/ResultExtension.swift new file mode 100644 index 000000000..d9a327768 --- /dev/null +++ b/Core/Core/Extensions/ResultExtension.swift @@ -0,0 +1,23 @@ +// +// ResultExtension.swift +// Core +// +// Created by Eugene Yatsenko on 11.10.2023. +// + +import Foundation + +extension Result { + @discardableResult + public func success(_ handler: (Success) -> Void) -> Self { + guard case let .success(value) = self else { return self } + handler(value) + return self + } + @discardableResult + public func failure(_ handler: (Failure) -> Void) -> Self { + guard case let .failure(error) = self else { return self } + handler(error) + return self + } +} diff --git a/Core/Core/Extensions/SKStoreReviewControllerExtension.swift b/Core/Core/Extensions/SKStoreReviewControllerExtension.swift new file mode 100644 index 000000000..be214f661 --- /dev/null +++ b/Core/Core/Extensions/SKStoreReviewControllerExtension.swift @@ -0,0 +1,20 @@ +// +// SKStoreReviewControllerExtension.swift +// Core +// +// Created by  Stepanok Ivan on 16.11.2023. +// + +import Foundation +import StoreKit + +extension SKStoreReviewController { + public static func requestReviewInCurrentScene() { + if let scene = UIApplication.shared.connectedScenes + .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { + DispatchQueue.main.async { + requestReview(in: scene) + } + } + } +} diff --git a/Core/Core/Extensions/UIApplicationExtension.swift b/Core/Core/Extensions/UIApplicationExtension.swift index 616b9f466..022c1770a 100644 --- a/Core/Core/Extensions/UIApplicationExtension.swift +++ b/Core/Core/Extensions/UIApplicationExtension.swift @@ -6,6 +6,7 @@ // import UIKit +import Theme extension UIApplication { @@ -44,10 +45,10 @@ extension UINavigationController { navigationBar.shadowImage = UIImage() let image = CoreAssets.arrowLeft.image - navigationBar.backIndicatorImage = image.withTintColor(CoreAssets.accentColor.color) - navigationBar.tintColor = .clear - navigationBar.backIndicatorTransitionMaskImage = image.withTintColor(CoreAssets.accentColor.color) - navigationBar.titleTextAttributes = [.foregroundColor: CoreAssets.textPrimary.color] + navigationBar.backIndicatorImage = image.withTintColor(Theme.UIColors.accentColor) + navigationBar.backItem?.backButtonTitle = " " + navigationBar.backIndicatorTransitionMaskImage = image.withTintColor(Theme.UIColors.accentColor) + navigationBar.titleTextAttributes = [.foregroundColor: Theme.UIColors.textPrimary] } } @@ -58,6 +59,10 @@ extension UINavigationController: UIGestureRecognizerDelegate { } public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - return viewControllers.count > 1 + if #available(iOS 17, *) { + return false + } else { + return viewControllers.count > 1 + } } } diff --git a/Core/Core/Extensions/UINavigationController+Animation.swift b/Core/Core/Extensions/UINavigationController+Animation.swift index 21671ea44..4f720f78c 100644 --- a/Core/Core/Extensions/UINavigationController+Animation.swift +++ b/Core/Core/Extensions/UINavigationController+Animation.swift @@ -24,7 +24,7 @@ public extension UINavigationController { duration: CFTimeInterval = 0.3 ) { addTransition(transitionType: type, duration: duration) - pushViewController(vc, animated: false) + pushViewController(vc, animated: UIAccessibility.isVoiceOverRunning) } private func addTransition( diff --git a/Core/Core/Extensions/ViewExtension.swift b/Core/Core/Extensions/ViewExtension.swift index d6584cbcf..1c8833612 100644 --- a/Core/Core/Extensions/ViewExtension.swift +++ b/Core/Core/Extensions/ViewExtension.swift @@ -6,8 +6,9 @@ // import Foundation -import SwiftUIIntrospect +@_spi(Advanced) import SwiftUIIntrospect import SwiftUI +import Theme public extension View { @@ -89,11 +90,59 @@ public extension View { .padding(.horizontal, 48) } + @ViewBuilder func frameLimit(sizePortrait: CGFloat = 560, sizeLandscape: CGFloat = 648) -> some View { - return HStack { - Spacer(minLength: 0) - self.frame(maxWidth: UIDevice.current.orientation == .portrait ? sizePortrait : sizeLandscape) - Spacer(minLength: 0) + if UIDevice.current.userInterfaceIdiom == .pad { + HStack { + Spacer(minLength: 0) + self.frame(maxWidth: UIDevice.current.orientation.isPortrait ? sizePortrait : sizeLandscape) + Spacer(minLength: 0) + } + } else { self } + } + + @ViewBuilder + func adaptiveHStack( + spacing: CGFloat = 0, + currentOrientation: UIInterfaceOrientation, + @ViewBuilder content: () -> Content + ) -> some View { + if currentOrientation.isLandscape && UIDevice.current.userInterfaceIdiom != .pad { + VStack(alignment: .center, spacing: spacing, content: content) + } else if currentOrientation.isPortrait && UIDevice.current.userInterfaceIdiom != .pad { + HStack(spacing: spacing, content: content) + } else if UIDevice.current.userInterfaceIdiom != .phone { + HStack(spacing: spacing, content: content) + } + } + + @ViewBuilder + func adaptiveStack( + spacing: CGFloat = 0, + isHorizontal: Bool, + @ViewBuilder content: () -> Content + ) -> some View { + if isHorizontal, UIDevice.current.userInterfaceIdiom != .pad { + HStack(spacing: spacing, content: content) + } else { + VStack(alignment: .center, spacing: spacing, content: content) + } + } + + @ViewBuilder + func adaptiveNavigationStack( + spacing: CGFloat = 0, + isHorizontal: Bool, + @ViewBuilder content: () -> Content + ) -> some View { + if UIDevice.current.userInterfaceIdiom == .pad { + HStack(spacing: spacing, content: content) + } else { + if isHorizontal { + HStack(alignment: .top, spacing: spacing, content: content) + } else { + VStack(alignment: .center, spacing: spacing, content: content) + } } } @@ -118,19 +167,40 @@ public extension View { } } + func roundedBackgroundWeb( + _ color: Color = Theme.Colors.background, + strokeColor: Color = Theme.Colors.backgroundStroke, + ipadMaxHeight: CGFloat = .infinity, + maxIpadWidth: CGFloat = 420 + ) -> some View { + var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + return VStack { + VStack {}.frame(height: 1) + ZStack { + self + .frame(maxWidth: maxIpadWidth, maxHeight: idiom == .pad ? ipadMaxHeight : .infinity) + .clipShape(RoundedCorners(tl: 24, tr: 24)) + RoundedCorners(tl: 24, tr: 24) + .stroke(style: StrokeStyle(lineWidth: 1)) + .foregroundColor(strokeColor) + .offset(y: -1) + } + } + } + func hideNavigationBar() -> some View { if #available(iOS 16.0, *) { return self.navigationBarHidden(true) } else { return self.introspect( .navigationView(style: .stack), - on: .iOS(.v14, .v15, .v16, .v17), + on: .iOS(.v15...), scope: .ancestor) { $0.isNavigationBarHidden = true } } } - + func hideScrollContentBackground() -> some View { if #available(iOS 16.0, *) { return self.scrollContentBackground(.hidden) @@ -171,6 +241,21 @@ public extension View { } } +public extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } +} + private struct FirstAppear: ViewModifier { let action: () -> Void @@ -195,7 +280,18 @@ public extension Image { .scaledToFit() .frame(height: 24) .padding(.horizontal, 8) - .padding(.top, topPadding) + .offset(y: topPadding) .foregroundColor(color) } } + +public extension EnvironmentValues { + var isHorizontal: Bool { + if UIDevice.current.userInterfaceIdiom != .pad { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + return windowScene.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true + } + } + return false + } +} diff --git a/Core/Core/Network/API.swift b/Core/Core/Network/API.swift index d891c7af5..7e3964b62 100644 --- a/Core/Core/Network/API.swift +++ b/Core/Core/Network/API.swift @@ -12,9 +12,9 @@ import WebKit public final class API { private let session: Alamofire.Session - private let config: Config + private let config: ConfigProtocol - public init(session: Session, config: Config) { + public init(session: Session, config: ConfigProtocol) { self.session = session self.config = config } @@ -65,13 +65,25 @@ public final class API { if !route.path.isEmpty { url = url.appendingPathComponent(route.path) } - return try await session.request( + + let result = session.request( url, method: route.httpMethod, parameters: parameters, encoding: encoding, headers: route.headers - ).validateResponse().serializingData().value + ).validateResponse().serializingData() + + let latestVersion = await result.response.response?.headers["EDX-APP-LATEST-VERSION"] + + if await result.response.response?.statusCode != 426 { + if let latestVersion = latestVersion { + NotificationCenter.default.post(name: .onActualVersionReceived, object: latestVersion) + } + } + + return try await result.value + } private func callCookies( diff --git a/Core/Core/Network/Alamofire+Error.swift b/Core/Core/Network/Alamofire+Error.swift index 8984b36df..277a79fed 100644 --- a/Core/Core/Network/Alamofire+Error.swift +++ b/Core/Core/Network/Alamofire+Error.swift @@ -8,6 +8,10 @@ import Alamofire public extension Error { + var isUpdateRequeiredError: Bool { + self.asAFError?.responseCode == 426 + } + var isInternetError: Bool { guard let afError = self.asAFError, let urlError = afError.underlyingError as? URLError else { diff --git a/Core/Core/Network/AuthEndpoint.swift b/Core/Core/Network/AuthEndpoint.swift index c477c451c..e93e5c860 100644 --- a/Core/Core/Network/AuthEndpoint.swift +++ b/Core/Core/Network/AuthEndpoint.swift @@ -9,7 +9,8 @@ import Foundation import Alamofire enum AuthEndpoint: EndPointType { - case getAccessToken(username: String, password: String, clientId: String) + case getAccessToken(username: String, password: String, clientId: String, tokenType: String) + case exchangeAccessToken(externalToken: String, backend: String, clientId: String, tokenType: String) case getUserInfo case getAuthCookies case getRegisterFields @@ -21,6 +22,8 @@ enum AuthEndpoint: EndPointType { switch self { case .getAccessToken: return "/oauth2/access_token" + case let .exchangeAccessToken(_, backend, _, _): + return "/oauth2/exchange_access_token/\(backend)/" case .getUserInfo: return "/api/mobile/v0.5/my_user_info" case .getAuthCookies: @@ -38,7 +41,7 @@ enum AuthEndpoint: EndPointType { var httpMethod: HTTPMethod { switch self { - case .getAccessToken: + case .getAccessToken, .exchangeAccessToken: return .post case .getUserInfo: return .get @@ -61,12 +64,22 @@ enum AuthEndpoint: EndPointType { var task: HTTPTask { switch self { - case let .getAccessToken(username, password, clientId): + case let .getAccessToken(username, password, clientId, tokenType): let params: [String: Encodable] = [ "grant_type": Constants.GrantTypePassword, "client_id": clientId, "username": username, - "password": password + "password": password, + "token_type": tokenType, + "asymmetric_jwt": true + ] + return .requestParameters(parameters: params, encoding: URLEncoding.httpBody) + case let .exchangeAccessToken(externalToken, _, clientId, tokenType): + let params: [String: Encodable] = [ + "client_id": clientId, + "token_type": tokenType, + "access_token": externalToken, + "asymmetric_jwt": true ] return .requestParameters(parameters: params, encoding: URLEncoding.httpBody) case .getUserInfo: diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift index 774c767f0..4c2e6879f 100644 --- a/Core/Core/Network/DownloadManager.swift +++ b/Core/Core/Network/DownloadManager.swift @@ -12,42 +12,66 @@ import Combine public enum DownloadState: String { case waiting case inProgress - case paused case finished + + public var order: Int { + switch self { + case .inProgress: + 1 + case .waiting: + 2 + case .finished: + 3 + } + } } public enum DownloadType: String { case video } -public struct DownloadData { +public struct DownloadDataTask: Identifiable, Hashable { public let id: String public let courseId: String public let url: String public let fileName: String - public let progress: Double + public let displayName: String + public var progress: Double public let resumeData: Data? - public let state: DownloadState + public var state: DownloadState public let type: DownloadType - + public let fileSize: Int + + public var fileSizeInMb: Double { + Double(fileSize) / 1024.0 / 1024.0 + } + + public var fileSizeInMbText: String { + String(format: "%.2fMB", fileSizeInMb) + } + public init( id: String, courseId: String, url: String, fileName: String, + displayName: String, progress: Double, resumeData: Data?, state: DownloadState, - type: DownloadType + type: DownloadType, + fileSize: Int ) { self.id = id self.courseId = courseId self.url = url self.fileName = fileName + self.displayName = displayName self.progress = progress self.resumeData = resumeData self.state = state self.type = type + self.fileSize = fileSize } } @@ -57,26 +81,57 @@ public class NoWiFiError: LocalizedError { //sourcery: AutoMockable public protocol DownloadManagerProtocol { + var currentDownloadTask: DownloadDataTask? { get } func publisher() -> AnyPublisher + func eventPublisher() -> AnyPublisher + + func getDownloadTasks() async -> [DownloadDataTask] + func getDownloadTasksForCourse(_ courseId: String) async -> [DownloadDataTask] + func cancelDownloading(courseId: String, blocks: [CourseBlock]) async throws + func cancelDownloading(task: DownloadDataTask) async throws + func cancelDownloading(courseId: String) async throws + func deleteFile(blocks: [CourseBlock]) async + func deleteAllFiles() async + func fileUrl(for blockId: String) async -> URL? + func addToDownloadQueue(blocks: [CourseBlock]) throws - func getDownloadsForCourse(_ courseId: String) -> [DownloadData] - func cancelDownloading(courseId: String, blocks: [CourseBlock]) throws + func isLargeVideosSize(blocks: [CourseBlock]) -> Bool func resumeDownloading() throws - func pauseDownloading() - func deleteFile(blocks: [CourseBlock]) - func deleteAllFiles() func fileUrl(for blockId: String) -> URL? } +public enum DownloadManagerEvent { + case added + case started(DownloadDataTask) + case progress(Double, DownloadDataTask) + case paused(DownloadDataTask) + case canceled(DownloadDataTask) + case finished(DownloadDataTask) + case courseCanceled(String) + case deletedFile(String) + case clearedAll +} + public class DownloadManager: DownloadManagerProtocol { - + + // MARK: - Properties + + public var currentDownloadTask: DownloadDataTask? private let persistence: CorePersistenceProtocol private let appStorage: CoreStorage private let connectivity: ConnectivityProtocol private var downloadRequest: DownloadRequest? - private var currentDownload: DownloadData? private var isDownloadingInProgress: Bool = false - + private var currentDownloadEventPublisher: PassthroughSubject = .init() + private let backgroundTaskProvider = BackgroundTaskProvider() + private var cancellables = Set() + + private var downloadQuality: DownloadQuality { + appStorage.userSettings?.downloadQuality ?? .auto + } + + // MARK: - Init + public init( persistence: CorePersistenceProtocol, appStorage: CoreStorage, @@ -85,146 +140,259 @@ public class DownloadManager: DownloadManagerProtocol { self.persistence = persistence self.appStorage = appStorage self.connectivity = connectivity + self.backgroundTask() + try? self.resumeDownloading() } - + + // MARK: - Publishers + public func publisher() -> AnyPublisher { - return persistence.publisher() + persistence.publisher() } - - public func addToDownloadQueue(blocks: [CourseBlock]) throws { - if userCanDownload() { - persistence.addToDownloadQueue(blocks: blocks) - guard !isDownloadingInProgress else { return } - try newDownload() - } else { - throw NoWiFiError() - } + + public func eventPublisher() -> AnyPublisher { + currentDownloadEventPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() } - - private func newDownload() throws { - if userCanDownload() { - guard let download = persistence.getNextBlockForDownloading() else { - isDownloadingInProgress = false - return + + // MARK: - Intents + + public func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + (blocks.reduce(0) { + $0 + Double($1.encodedVideo?.video(downloadQuality: downloadQuality)?.fileSize ?? 0) + } / 1024 / 1024 / 1024) > 1 + } + + public func getDownloadTasks() async -> [DownloadDataTask] { + await withCheckedContinuation { continuation in + persistence.getDownloadDataTasks { downloads in + continuation.resume(returning: downloads) } - currentDownload = download - try downloadFileWithProgress(download) - } else { - throw NoWiFiError() } } - - private func userCanDownload() -> Bool { - if appStorage.userSettings?.wifiOnly ?? true { - if !connectivity.isMobileData { - return true - } else { - return false + + public func getDownloadTasksForCourse(_ courseId: String) async -> [DownloadDataTask] { + await withCheckedContinuation { continuation in + persistence.getDownloadDataTasksForCourse(courseId) { downloads in + continuation.resume(returning: downloads) } + } + } + + public func addToDownloadQueue(blocks: [CourseBlock]) throws { + if userCanDownload() { + persistence.addToDownloadQueue( + blocks: blocks, + downloadQuality: downloadQuality + ) + currentDownloadEventPublisher.send(.added) + guard !isDownloadingInProgress else { return } + try newDownload() } else { - return true + throw NoWiFiError() } } - - public func getDownloadsForCourse(_ courseId: String) -> [DownloadData] { - return persistence.getDownloadsForCourse(courseId) + + public func resumeDownloading() throws { + try newDownload() } - - public func cancelDownloading(courseId: String, blocks: [CourseBlock]) throws { + + public func cancelDownloading(courseId: String, blocks: [CourseBlock]) async throws { downloadRequest?.cancel() - - let downloaded = getDownloadsForCourse(courseId).filter { $0.state == .finished } + + let downloaded = await getDownloadTasksForCourse(courseId).filter { $0.state == .finished } let blocksForDelete = blocks.filter { block in downloaded.first(where: { $0.id == block.id }) == nil } - - deleteFile(blocks: blocksForDelete) + + await deleteFile(blocks: blocksForDelete) + downloaded.forEach { + currentDownloadEventPublisher.send(.canceled($0)) + } try newDownload() } - - private func downloadFileWithProgress(_ download: DownloadData) throws { - if let url = URL(string: download.url) { - persistence.updateDownloadState( - id: download.id, - state: .inProgress, - resumeData: download.resumeData - ) - self.isDownloadingInProgress = true - if let resumeData = download.resumeData { - downloadRequest = AF.download(resumingWith: resumeData) - } else { - downloadRequest = AF.download(url) - } - #if DEBUG - downloadRequest?.downloadProgress { prog in - let completed = Double(prog.fractionCompleted * 100) - print(">>>>> Downloading", download.url, completed, "%") + + public func cancelDownloading(task: DownloadDataTask) async throws { + downloadRequest?.cancel() + do { + try persistence.deleteDownloadDataTask(id: task.id) + if let fileUrl = await fileUrl(for: task.id) { + try FileManager.default.removeItem(at: fileUrl) } - #endif - downloadRequest?.responseData(completionHandler: { [weak self] data in - guard let self else { return } - if let data = data.value, let url = self.videosFolderUrl() { - self.saveFile(fileName: download.fileName, data: data, folderURL: url) - self.persistence.updateDownloadState( - id: download.id, - state: .finished, - resumeData: nil - ) - try? self.newDownload() - } - }) + currentDownloadEventPublisher.send(.canceled(task)) + } catch { + NSLog("Error deleting file: \(error.localizedDescription)") } - } - - public func resumeDownloading() throws { try newDownload() } - - public func pauseDownloading() { - guard let currentDownload else { return } - downloadRequest?.cancel(byProducingResumeData: { resumeData in - self.persistence.updateDownloadState( - id: currentDownload.id, - state: .paused, - resumeData: resumeData - ) - }) + + public func cancelDownloading(courseId: String) async throws { + let downloads = await getDownloadTasksForCourse(courseId) + for downloadData in downloads { + do { + try persistence.deleteDownloadDataTask(id: downloadData.id) + if let fileUrl = await fileUrl(for: downloadData.id) { + try FileManager.default.removeItem(at: fileUrl) + } + } catch { + debugLog("Error deleting file: \(error.localizedDescription)") + } + } + currentDownloadEventPublisher.send(.courseCanceled(courseId)) + downloadRequest?.cancel() + try newDownload() } - - public func deleteFile(blocks: [CourseBlock]) { + + public func deleteFile(blocks: [CourseBlock]) async { for block in blocks { do { - try persistence.deleteDownloadData(id: block.id) - if let fileUrl = fileUrl(for: block.id) { - try FileManager.default.removeItem(at: fileUrl) + try persistence.deleteDownloadDataTask(id: block.id) + currentDownloadEventPublisher.send(.deletedFile(block.id)) + if let fileURL = await fileUrl(for: block.id) { + try FileManager.default.removeItem(at: fileURL) } } catch { - NSLog("Error deleting file: \(error.localizedDescription)") + debugLog("Error deleting file: \(error.localizedDescription)") } } } - - public func deleteAllFiles() { - let downloadData = persistence.getAllDownloadData() - downloadData.forEach { - if let fileURL = fileUrl(for: $0.id) { + + public func deleteAllFiles() async { + let downloadsData = await getDownloadTasks() + for downloadData in downloadsData { + if let fileURL = await fileUrl(for: downloadData.id) { do { try FileManager.default.removeItem(at: fileURL) } catch { - NSLog("Error deleting All files: \(error.localizedDescription)") + debugLog("Error deleting All files: \(error.localizedDescription)") } } } + currentDownloadEventPublisher.send(.clearedAll) } - + + public func fileUrl(for blockId: String) async -> URL? { + await withCheckedContinuation { continuation in + persistence.downloadDataTask(for: blockId) { [weak self] data in + guard let data = data, data.url.count > 0, data.state == .finished else { + continuation.resume(returning: nil) + return + } + let path = self?.videosFolderUrl + let fileName = data.fileName + continuation.resume(returning: path?.appendingPathComponent(fileName)) + } + } + } + public func fileUrl(for blockId: String) -> URL? { - guard let data = persistence.downloadData(by: blockId), + guard let data = persistence.downloadDataTask(for: blockId), data.url.count > 0, data.state == .finished else { return nil } - let path = videosFolderUrl() + let path = videosFolderUrl let fileName = data.fileName return path?.appendingPathComponent(fileName) } - - private func videosFolderUrl() -> URL? { + + private func newDownload() throws { + guard userCanDownload() else { + throw NoWiFiError() + } + guard let downloadTask = persistence.getNextBlockForDownloading() else { + isDownloadingInProgress = false + return + } + currentDownloadTask = downloadTask + try downloadFileWithProgress(downloadTask) + currentDownloadEventPublisher.send(.started(downloadTask)) + } + + private func userCanDownload() -> Bool { + if appStorage.userSettings?.wifiOnly ?? true { + if !connectivity.isMobileData { + return true + } else { + return false + } + } else { + return true + } + } + + private func downloadFileWithProgress(_ download: DownloadDataTask) throws { + guard let url = URL(string: download.url) else { + return + } + + persistence.updateDownloadState( + id: download.id, + state: .inProgress, + resumeData: download.resumeData + ) + self.isDownloadingInProgress = true + if let resumeData = download.resumeData { + downloadRequest = AF.download(resumingWith: resumeData) + } else { + downloadRequest = AF.download(url) + } + + downloadRequest?.downloadProgress { [weak self] prog in + guard let self else { return } + let fractionCompleted = prog.fractionCompleted + self.currentDownloadTask?.progress = fractionCompleted + self.currentDownloadTask?.state = .inProgress + self.currentDownloadEventPublisher.send(.progress(fractionCompleted, download)) + let completed = Double(fractionCompleted * 100) + debugLog(">>>>> Downloading", download.url, completed, "%") + } + + downloadRequest?.responseData { [weak self] data in + guard let self else { return } + if let data = data.value, let url = self.videosFolderUrl { + self.saveFile(fileName: download.fileName, data: data, folderURL: url) + self.persistence.updateDownloadState( + id: download.id, + state: .finished, + resumeData: nil + ) + self.currentDownloadTask?.state = .finished + self.currentDownloadEventPublisher.send(.finished(download)) + try? self.newDownload() + } + } + } + + private func waitingAll() { + persistence.getDownloadDataTasks { [weak self] tasks in + guard let self else { return } + Task { + for task in tasks.filter({ $0.state == .inProgress }) { + self.persistence.updateDownloadState( + id: task.id, + state: .waiting, + resumeData: nil + ) + self.currentDownloadEventPublisher.send(.added) + } + self.downloadRequest?.cancel() + } + } + } + + // MARK: - Private Intents + + private func backgroundTask() { + backgroundTaskProvider.eventPublisher() + .sink { [weak self] state in + guard let self else { return } + switch state { + case.didBecomeActive: try? self.resumeDownloading() + case .didEnterBackground: self.waitingAll() + } + } + .store(in: &cancellables) + } + + lazy var videosFolderUrl: URL? = { let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let directoryURL = documentDirectoryURL.appendingPathComponent("Files", isDirectory: true) @@ -239,18 +407,96 @@ public class DownloadManager: DownloadManagerProtocol { ) return URL(fileURLWithPath: directoryURL.path) } catch { - print(error.localizedDescription) + debugLog(error.localizedDescription) return nil } } - } - + }() + private func saveFile(fileName: String, data: Data, folderURL: URL) { let fileURL = folderURL.appendingPathComponent(fileName) do { try data.write(to: fileURL) } catch { - NSLog("SaveFile Error", error.localizedDescription) + debugLog("SaveFile Error", error.localizedDescription) + } + } +} + +@available(iOSApplicationExtension, unavailable) +public final class BackgroundTaskProvider { + + private var backgroundTask: UIBackgroundTaskIdentifier = .invalid + private var currentEventPublisher: PassthroughSubject = .init() + + public enum Events { + case didBecomeActive + case didEnterBackground + } + + public func eventPublisher() -> AnyPublisher { + currentEventPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + // MARK: - Init - + + deinit { + NotificationCenter.default.removeObserver( + self, + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } + + public init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(didEnterBackgroundNotification), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(didBecomeActiveNotification), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } + + @objc + func didEnterBackgroundNotification() { + registerBackgroundTask() + currentEventPublisher.send(.didEnterBackground) + } + + @objc + func didBecomeActiveNotification() { + endBackgroundTaskIfActive() + currentEventPublisher.send(.didBecomeActive) + } + + // MARK: - Background Task - + + private func registerBackgroundTask() { + backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in + debugLog("iOS has signaled time has expired") + self?.endBackgroundTaskIfActive() + } + } + + private func endBackgroundTaskIfActive() { + let isBackgroundTaskActive = backgroundTask != .invalid + if isBackgroundTaskActive { + debugLog("Background task ended.") + UIApplication.shared.endBackgroundTask(backgroundTask) + backgroundTask = .invalid } } } @@ -258,32 +504,65 @@ public class DownloadManager: DownloadManagerProtocol { // Mark - For testing and SwiftUI preview #if DEBUG public class DownloadManagerMock: DownloadManagerProtocol { - + public init() { } - + + public var currentDownloadTask: DownloadDataTask? { + return nil + } + public func publisher() -> AnyPublisher { return Just(1).eraseToAnyPublisher() } - + + public func eventPublisher() -> AnyPublisher { + return Just( + .canceled( + .init( + id: "", + courseId: "", + url: "", + fileName: "", + displayName: "", + progress: 1, + resumeData: nil, + state: .inProgress, + type: .video, + fileSize: 0 + ) + ) + ).eraseToAnyPublisher() + } + public func addToDownloadQueue(blocks: [CourseBlock]) { } - - public func getDownloadsForCourse(_ courseId: String) -> [DownloadData] { - return [] + + public func getDownloadTasks() -> [DownloadDataTask] { + [] } - - public func cancelDownloading(courseId: String, blocks: [CourseBlock]) { - + + public func getDownloadTasksForCourse(_ courseId: String) async -> [DownloadDataTask] { + await withCheckedContinuation { continuation in + continuation.resume(returning: []) + } } - - public func resumeDownloading() { - + + public func cancelDownloading(courseId: String, blocks: [CourseBlock]) async throws { + } - - public func pauseDownloading() { + + public func cancelDownloading(task: DownloadDataTask) { + + } + + public func cancelDownloading(courseId: String) async { + + } + + public func resumeDownloading() { } @@ -298,6 +577,10 @@ public class DownloadManagerMock: DownloadManagerProtocol { public func fileUrl(for blockId: String) -> URL? { return nil } - + + public func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + false + } + } #endif diff --git a/Core/Core/Network/RequestInterceptor.swift b/Core/Core/Network/RequestInterceptor.swift index cecdf0570..f27b6f310 100644 --- a/Core/Core/Network/RequestInterceptor.swift +++ b/Core/Core/Network/RequestInterceptor.swift @@ -10,10 +10,10 @@ import Alamofire final public class RequestInterceptor: Alamofire.RequestInterceptor { - private let config: Config + private let config: ConfigProtocol private var storage: CoreStorage - public init(config: Config, storage: CoreStorage) { + public init(config: ConfigProtocol, storage: CoreStorage) { self.config = config self.storage = storage } @@ -35,9 +35,26 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor { // Set the Authorization header value using the access token. if let token = storage.accessToken { - urlRequest.setValue("Bearer " + token, forHTTPHeaderField: "Authorization") + urlRequest.setValue("\(config.tokenType.rawValue) \(token)", forHTTPHeaderField: "Authorization") } + let userAgent: String = { + if let info = Bundle.main.infoDictionary { + let executable: AnyObject = info[kCFBundleExecutableKey as String] as AnyObject? ?? "Unknown" as AnyObject + let bundle: AnyObject = info[kCFBundleIdentifierKey as String] as AnyObject? ?? "Unknown" as AnyObject + let version: AnyObject = info["CFBundleShortVersionString"] as AnyObject? ?? "Unknown" as AnyObject + let os: AnyObject = ProcessInfo.processInfo.operatingSystemVersionString as AnyObject + var mutableUserAgent = NSMutableString(string: "\(executable)/\(bundle) (\(version); OS \(os))") as CFMutableString + let transform = NSString(string: "Any-Latin; Latin-ASCII; [:^ASCII:] Remove") as CFString + if CFStringTransform(mutableUserAgent, nil, transform, false) == true { + return mutableUserAgent as String + } + } + return "Alamofire" + }() + + urlRequest.setValue(userAgent, forHTTPHeaderField: "User-Agent") + completion(.success(urlRequest)) } @@ -84,49 +101,52 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor { private func refreshToken( refreshToken: String, - completion: @escaping (_ succeeded: Bool) -> Void) { - guard !isRefreshing else { return } - - isRefreshing = true - - let url = config.baseURL.appendingPathComponent("/oauth2/access_token") - - let parameters = [ - "grant_type": Constants.GrantTypeRefreshToken, - "client_id": config.oAuthClientId, - "refresh_token": refreshToken - ] - AF.request( - url, - method: .post, - parameters: parameters, - encoding: URLEncoding.httpBody - ).response { [weak self] response in - guard let self = self else { return } - switch response.result { - case let .success(data): - do { - let json = try JSONSerialization.jsonObject( - with: data!, - options: .mutableContainers - ) as? [String: AnyObject] - guard let json, - let accessToken = json["access_token"] as? String, - let refreshToken = json["refresh_token"] as? String, - accessToken.count > 0, - refreshToken.count > 0 else { - return completion(false) - } - self.storage.accessToken = accessToken - self.storage.refreshToken = refreshToken - completion(true) - } catch { - completion(false) + completion: @escaping (_ succeeded: Bool) -> Void + ) { + guard !isRefreshing else { return } + + isRefreshing = true + + let url = config.baseURL.appendingPathComponent("/oauth2/access_token") + + let parameters: [String: Encodable] = [ + "grant_type": Constants.GrantTypeRefreshToken, + "client_id": config.oAuthClientId, + "refresh_token": refreshToken, + "token_type": config.tokenType.rawValue, + "asymmetric_jwt": true + ] + AF.request( + url, + method: .post, + parameters: parameters, + encoding: URLEncoding.httpBody + ).response { [weak self] response in + guard let self = self else { return } + switch response.result { + case let .success(data): + do { + let json = try JSONSerialization.jsonObject( + with: data!, + options: .mutableContainers + ) as? [String: AnyObject] + guard let json, + let accessToken = json["access_token"] as? String, + let refreshToken = json["refresh_token"] as? String, + accessToken.count > 0, + refreshToken.count > 0 else { + return completion(false) } - case .failure: + self.storage.accessToken = accessToken + self.storage.refreshToken = refreshToken + completion(true) + } catch { completion(false) } - self.isRefreshing = false + case .failure: + completion(false) } + self.isRefreshing = false } + } } diff --git a/Core/Core/Providers/Ajax/AjaxProvider.swift b/Core/Core/Providers/Ajax/AjaxProvider.swift new file mode 100644 index 000000000..51dfb3d41 --- /dev/null +++ b/Core/Core/Providers/Ajax/AjaxProvider.swift @@ -0,0 +1,87 @@ +// +// AjaxProvider.swift +// Core +// +// Created by Eugene Yatsenko on 12.12.2023. +// + +import Foundation +import WebKit +import Swinject + +struct AjaxInjection: WebViewScriptInjectionProtocol { + private struct AJAXCallbackData { + private enum Keys: String { + case url = "url" + case statusCode = "status" + case responseText = "response_text" + } + + let url: String + let statusCode: Int + let responseText: String + + init(data: [AnyHashable: Any]) { + url = data[Keys.url.rawValue] as? String ?? "" + statusCode = data[Keys.statusCode.rawValue] as? Int ?? 0 + responseText = data[Keys.responseText.rawValue] as? String ?? "" + } + } + + private enum XBlockCompletionCallbackType: String { + case html = "publish_completion" + case problem = "problem_check" + case dragAndDrop = "do_attempt" + case ora = "render_grade" + } + + private let AJAXCallBackHandler = "ajaxCallbackHandler" + private let ajaxScriptFile = "ajaxHandler" + + var id: String = "AjaxInjection" + var script: String { + guard let url = Bundle(for: CoreBundle.self).url(forResource: ajaxScriptFile, withExtension: "js"), + let script = try? String(contentsOf: url, encoding: .utf8) else { return "" } + return script + } + + var messages: [WebviewMessage]? { + [ + WebviewMessage(name: AJAXCallBackHandler) { result, _ in + guard let data = result as? [AnyHashable: Any] else { return } + let callback = AJAXCallbackData(data: data) + let requestURL = callback.url + + if callback.statusCode != 200 { + return + } + + var complete = false + if isBlockOf(type: .ora, with: requestURL) { + complete = callback.responseText.contains("is--complete") + } else { + complete = isBlockOf(type: .html, with: requestURL) + || isBlockOf(type: .problem, with: requestURL) + || isBlockOf(type: .dragAndDrop, with: requestURL) + } + if complete { + NotificationCenter.default.post( + name: NSNotification.blockCompletion, + object: nil + ) + } + } + ] + } + var forMainFrameOnly: Bool = false + + var injectionTime: WKUserScriptInjectionTime = .atDocumentEnd + + private func isBlockOf(type: XBlockCompletionCallbackType, with requestURL: String) -> Bool { + return requestURL.contains(type.rawValue) + } +} + +public extension NSNotification { + static let blockCompletion = Notification.Name.init("block_completion") +} diff --git a/Core/Core/Providers/Ajax/ajaxHandler.js b/Core/Core/Providers/Ajax/ajaxHandler.js new file mode 100644 index 000000000..dcabf4036 --- /dev/null +++ b/Core/Core/Providers/Ajax/ajaxHandler.js @@ -0,0 +1,18 @@ +//Every time an Ajax call is being invoked the listener will recognize it and will call the native app with the request details + +$(document).ajaxSuccess(function(event, request, settings) { + callNativeApp({ + "status": request.status, + "url":settings.url, + "response_text": request.responseText + }); +}); + +function callNativeApp(data) { + try { + webkit.messageHandlers.ajaxCallbackHandler.postMessage(data); + } + catch(err) { + console.log('The native context does not exist yet'); + } +} diff --git a/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift b/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift new file mode 100644 index 000000000..1ed3d4c06 --- /dev/null +++ b/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift @@ -0,0 +1,94 @@ +// +// AppleAuthProvider.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation +import AuthenticationServices +import Swinject + +public final class AppleAuthProvider: NSObject, ASAuthorizationControllerDelegate { + + private let config: ConfigProtocol + + public init(config: ConfigProtocol) { + self.config = config + super.init() + } + + private var completion: ((Result) -> Void)? + private let appleIDProvider = ASAuthorizationAppleIDProvider() + + public func request(completion: ((Result) -> Void)?) { + let request = appleIDProvider.createRequest() + request.requestedScopes = [.fullName, .email] + + let authorizationController = ASAuthorizationController(authorizationRequests: [request]) + authorizationController.delegate = self + authorizationController.performRequests() + + self.completion = completion + } + + public func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { + guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential else { + completion?(.failure(SocialAuthError.unknownError)) + return + } + + var storage = Container.shared.resolve(CoreStorage.self) + let pncf = PersonNameComponentsFormatter() + + var name = storage?.appleSignFullName ?? "" + if let components = credentials.fullName, !pncf.string(from: components).isEmpty { + name = pncf.string(from: components) + storage?.appleSignFullName = name + } + + var email = storage?.appleSignEmail ?? "" + if let appleEmail = credentials.email, !appleEmail.isEmpty { + email = appleEmail + storage?.appleSignEmail = appleEmail + } + + guard let data = credentials.identityToken, + let code = String(data: data, encoding: .utf8) else { + completion?(.failure(SocialAuthError.unknownError)) + return + } + + debugLog("User id is \(data) \n Full Name is \(name) \n Email id is \(email)") + + let appleCredentials = SocialAuthResponse( + name: name, + email: email, + token: code + ) + + completion?(.success(appleCredentials)) + } + + public func authorizationController( + controller: ASAuthorizationController, + didCompleteWithError error: Error + ) { + debugLog(error) + completion?(.failure(failure(ASAuthorizationError(_nsError: error as NSError)))) + } + + private func failure(_ error: ASAuthorizationError) -> Error { + switch error.code { + case .canceled: + return SocialAuthError.socialAuthCanceled + case .failed: + return SocialAuthError.error(text: CoreLocalization.Error.authorizationFailed) + default: + return error + } + } +} diff --git a/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift b/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift new file mode 100644 index 000000000..a9167451e --- /dev/null +++ b/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift @@ -0,0 +1,27 @@ +// +// SocialAuthError.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation + +public enum SocialAuthError: Error { + case error(text: String) + case socialAuthCanceled + case unknownError +} + +extension SocialAuthError: LocalizedError { + public var errorDescription: String? { + switch self { + case .error(let text): + return text + case .socialAuthCanceled: + return CoreLocalization.socialSignCanceled + case .unknownError: + return CoreLocalization.Error.unknownError + } + } +} diff --git a/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift b/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift new file mode 100644 index 000000000..66ae46b6d --- /dev/null +++ b/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift @@ -0,0 +1,86 @@ +// +// FacebookAuthProvider.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation +import FacebookLogin + +public final class FacebookAuthProvider { + + private let loginManager = LoginManager() + + public init() {} + + @MainActor + public func signIn( + withPresenting: UIViewController + ) async -> Result { + await withCheckedContinuation { continuation in + loginManager.logIn( + permissions: [], + from: withPresenting + ) { result, error in + if let error = error { + continuation.resume(returning: .failure(error)) + return + } + + guard let result = result, + let tokenString = AccessToken.current?.tokenString else { + continuation.resume( + returning: .failure(SocialAuthError.unknownError) + ) + return + } + + if result.isCancelled { + continuation.resume(returning: .failure(SocialAuthError.socialAuthCanceled)) + return + } + + GraphRequest( + graphPath: "me", + parameters: ["fields": "name, email"] + ).start { [weak self] _, result, _ in + guard let self else { return } + guard let userInfo = result as? [String: Any] else { + continuation.resume( + returning: .success( + SocialAuthResponse( + name: "", + email: "", + token: tokenString + ) + ) + ) + return + } + continuation.resume( + returning: .success( + SocialAuthResponse( + name: userInfo["name"] as? String ?? "", + email: userInfo["email"] as? String ?? "", + token: tokenString + ) + ) + ) + } + } + } + } + + 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 new file mode 100644 index 000000000..c600c1735 --- /dev/null +++ b/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift @@ -0,0 +1,52 @@ +// +// GoogleAuthProvider.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import GoogleSignIn +import Foundation + +public final class GoogleAuthProvider { + + public init() {} + + @MainActor + public func signIn( + withPresenting: UIViewController + ) 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)) + return + } + guard let result = result else { + continuation.resume( + returning: .failure( + SocialAuthError.error(text: CoreLocalization.Error.unknownError) + ) + ) + return + } + continuation.resume( + returning: .success( + SocialAuthResponse( + name: result.user.profile?.name ?? "", + email: result.user.profile?.email ?? "", + token: result.user.accessToken.tokenString + ) + ) + ) + } + ) + } + } + + public func signOut() { + GIDSignIn.sharedInstance.signOut() + } +} diff --git a/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift b/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift new file mode 100644 index 000000000..16178b17c --- /dev/null +++ b/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift @@ -0,0 +1,118 @@ +// +// MicrosoftAuthProvider.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation +import MSAL +import Swinject + +public typealias MSLoginCompletionHandler = (account: MSALAccount, token: String) + +public final class MicrosoftAuthProvider { + + private let scopes = ["User.Read", "email"] + private var result: MSALResult? + + public init() {} + + @MainActor + public func signIn( + withPresenting: UIViewController + ) async -> Result { + await withCheckedContinuation { continuation in + do { + let clientApplication = try createClientApplication() + + let webParameters = MSALWebviewParameters(authPresentationViewController: withPresenting) + let parameters = MSALInteractiveTokenParameters(scopes: scopes, webviewParameters: webParameters) + clientApplication.acquireToken(with: parameters) { result, error in + if let error = error { + continuation.resume(returning: .failure(error)) + return + } + + guard let result = result else { + continuation.resume( + returning: .failure( + SocialAuthError.error(text: CoreLocalization.Error.unknownError) + ) + ) + return + } + + self.result = result + let account = result.account + + continuation.resume( + returning: .success( + SocialAuthResponse( + name: account.accountClaims?["name"] as? String ?? "" , + email: account.accountClaims?["email"] as? String ?? "", + token: result.accessToken + ) + ) + ) + } + } catch let error { + continuation.resume(returning: .failure(error)) + } + } + } + + public func signOut() { + do { + let account = try? currentAccount() + + if let account = account { + let application = try createClientApplication() + try application.remove(account) + } + } catch let error { + debugLog("Logout", "Received error signing user out: \(error)") + } + } + + private func createClientApplication() throws -> MSALPublicClientApplication { + guard let config = Container.shared.resolve(ConfigProtocol.self), let appID = config.microsoft.appID else { + throw SocialAuthError.error(text: "Configuration error") + } + let configuration = MSALPublicClientApplicationConfig(clientId: appID) + + do { + return try MSALPublicClientApplication(configuration: configuration) + } catch { + throw SocialAuthError.error(text: error.localizedDescription) + } + } + + @discardableResult + private func currentAccount() throws -> MSALAccount { + let clientApplication = try createClientApplication() + + guard let account = try clientApplication.allAccounts().first else { + throw SocialAuthError.error(text: "Account not found") + } + + 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/Core/Core/Providers/SocialAuth/SocialAuthResponse.swift b/Core/Core/Providers/SocialAuth/SocialAuthResponse.swift new file mode 100644 index 000000000..55589d476 --- /dev/null +++ b/Core/Core/Providers/SocialAuth/SocialAuthResponse.swift @@ -0,0 +1,20 @@ +// +// SocialAuthResponse.swift +// Core +// +// Created by Eugene Yatsenko on 07.12.2023. +// + +import Foundation + +public struct SocialAuthResponse { + public var name: String + public var email: String + public var token: String + + public init(name: String, email: String, token: String) { + self.name = name + self.email = email + self.token = token + } +} diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index 887501306..1d1c84625 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -24,30 +24,16 @@ public typealias AssetImageTypeAlias = ImageAsset.Image // swiftlint:disable identifier_name line_length nesting type_body_length type_name public enum CoreAssets { - public static let authBackground = ImageAsset(name: "authBackground") - public static let checkEmail = ImageAsset(name: "checkEmail") - public static let accentColor = ColorAsset(name: "AccentColor") - public static let alert = ColorAsset(name: "Alert") - public static let avatarStroke = ColorAsset(name: "AvatarStroke") - public static let background = ColorAsset(name: "Background") - public static let backgroundStroke = ColorAsset(name: "BackgroundStroke") - public static let cardViewBackground = ColorAsset(name: "CardViewBackground") - public static let cardViewStroke = ColorAsset(name: "CardViewStroke") - public static let certificateForeground = ColorAsset(name: "CertificateForeground") - public static let commentCellBackground = ColorAsset(name: "CommentCellBackground") - public static let shadowColor = ColorAsset(name: "ShadowColor") - public static let snackbarErrorColor = ColorAsset(name: "SnackbarErrorColor") - public static let snackbarErrorTextColor = ColorAsset(name: "SnackbarErrorTextColor") - public static let snackbarInfoAlert = ColorAsset(name: "SnackbarInfoAlert") - public static let styledButtonBackground = ColorAsset(name: "StyledButtonBackground") - public static let styledButtonText = ColorAsset(name: "StyledButtonText") - public static let textPrimary = ColorAsset(name: "TextPrimary") - public static let textSecondary = ColorAsset(name: "TextSecondary") - public static let textInputBackground = ColorAsset(name: "TextInputBackground") - public static let textInputStroke = ColorAsset(name: "TextInputStroke") - public static let textInputUnfocusedBackground = ColorAsset(name: "TextInputUnfocusedBackground") - public static let textInputUnfocusedStroke = ColorAsset(name: "TextInputUnfocusedStroke") - public static let warning = ColorAsset(name: "warning") + public static let appleButtonColor = ColorAsset(name: "AppleButtonColor") + public static let facebookButtonColor = ColorAsset(name: "FacebookButtonColor") + public static let googleButtonColor = ColorAsset(name: "GoogleButtonColor") + public static let microsoftButtonColor = ColorAsset(name: "MicrosoftButtonColor") + public static let assignmentIcon = ImageAsset(name: "assignment_icon") + public static let calendarIcon = ImageAsset(name: "calendar_icon") + public static let certificateIcon = ImageAsset(name: "certificate_icon") + public static let lockIcon = ImageAsset(name: "lock_icon") + public static let lockWithWatchIcon = ImageAsset(name: "lock_with_watch_icon") + public static let schoolCapIcon = ImageAsset(name: "school_cap_icon") public static let bookCircle = ImageAsset(name: "book.circle") public static let bubbleLeftCircle = ImageAsset(name: "bubble.left.circle") public static let docCircle = ImageAsset(name: "doc.circle") @@ -84,28 +70,46 @@ public enum CoreAssets { public static let profile = ImageAsset(name: "profile") public static let programs = ImageAsset(name: "programs") public static let addPhoto = ImageAsset(name: "addPhoto") + public static let bgDelete = ImageAsset(name: "bg_delete") public static let checkmark = ImageAsset(name: "checkmark") - public static let deleteAccount = ImageAsset(name: "deleteAccount") + public static let deleteChar = ImageAsset(name: "delete_char") + public static let deleteEyes = ImageAsset(name: "delete_eyes") public static let done = ImageAsset(name: "done") public static let gallery = ImageAsset(name: "gallery") public static let leaveProfile = ImageAsset(name: "leaveProfile") public static let logOut = ImageAsset(name: "logOut") public static let noAvatar = ImageAsset(name: "noAvatar") public static let removePhoto = ImageAsset(name: "removePhoto") + public static let iconApple = ImageAsset(name: "icon_apple") + public static let iconFacebookWhite = ImageAsset(name: "icon_facebook_white") + public static let iconGoogleWhite = ImageAsset(name: "icon_google_white") + public static let iconMicrosoftWhite = ImageAsset(name: "icon_microsoft_white") public static let rotateDevice = ImageAsset(name: "rotateDevice") public static let sub = ImageAsset(name: "sub") public static let alarm = ImageAsset(name: "alarm") - public static let appLogo = ImageAsset(name: "appLogo") public static let arrowLeft = ImageAsset(name: "arrowLeft") public static let arrowRight16 = ImageAsset(name: "arrowRight16") public static let certificate = ImageAsset(name: "certificate") public static let check = ImageAsset(name: "check") + public static let checkEmail = ImageAsset(name: "checkEmail") public static let clearInput = ImageAsset(name: "clearInput") public static let edit = ImageAsset(name: "edit") + public static let favorite = ImageAsset(name: "favorite") public static let goodWork = ImageAsset(name: "goodWork") + public static let airmail = ImageAsset(name: "airmail") + public static let defaultMail = ImageAsset(name: "defaultMail") + public static let fastmail = ImageAsset(name: "fastmail") + public static let googlegmail = ImageAsset(name: "googlegmail") + public static let msOutlook = ImageAsset(name: "ms-outlook") + public static let proton = ImageAsset(name: "proton") + public static let readdleSpark = ImageAsset(name: "readdle-spark") + public static let ymail = ImageAsset(name: "ymail") public static let noCourseImage = ImageAsset(name: "noCourseImage") public static let notAvaliable = ImageAsset(name: "notAvaliable") public static let playVideo = ImageAsset(name: "playVideo") + public static let star = ImageAsset(name: "star") + public static let starOutline = ImageAsset(name: "star_outline") + public static let warningFilled = ImageAsset(name: "warning_filled") } // swiftlint:enable identifier_name line_length nesting type_body_length type_name diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index 0197b0494..8a4236511 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -12,11 +12,15 @@ import Foundation public enum CoreLocalization { /// Done public static let done = CoreLocalization.tr("Localizable", "DONE", fallback: "Done") + /// The user canceled the sign-in flow. + public static let socialSignCanceled = CoreLocalization.tr("Localizable", "SOCIAL_SIGN_CANCELED", fallback: "The user canceled the sign-in flow.") public enum Alert { /// ACCEPT public static let accept = CoreLocalization.tr("Localizable", "ALERT.ACCEPT", fallback: "ACCEPT") /// CANCEL public static let cancel = CoreLocalization.tr("Localizable", "ALERT.CANCEL", fallback: "CANCEL") + /// DELETE + public static let delete = CoreLocalization.tr("Localizable", "ALERT.DELETE", fallback: "DELETE") /// Keep editing public static let keepEditing = CoreLocalization.tr("Localizable", "ALERT.KEEP_EDITING", fallback: "Keep editing") /// Leave @@ -29,8 +33,6 @@ public enum CoreLocalization { public static let backToOutline = CoreLocalization.tr("Localizable", "COURSEWARE.BACK_TO_OUTLINE", fallback: "Back to outline") /// Continue public static let `continue` = CoreLocalization.tr("Localizable", "COURSEWARE.CONTINUE", fallback: "Continue") - /// Continue with: - public static let continueWith = CoreLocalization.tr("Localizable", "COURSEWARE.CONTINUE_WITH", fallback: "Continue with:") /// Course content public static let courseContent = CoreLocalization.tr("Localizable", "COURSEWARE.COURSE_CONTENT", fallback: "Course content") /// Course units @@ -51,9 +53,35 @@ public enum CoreLocalization { public static let nextSectionDescriptionLast = CoreLocalization.tr("Localizable", "COURSEWARE.NEXT_SECTION_DESCRIPTION_LAST", fallback: "” press “Next section”.") /// Prev public static let previous = CoreLocalization.tr("Localizable", "COURSEWARE.PREVIOUS", fallback: "Prev") + /// Resume + public static let resume = CoreLocalization.tr("Localizable", "COURSEWARE.RESUME", fallback: "Resume") + /// Resume with: + public static let resumeWith = CoreLocalization.tr("Localizable", "COURSEWARE.RESUME_WITH", fallback: "Resume with:") /// Section “ public static let section = CoreLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section “") } + public enum CourseDates { + /// Completed + public static let completed = CoreLocalization.tr("Localizable", "COURSE_DATES.COMPLETED", fallback: "Completed") + /// Due next + public static let dueNext = CoreLocalization.tr("Localizable", "COURSE_DATES.DUE_NEXT", fallback: "Due next") + /// Item Hidden + public static let itemHidden = CoreLocalization.tr("Localizable", "COURSE_DATES.ITEM_HIDDEN", fallback: "Item Hidden") + /// Items Hidden + public static let itemsHidden = CoreLocalization.tr("Localizable", "COURSE_DATES.ITEMS_HIDDEN", fallback: "Items Hidden") + /// Past due + public static let pastDue = CoreLocalization.tr("Localizable", "COURSE_DATES.PAST_DUE", fallback: "Past due") + /// Today + public static let today = CoreLocalization.tr("Localizable", "COURSE_DATES.TODAY", fallback: "Today") + /// Tomorrow + public static let tomorrow = CoreLocalization.tr("Localizable", "COURSE_DATES.TOMORROW", fallback: "Tomorrow") + /// Unreleased + public static let unreleased = CoreLocalization.tr("Localizable", "COURSE_DATES.UNRELEASED", fallback: "Unreleased") + /// Verified Only + public static let verifiedOnly = CoreLocalization.tr("Localizable", "COURSE_DATES.VERIFIED_ONLY", fallback: "Verified Only") + /// Yesterday + public static let yesterday = CoreLocalization.tr("Localizable", "COURSE_DATES.YESTERDAY", fallback: "Yesterday") + } public enum Date { /// Ended public static let ended = CoreLocalization.tr("Localizable", "DATE.ENDED", fallback: "Ended") @@ -79,6 +107,8 @@ public enum CoreLocalization { public static let downloaded = CoreLocalization.tr("Localizable", "DOWNLOAD_MANAGER.DOWNLOADED", fallback: "Downloaded") } public enum Error { + /// Authorization failed. + public static let authorizationFailed = CoreLocalization.tr("Localizable", "ERROR.AUTHORIZATION_FAILED", fallback: "Authorization failed.") /// Invalid credentials public static let invalidCredentials = CoreLocalization.tr("Localizable", "ERROR.INVALID_CREDENTIALS", fallback: "Invalid credentials") /// No cached data for offline mode @@ -123,6 +153,64 @@ public enum CoreLocalization { /// Search public static let search = CoreLocalization.tr("Localizable", "PICKER.SEARCH", fallback: "Search") } + public enum Review { + /// What could have been better? + public static let better = CoreLocalization.tr("Localizable", "REVIEW.BETTER", fallback: "What could have been better?") + /// We’re sorry to hear your learning experience has had some issues. We appreciate all feedback. + public static let feedbackDescription = CoreLocalization.tr("Localizable", "REVIEW.FEEDBACK_DESCRIPTION", fallback: "We’re sorry to hear your learning experience has had some issues. We appreciate all feedback.") + /// Leave Us Feedback + public static let feedbackTitle = CoreLocalization.tr("Localizable", "REVIEW.FEEDBACK_TITLE", fallback: "Leave Us Feedback") + /// Not now + public static let notNow = CoreLocalization.tr("Localizable", "REVIEW.NOT_NOW", fallback: "Not now") + /// We received your feedback and will use it to help improve your learning experience going forward. Thank you for sharing! + public static let thanksForFeedbackDescription = CoreLocalization.tr("Localizable", "REVIEW.THANKS_FOR_FEEDBACK_DESCRIPTION", fallback: "We received your feedback and will use it to help improve your learning experience going forward. Thank you for sharing!") + /// Thank You + public static let thanksForFeedbackTitle = CoreLocalization.tr("Localizable", "REVIEW.THANKS_FOR_FEEDBACK_TITLE", fallback: "Thank You") + /// Thank you for sharing your feedback with us. Would you like to share your review of this app with other users on the app store? + public static let thanksForVoteDescription = CoreLocalization.tr("Localizable", "REVIEW.THANKS_FOR_VOTE_DESCRIPTION", fallback: "Thank you for sharing your feedback with us. Would you like to share your review of this app with other users on the app store?") + /// Thank You + public static let thanksForVoteTitle = CoreLocalization.tr("Localizable", "REVIEW.THANKS_FOR_VOTE_TITLE", fallback: "Thank You") + /// Your feedback matters to us. Would you take a moment to rate the app by tapping a star below? Thanks for your support! + public static let voteDescription = CoreLocalization.tr("Localizable", "REVIEW.VOTE_DESCRIPTION", fallback: "Your feedback matters to us. Would you take a moment to rate the app by tapping a star below? Thanks for your support!") + /// Enjoying Open edX? + public static let voteTitle = CoreLocalization.tr("Localizable", "REVIEW.VOTE_TITLE", fallback: "Enjoying Open edX?") + public enum Button { + /// Rate Us + public static let rateUs = CoreLocalization.tr("Localizable", "REVIEW.BUTTON.RATE_US", fallback: "Rate Us") + /// Share Feedback + public static let shareFeedback = CoreLocalization.tr("Localizable", "REVIEW.BUTTON.SHARE_FEEDBACK", fallback: "Share Feedback") + /// Submit + public static let submit = CoreLocalization.tr("Localizable", "REVIEW.BUTTON.SUBMIT", fallback: "Submit") + } + public enum Email { + /// Select email client: + public static let title = CoreLocalization.tr("Localizable", "REVIEW.EMAIL.TITLE", fallback: "Select email client:") + } + } + public enum Settings { + /// Lower data usage + public static let downloadQuality360Description = CoreLocalization.tr("Localizable", "SETTINGS.DOWNLOAD_QUALITY_360_DESCRIPTION", fallback: "Lower data usage") + /// 360p + public static let downloadQuality360Title = CoreLocalization.tr("Localizable", "SETTINGS.DOWNLOAD_QUALITY_360_TITLE", fallback: "360p") + /// 540p + public static let downloadQuality540Title = CoreLocalization.tr("Localizable", "SETTINGS.DOWNLOAD_QUALITY_540_TITLE", fallback: "540p") + /// Best quality + public static let downloadQuality720Description = CoreLocalization.tr("Localizable", "SETTINGS.DOWNLOAD_QUALITY_720_DESCRIPTION", fallback: "Best quality") + /// 720p + public static let downloadQuality720Title = CoreLocalization.tr("Localizable", "SETTINGS.DOWNLOAD_QUALITY_720_TITLE", fallback: "720p") + /// Recommended + public static let downloadQualityAutoDescription = CoreLocalization.tr("Localizable", "SETTINGS.DOWNLOAD_QUALITY_AUTO_DESCRIPTION", fallback: "Recommended") + /// Auto + public static let downloadQualityAutoTitle = CoreLocalization.tr("Localizable", "SETTINGS.DOWNLOAD_QUALITY_AUTO_TITLE", fallback: "Auto") + /// Video download quality + public static let videoDownloadQualityTitle = CoreLocalization.tr("Localizable", "SETTINGS.VIDEO_DOWNLOAD_QUALITY_TITLE", fallback: "Video download quality") + } + public enum SignIn { + /// Sign in + public static let logInBtn = CoreLocalization.tr("Localizable", "SIGN_IN.LOG_IN_BTN", fallback: "Sign in") + /// Register + public static let registerBtn = CoreLocalization.tr("Localizable", "SIGN_IN.REGISTER_BTN", fallback: "Register") + } public enum View { public enum Snackbar { /// Try Again @@ -133,6 +221,8 @@ public enum CoreLocalization { public enum Alert { /// Cancel public static let cancel = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.CANCEL", fallback: "Cancel") + /// Continue + public static let `continue` = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.CONTINUE", fallback: "Continue") /// Ok public static let ok = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.OK", fallback: "Ok") } diff --git a/Core/Core/Theme.swift b/Core/Core/Theme.swift deleted file mode 100644 index b63ad99f9..000000000 --- a/Core/Core/Theme.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// Theme.swift -// Core -// -// Created by Vladimir Chekyrta on 13.09.2022. -// - -import Foundation -import SwiftUI - -public struct Theme { - - public struct Colors { - public private(set) static var accentColor = CoreAssets.accentColor.swiftUIColor - public private(set) static var alert = CoreAssets.alert.swiftUIColor - public private(set) static var avatarStroke = CoreAssets.avatarStroke.swiftUIColor - public private(set) static var background = CoreAssets.background.swiftUIColor - public private(set) static var backgroundStroke = CoreAssets.backgroundStroke.swiftUIColor - public private(set) static var cardViewBackground = CoreAssets.cardViewBackground.swiftUIColor - public private(set) static var cardViewStroke = CoreAssets.cardViewStroke.swiftUIColor - public private(set) static var certificateForeground = CoreAssets.certificateForeground.swiftUIColor - public private(set) static var commentCellBackground = CoreAssets.commentCellBackground.swiftUIColor - public private(set) static var shadowColor = CoreAssets.shadowColor.swiftUIColor - public private(set) static var snackbarErrorColor = CoreAssets.snackbarErrorColor.swiftUIColor - public private(set) static var snackbarErrorTextColor = CoreAssets.snackbarErrorTextColor.swiftUIColor - public private(set) static var snackbarInfoAlert = CoreAssets.snackbarInfoAlert.swiftUIColor - public private(set) static var styledButtonBackground = CoreAssets.styledButtonBackground.swiftUIColor - public private(set) static var styledButtonText = CoreAssets.styledButtonText.swiftUIColor - public private(set) static var textPrimary = CoreAssets.textPrimary.swiftUIColor - public private(set) static var textSecondary = CoreAssets.textSecondary.swiftUIColor - public private(set) static var textInputBackground = CoreAssets.textInputBackground.swiftUIColor - public private(set) static var textInputStroke = CoreAssets.textInputStroke.swiftUIColor - public private(set) static var textInputUnfocusedBackground = CoreAssets.textInputUnfocusedBackground.swiftUIColor - public private(set) static var textInputUnfocusedStroke = CoreAssets.textInputUnfocusedStroke.swiftUIColor - public private(set) static var warning = CoreAssets.warning.swiftUIColor - - public static func update( - accentColor: Color = CoreAssets.accentColor.swiftUIColor, - alert: Color = CoreAssets.alert.swiftUIColor, - avatarStroke: Color = CoreAssets.avatarStroke.swiftUIColor, - background: Color = CoreAssets.background.swiftUIColor, - backgroundStroke: Color = CoreAssets.backgroundStroke.swiftUIColor, - cardViewBackground: Color = CoreAssets.cardViewBackground.swiftUIColor, - cardViewStroke: Color = CoreAssets.cardViewStroke.swiftUIColor, - certificateForeground: Color = CoreAssets.certificateForeground.swiftUIColor, - commentCellBackground: Color = CoreAssets.commentCellBackground.swiftUIColor, - shadowColor: Color = CoreAssets.shadowColor.swiftUIColor, - snackbarErrorColor: Color = CoreAssets.snackbarErrorColor.swiftUIColor, - snackbarErrorTextColor: Color = CoreAssets.snackbarErrorTextColor.swiftUIColor, - snackbarInfoAlert: Color = CoreAssets.snackbarInfoAlert.swiftUIColor, - styledButtonBackground: Color = CoreAssets.styledButtonBackground.swiftUIColor, - styledButtonText: Color = CoreAssets.styledButtonText.swiftUIColor, - textPrimary: Color = CoreAssets.textPrimary.swiftUIColor, - textSecondary: Color = CoreAssets.textSecondary.swiftUIColor, - textInputBackground: Color = CoreAssets.textInputBackground.swiftUIColor, - textInputStroke: Color = CoreAssets.textInputStroke.swiftUIColor, - textInputUnfocusedBackground: Color = CoreAssets.textInputUnfocusedBackground.swiftUIColor, - textInputUnfocusedStroke: Color = CoreAssets.textInputUnfocusedStroke.swiftUIColor, - warning: Color = CoreAssets.warning.swiftUIColor - ) { - self.accentColor = accentColor - self.alert = alert - self.avatarStroke = avatarStroke - self.background = background - self.backgroundStroke = backgroundStroke - self.cardViewBackground = cardViewBackground - self.cardViewStroke = cardViewStroke - self.certificateForeground = certificateForeground - self.commentCellBackground = commentCellBackground - self.shadowColor = shadowColor - self.snackbarErrorColor = snackbarErrorColor - self.snackbarErrorTextColor = snackbarErrorTextColor - self.snackbarInfoAlert = snackbarInfoAlert - self.styledButtonBackground = styledButtonBackground - self.styledButtonText = styledButtonText - self.textPrimary = textPrimary - self.textSecondary = textSecondary - self.textInputBackground = textInputBackground - self.textInputStroke = textInputStroke - self.textInputUnfocusedBackground = textInputUnfocusedBackground - self.textInputUnfocusedStroke = textInputUnfocusedStroke - self.warning = warning - } - } - - public struct Fonts { - - public static let displayLarge: Font = .custom("SFPro-Regular", size: 57) - public static let displayMedium: Font = .custom("SFPro-Regular", size: 45) - public static let displaySmall: Font = .custom("SFPro-Bold", size: 36) - - public static let headlineLarge: Font = .custom("SFPro-Regular", size: 32) - public static let headlineMedium: Font = .custom("SFPro-Regular", size: 28) - public static let headlineSmall: Font = .custom("SFPro-Regular", size: 24) - - public static let titleLarge: Font = .custom("SFPro-Bold", size: 22) - public static let titleMedium: Font = .custom("SFPro-Semibold", size: 18) - public static let titleSmall: Font = .custom("SFPro-Medium", size: 14) - - public static let bodyLarge: Font = .custom("SFPro-Regular", size: 16) - public static let bodyMedium: Font = .custom("SFPro-Regular", size: 14) - public static let bodySmall: Font = .custom("SFPro-Regular", size: 12) - - public static let labelLarge: Font = .custom("SFPro-Medium", size: 14) - public static let labelMedium: Font = .custom("SFPro-Regular", size: 12) - public static let labelSmall: Font = .custom("SFPro-Regular", size: 10) - } - - public struct Shapes { - public static let screenBackgroundRadius = 24.0 - public static let cardImageRadius = 10.0 - public static let textInputShape = RoundedRectangle(cornerRadius: 8) - public static let buttonShape = RoundedCorners(tl: 8, tr: 8, bl: 8, br: 8) - public static let unitButtonShape = RoundedCorners(tl: 21, tr: 21, bl: 21, br: 21) - public static let roundedScreenBackgroundShape = RoundedCorners( - tl: Theme.Shapes.screenBackgroundRadius, - tr: Theme.Shapes.screenBackgroundRadius, - bl: Theme.Shapes.screenBackgroundRadius, - br: Theme.Shapes.screenBackgroundRadius - ) - public static let roundedScreenBackgroundShapeCroppedBottom = RoundedCorners( - tl: Theme.Shapes.screenBackgroundRadius, - tr: Theme.Shapes.screenBackgroundRadius - ) - public static let cardShape = RoundedCorners(tl: 12, tr: 12, bl: 12, br: 12) - } - - public struct Timeout { - public static let snackbarMessageShortTimeout: TimeInterval = 3 - public static let snackbarMessageLongTimeout: TimeInterval = 5 - } - -} - -public extension Theme.Fonts { - // swiftlint:disable type_name - class __ {} - static func registerFonts() { - guard let url = Bundle(for: __.self).url(forResource: "SF-Pro", withExtension: "ttf") else { return } - CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil) - } - // swiftlint:enable type_name -} - -extension View { - public func loadFonts() -> some View { - Theme.Fonts.registerFonts() - return self - } -} diff --git a/Core/Core/View/Base/AlertView.swift b/Core/Core/View/Base/AlertView.swift index f230eba9b..ad501bdb9 100644 --- a/Core/Core/View/Base/AlertView.swift +++ b/Core/Core/View/Base/AlertView.swift @@ -6,9 +6,10 @@ // import SwiftUI +import Theme public enum AlertViewType: Equatable { - case `default`(positiveAction: String) + case `default`(positiveAction: String, image: SwiftUI.Image?) case action(String, SwiftUI.Image) case logOut case leaveProfile @@ -16,7 +17,7 @@ public enum AlertViewType: Equatable { var contentPadding: CGFloat { switch self { case .`default`: - return 5 + return 16 case .action, .logOut, .leaveProfile: return 36 } @@ -33,6 +34,8 @@ public struct AlertView: View { private var nextSectionTapped: (() -> Void) = {} private let type: AlertViewType + @Environment(\.isHorizontal) private var isHorizontal + public init( alertTitle: String, alertMessage: String, @@ -68,76 +71,126 @@ public struct AlertView: View { } public var body: some View { - GeometryReader { reader in - ZStack(alignment: .center) { - Color.black.opacity(0.5) - .onTapGesture { - onCloseTapped() - } - VStack(alignment: .center, spacing: 20) { - if case let .action(_, image) = type { - image.padding(.top, 48) - } + ZStack(alignment: .center) { + Color.black.opacity(0.5) + .onTapGesture { + onCloseTapped() + } + ZStack(alignment: .topTrailing) { + adaptiveStack(spacing: isHorizontal ? 10 : 20, isHorizontal: (type == .leaveProfile && isHorizontal)) { if type == .logOut { - CoreAssets.logOut.swiftUIImage - .padding(.top, 54) + HStack { + Spacer(minLength: 100) + CoreAssets.logOut.swiftUIImage + .padding(.top, isHorizontal ? 20 : 54) + Spacer(minLength: 100) + } Text(alertMessage) .font(Theme.Fonts.titleLarge) - .padding(.vertical, 40) + .padding(.vertical, isHorizontal ? 6 : 40) .multilineTextAlignment(.center) .padding(.horizontal, 40) .frame(maxWidth: 250) } else if type == .leaveProfile { - CoreAssets.leaveProfile.swiftUIImage - .padding(.top, 54) - Text(alertTitle) - .font(Theme.Fonts.titleLarge) - .padding(.horizontal, 40) - Text(alertMessage) - .font(Theme.Fonts.bodyMedium) - .multilineTextAlignment(.center) - .padding(.horizontal, 40) + VStack(spacing: 20) { + CoreAssets.leaveProfile.swiftUIImage + .padding(.top, isHorizontal ? 20 : 54) + Text(alertTitle) + .font(Theme.Fonts.titleLarge) + .padding(.horizontal, 40) + Text(alertMessage) + .font(Theme.Fonts.bodyMedium) + .multilineTextAlignment(.center) + .padding(.horizontal, 40) + + }.padding(.bottom, 20) } else { - Text(alertTitle) - .font(Theme.Fonts.titleLarge) - .padding(.horizontal, 40) - Text(alertMessage) - .font(Theme.Fonts.bodyMedium) - .multilineTextAlignment(.center) - .padding(.horizontal, 40) - .frame(maxWidth: 250) + HStack { + VStack(alignment: .center, spacing: 10) { + if case let .action(_, image) = type { + image.padding(.top, 48) + } + if case let .default(_, image) = type { + image.flatMap { $0.padding(.top, 48) } + } + Text(alertTitle) + .font(Theme.Fonts.titleLarge) + .padding(.horizontal, 40) + .padding(.top, 10) + Text(alertMessage) + .font(Theme.Fonts.bodyMedium) + .multilineTextAlignment(.center) + .padding(.horizontal, 40) + .frame(maxWidth: 250) + } + if isHorizontal { + if case let .action(action, _) = type { + VStack(spacing: 20) { + if nextSectionName != nil { + UnitButtonView(type: .nextSection, action: { nextSectionTapped() }) + .frame(maxWidth: 215) + } + UnitButtonView(type: .custom(action), + bgColor: .clear, + action: { okTapped() }) + .frame(maxWidth: 215) + + if let nextSectionName { + Group { + Text(CoreLocalization.Courseware.nextSectionDescriptionFirst) + + Text(nextSectionName) + + Text(CoreLocalization.Courseware.nextSectionDescriptionLast) + }.frame(maxWidth: 215) + .padding(.horizontal, 40) + .multilineTextAlignment(.center) + .font(Theme.Fonts.labelSmall) + .foregroundColor(Theme.Colors.textSecondary) + } + }.padding(.top, 70) + .padding(.trailing, 20) + } + } + } } HStack { switch type { - case let .`default`(positiveAction): - StyledButton(positiveAction, action: { okTapped() }) - .frame(maxWidth: 135) - StyledButton(CoreLocalization.Alert.cancel, action: { onCloseTapped() }) - .frame(maxWidth: 135) - .saturation(0) + case let .`default`(positiveAction, _): + HStack { + StyledButton(positiveAction, action: { okTapped() }) + .frame(maxWidth: 135) + StyledButton(CoreLocalization.Alert.cancel, action: { onCloseTapped() }) + .frame(maxWidth: 135) + .saturation(0) + } + .padding(.leading, 10) + .padding(.trailing, 10) + .padding(.bottom, 10) case let .action(action, _): - VStack(spacing: 20) { - if nextSectionName != nil { - UnitButtonView(type: .nextSection, action: { nextSectionTapped() }) - .frame(maxWidth: 215) - } - UnitButtonView(type: .custom(action), - bgColor: .clear, - action: { okTapped() }) + if !isHorizontal { + VStack(spacing: 20) { + if nextSectionName != nil { + UnitButtonView(type: .nextSection, action: { nextSectionTapped() }) + .frame(maxWidth: 215) + } + UnitButtonView(type: .custom(action), + bgColor: .clear, + action: { okTapped() }) .frame(maxWidth: 215) - - if let nextSectionName { - Group { - Text(CoreLocalization.Courseware.nextSectionDescriptionFirst) + - Text(nextSectionName) + - Text(CoreLocalization.Courseware.nextSectionDescriptionLast) - }.frame(maxWidth: 215) - .padding(.horizontal, 40) - .multilineTextAlignment(.center) - .font(Theme.Fonts.labelSmall) - .foregroundColor(Theme.Colors.textSecondary) + + if let nextSectionName { + Group { + Text(CoreLocalization.Courseware.nextSectionDescriptionFirst) + + Text(nextSectionName) + + Text(CoreLocalization.Courseware.nextSectionDescriptionLast) + }.frame(maxWidth: 215) + .padding(.horizontal, 40) + .multilineTextAlignment(.center) + .font(Theme.Fonts.labelSmall) + .foregroundColor(Theme.Colors.textSecondary) + } } - + } else { + EmptyView() } case .logOut: Button(action: { @@ -199,7 +252,7 @@ public struct AlertView: View { .foregroundColor(.clear) ) .frame(maxWidth: 215) - .padding(.bottom, 24) + .padding(.bottom, isHorizontal ? 10 : 24) Button(action: { onCloseTapped() }, label: { @@ -227,33 +280,37 @@ public struct AlertView: View { .foregroundColor(Theme.Colors.textPrimary) ) .frame(maxWidth: 215) - } + }.padding(.trailing, isHorizontal ? 20 : 0) } } - .padding(.top, 5) - .padding(.bottom, type.contentPadding) + .padding(.top, 16) + .padding(.bottom, isHorizontal ? 16 : type.contentPadding) } - .background( - Theme.Shapes.cardShape - .fill(Theme.Colors.cardViewBackground) - .shadow(radius: 24) - .frame(width: reader.size.width < 420 - ? reader.size.width - 80 - : 360) - ) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) - .foregroundColor(Theme.Colors.backgroundStroke) - .frame(width: reader.size.width < 420 - ? reader.size.width - 80 - : 360) - ) - .padding() - } - - .ignoresSafeArea() + Button(action: { + onCloseTapped() + }, label: { + Image(systemName: "xmark") + .padding(.trailing, 40) + .padding(.top, 24) + }) + + }.frame(maxWidth: type == .logOut ? 390 : nil) + .background( + Theme.Shapes.cardShape + .fill(Theme.Colors.cardViewBackground) + .shadow(radius: 24) + .fixedSize(horizontal: false, vertical: false) + ) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) + .foregroundColor(Theme.Colors.backgroundStroke) + .fixedSize(horizontal: false, vertical: false) + ) + .frame(maxWidth: isHorizontal ? nil : 390) + .padding(40) } + .ignoresSafeArea() } } @@ -261,9 +318,9 @@ public struct AlertView: View { struct AlertView_Previews: PreviewProvider { static var previews: some View { AlertView( - alertTitle: "Warning", - alertMessage: "Something goes wrong. Do you want to exterminate your phone, right now", - nextSectionName: "Ahmad tea is a power", + alertTitle: "Congratulations!", + alertMessage: "You've passed the course", + nextSectionName: "Continue", mainAction: "Back to outline", image: CoreAssets.goodWork.swiftUIImage, onCloseTapped: {}, @@ -272,6 +329,23 @@ struct AlertView_Previews: PreviewProvider { ) .previewLayout(.sizeThatFits) .background(Color.gray) + + AlertView(alertTitle: "Comfirm log out", + alertMessage: "Are you sure you want to log out?", + positiveAction: "Yes", + onCloseTapped: {}, + okTapped: {}, + type: .logOut) + + AlertView(alertTitle: "Leave profile?", + alertMessage: "Changes you have made not be saved.", + positiveAction: "Yes", + onCloseTapped: {}, + okTapped: {}, + type: .leaveProfile) + + .previewLayout(.sizeThatFits) + .background(Color.gray) } } //swiftlint:enable all diff --git a/Core/Core/View/Base/AppReview/AppReviewView.swift b/Core/Core/View/Base/AppReview/AppReviewView.swift new file mode 100644 index 000000000..e7fedcf1b --- /dev/null +++ b/Core/Core/View/Base/AppReview/AppReviewView.swift @@ -0,0 +1,145 @@ +// +// AppReviewView.swift +// Core +// +// Created by  Stepanok Ivan on 26.10.2023. +// + +import SwiftUI +import StoreKit +import Theme + +public struct AppReviewView: View { + + @ObservedObject private var viewModel: AppReviewViewModel + + @Environment (\.isHorizontal) private var isHorizontal + @Environment (\.presentationMode) private var presentationMode + + public init(viewModel: AppReviewViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + ZStack { + Color.black.opacity(0.5) + .ignoresSafeArea() + .onTapGesture { + presentationMode.wrappedValue.dismiss() + } + if viewModel.showSelectMailClientView { + SelectMailClientView(clients: viewModel.clients, onMailTapped: { client in + viewModel.openMailClient(client) + }) + } else { + VStack(spacing: 20) { + if viewModel.state == .thanksForFeedback || viewModel.state == .thanksForVote { + CoreAssets.favorite.swiftUIImage + .resizable() + .frame(width: isHorizontal ? 50 : 100, + height: isHorizontal ? 50 : 100) + .foregroundColor(Theme.Colors.accentColor) + .onForeground { + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + self.presentationMode.wrappedValue.dismiss() + } + } + } + Text(viewModel.state.title) + .font(Theme.Fonts.titleMedium) + Text(viewModel.state.description) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.avatarStroke) + .multilineTextAlignment(.center) + switch viewModel.state { + case .vote: + StarRatingView(rating: $viewModel.rating) + + HStack(spacing: 28) { + Text(CoreLocalization.Review.notNow) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.accentColor) + .onTapGesture { presentationMode.wrappedValue.dismiss() } + + AppReviewButton(type: .submit, action: { + viewModel.reviewAction() + }, isActive: .constant(viewModel.rating != 0)) + } + + case .feedback: + TextEditor(text: $viewModel.feedback) + .padding(.horizontal, 12) + .padding(.vertical, 4) + .hideScrollContentBackground() + .background( + Theme.Shapes.textInputShape + .fill(Theme.Colors.commentCellBackground) + ) + .overlay( + ZStack(alignment: .topLeading) { + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill( + Theme.Colors.textInputStroke + ) + if viewModel.feedback.isEmpty { + Text(CoreLocalization.Review.better) + .font(Theme.Fonts.bodyMedium) + .foregroundColor(Theme.Colors.textSecondary) + .padding(16) + } + } + ) + .frame(height: viewModel.showReview ? (isHorizontal ? 80 : 162) : 0) + .opacity(viewModel.showReview ? 1 : 0) + + HStack(spacing: 28) { + Text(CoreLocalization.Review.notNow) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.accentColor) + .onTapGesture { presentationMode.wrappedValue.dismiss() } + + AppReviewButton(type: .shareFeedback, action: { + viewModel.writeFeedbackToMail() + }, isActive: .constant(viewModel.feedback.count >= 3)) + } + + case .thanksForVote, .thanksForFeedback: + HStack(spacing: 28) { + Text(CoreLocalization.Review.notNow) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.accentColor) + .onTapGesture { presentationMode.wrappedValue.dismiss() } + + AppReviewButton(type: .rateUs, action: { + presentationMode.wrappedValue.dismiss() + SKStoreReviewController.requestReviewInCurrentScene() + viewModel.storage.lastReviewDate = Date() + }, isActive: .constant(true)) + } + } + + }.onTapGesture { + UIApplication.shared.sendAction( + #selector(UIResponder.resignFirstResponder), + to: nil, from: nil, for: nil + ) + } + .padding(isHorizontal ? 20 : 40) + .background(Theme.Colors.background) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .frame(maxWidth: 400) + .padding(isHorizontal ? 14 : 24) + .shadow(color: Color.black.opacity(0.4), radius: 12, x: 0, y: 0) + } + } + } +} + +#if DEBUG +struct AppReviewView_Previews: PreviewProvider { + static var previews: some View { + AppReviewView(viewModel: AppReviewViewModel(config: ConfigMock(), storage: CoreStorageMock())) + } +} +#endif diff --git a/Core/Core/View/Base/AppReview/AppReviewViewModel.swift b/Core/Core/View/Base/AppReview/AppReviewViewModel.swift new file mode 100644 index 000000000..a14f78345 --- /dev/null +++ b/Core/Core/View/Base/AppReview/AppReviewViewModel.swift @@ -0,0 +1,155 @@ +// +// AppReviewViewModel.swift +// Core +// +// Created by  Stepanok Ivan on 27.10.2023. +// + +import SwiftUI +import StoreKit + +public class AppReviewViewModel: ObservableObject { + + enum ReviewState { + case vote + case feedback + case thanksForVote + case thanksForFeedback + + var title: String { + switch self { + case .vote: + CoreLocalization.Review.voteTitle + case .feedback: + CoreLocalization.Review.feedbackTitle + case .thanksForVote, .thanksForFeedback: + CoreLocalization.Review.thanksForVoteTitle + } + } + + var description: String { + switch self { + case .vote: + CoreLocalization.Review.voteDescription + case .feedback: + CoreLocalization.Review.feedbackDescription + case .thanksForVote: + CoreLocalization.Review.thanksForVoteDescription + case .thanksForFeedback: + CoreLocalization.Review.thanksForFeedbackDescription + } + } + } + + @Published var state: ReviewState = .vote + @Published var rating: Int = 0 + @Published var showReview: Bool = false + @Published var showSelectMailClientView: Bool = false + @Published var feedback: String = "" + @Published var clients: [ThirdPartyMailClient] = [] + let allClients = ThirdPartyMailClient.clients + + private let config: ConfigProtocol + var storage: CoreStorage + + public init(config: ConfigProtocol, storage: CoreStorage) { + self.config = config + self.storage = storage + } + + public func shouldShowRatingView() -> Bool { + guard let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { + return false + } + guard let lastShownVersion = storage.reviewLastShownVersion else { + storage.reviewLastShownVersion = currentVersion + + if let lastReviewDate = storage.lastReviewDate { + return hasPassedFourMonths(from: lastReviewDate) + } else { + return true + } + } + return isNewerVersion(currentVersion: currentVersion, lastVersion: lastShownVersion) + } + + private func hasPassedFourMonths(from date: Date) -> Bool { + let currentDate = Date() + let calendar = Calendar.current + + if let futureDate = calendar.date(byAdding: .month, value: 4, to: date) { + return currentDate >= futureDate + } + + return false + } + + func reviewAction() { + withAnimation(Animation.easeIn(duration: 0.2)) { + if rating <= 3 { + state = .feedback + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + withAnimation(Animation.easeIn(duration: 0.1)) { + self.showReview = true + } + } + } else { + state = .thanksForVote + } + } + } + + func writeFeedbackToMail() { + self.clients = allClients.filter({ ThirdPartyMailer.isMailClientAvailable($0) }) + if !clients.isEmpty { + withAnimation(Animation.bouncy(duration: 0.2)) { + showSelectMailClientView = true + } + } else { + openMailClient(ThirdPartyMailClient.systemDefault) + } + } + + func openMailClient(_ with: ThirdPartyMailClient) { + + let osVersion = UIDevice.current.systemVersion + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + let deviceModel = UIDevice.current.model + let feedbackDetails = "\n\n OS version: \(osVersion)\nApp version: \(appVersion)\nDevice model: \(deviceModel)" + + let mailUrl = with.composeURL( + to: config.feedbackEmail, + subject: "Feedback", + body: feedback + feedbackDetails, + cc: nil, + bcc: nil + ) + UIApplication.shared.open(mailUrl) + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.showSelectMailClientView = false + self.state = .thanksForFeedback + } + } + + private func isNewerVersion(currentVersion: String, lastVersion: String) -> Bool { + // Split versions into components + let currentComponents = currentVersion.split(separator: ".").compactMap { Int($0) } + let lastComponents = lastVersion.split(separator: ".").compactMap { Int($0) } + + // Check that the number of components is the same + guard currentComponents.count == lastComponents.count else { + return false + } + + // Check the condition + if currentComponents[0] > lastComponents[0] + 1 { + return true // Greater by one major version + } else if currentComponents[0] == lastComponents[0] + 1 { + return true // Equal to the major version but greater by two minor versions + } else if currentComponents[1] > lastComponents[1] + 1 { + return true // Greater by two minor versions + } + + return false + } +} diff --git a/Core/Core/View/Base/AppReview/Elements/AppReviewButton.swift b/Core/Core/View/Base/AppReview/Elements/AppReviewButton.swift new file mode 100644 index 000000000..15225b7e5 --- /dev/null +++ b/Core/Core/View/Base/AppReview/Elements/AppReviewButton.swift @@ -0,0 +1,44 @@ +// +// AppReviewButton.swift +// Core +// +// Created by  Stepanok Ivan on 31.10.2023. +// + +import SwiftUI +import Theme + +struct AppReviewButton: View { + let type: ButtonType + let action: () -> Void + @Binding var isActive: Bool + + enum ButtonType { + case submit, shareFeedback, rateUs + } + + var body: some View { + Button(action: { + if isActive { action() } + }, label: { + Group { + HStack(spacing: 4) { + Text(type == .submit ? CoreLocalization.Review.Button.submit + : (type == .shareFeedback ? CoreLocalization.Review.Button.shareFeedback : CoreLocalization.Review.Button.rateUs )) + .foregroundColor(isActive ? Color.white : Color.black.opacity(0.6)) + .font(Theme.Fonts.labelLarge) + .padding(3) + + }.padding(.horizontal, 20) + .padding(.vertical, 9) + }.fixedSize() + .background(isActive + ? Theme.Colors.accentColor + : Theme.Colors.cardViewStroke) + .accessibilityElement(children: .ignore) + .accessibilityLabel(type == .submit ? CoreLocalization.Review.Button.submit + : (type == .shareFeedback ? CoreLocalization.Review.Button.shareFeedback : CoreLocalization.Review.Button.rateUs )) + .cornerRadius(8) + }) + } +} diff --git a/Core/Core/View/Base/AppReview/Elements/SelectMailClientView.swift b/Core/Core/View/Base/AppReview/Elements/SelectMailClientView.swift new file mode 100644 index 000000000..822a67186 --- /dev/null +++ b/Core/Core/View/Base/AppReview/Elements/SelectMailClientView.swift @@ -0,0 +1,89 @@ +// +// SelectMailClientView.swift +// Core +// +// Created by  Stepanok Ivan on 31.10.2023. +// + +import SwiftUI +import Theme + +struct SelectMailClientView: View { + + let clients: [ThirdPartyMailClient] + + var onMailTapped: (ThirdPartyMailClient) -> Void + + init(clients: [ThirdPartyMailClient], onMailTapped: @escaping (ThirdPartyMailClient) -> Void) { + self.clients = clients + self.onMailTapped = onMailTapped + } + + @State var isOpen: Bool = false + + var body: some View { + ZStack { + VStack { + Spacer() + VStack(alignment: .leading, spacing: 0) { + Text(CoreLocalization.Review.Email.title) + .font(Theme.Fonts.labelLarge) + .padding(.leading, 16) + .padding(.top, 8) + ScrollView(.horizontal) { + HStack { + Button(action: { + onMailTapped(.systemDefault) + }, label: { + Image(.defaultMail).resizable() + .frame(width: 50, height: 50) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(.leading, 14) + .shadow(color: .black.opacity(0.2), radius: 8) + }) + + ForEach(clients, id: \.name) { client in + Group { + Button(action: { + onMailTapped(client) + }, label: { + client.icon?.resizable() + }) + }.frame(width: 50, height: 50) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .shadow(color: .black.opacity(0.2), radius: 8) + .padding(.leading, 4) + .padding(.vertical, 16) + } + } + } + + }.background( Theme.Colors.background) + .offset(y: isOpen ? 0 : 200) + } + }.onAppear { + withAnimation(Animation.bouncy.delay(0.3)) { + isOpen = true + } + } + } +} + +struct SelectMailClientView_Previews: PreviewProvider { + static var previews: some View { + + let clients: [ThirdPartyMailClient] = [ + ThirdPartyMailClient(name: "googlegmail", icon: Image(.googlegmail), URLScheme: ""), + ThirdPartyMailClient(name: "readdle-spark", icon: Image(.readdleSpark), URLScheme: ""), + ThirdPartyMailClient(name: "airmail", icon: Image(.airmail), URLScheme: ""), + ThirdPartyMailClient(name: "ms-outlook", icon: Image(.msOutlook), URLScheme: ""), + ThirdPartyMailClient(name: "ymail", icon: Image(.ymail), URLScheme: ""), + ThirdPartyMailClient(name: "fastmail", icon: Image(.fastmail), URLScheme: ""), + ThirdPartyMailClient(name: "protonmail", icon: Image(.proton), URLScheme: "") + ] + + SelectMailClientView(clients: clients, onMailTapped: { _ in + + }) + } +} diff --git a/Core/Core/View/Base/AppReview/Elements/StarRatingView.swift b/Core/Core/View/Base/AppReview/Elements/StarRatingView.swift new file mode 100644 index 000000000..4f5116f31 --- /dev/null +++ b/Core/Core/View/Base/AppReview/Elements/StarRatingView.swift @@ -0,0 +1,35 @@ +// +// StarRatingView.swift +// Core +// +// Created by  Stepanok Ivan on 31.10.2023. +// + +import SwiftUI +import Theme + +struct StarRatingView: View { + @Binding var rating: Int + + var body: some View { + HStack { + ForEach(1 ..< 6) { index in + Group { + if index <= rating { + CoreAssets.star.swiftUIImage + .resizable() + .frame(width: 48, height: 48) + } else { + CoreAssets.starOutline.swiftUIImage + .resizable() + .frame(width: 48, height: 48) + .foregroundColor(Theme.Colors.textPrimary) + } + } + .onTapGesture { + self.rating = index + } + } + } + } +} diff --git a/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift b/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift new file mode 100644 index 000000000..76d48270e --- /dev/null +++ b/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift @@ -0,0 +1,147 @@ +// +// ThirdPartyMailClient.swift +// +// Copyright (c) 2016-2022 Vincent Tourraine (https://www.vtourraine.net) +// +// Licensed under MIT License + +import SwiftUI + +/// A third-party mail client, offering a custom URL scheme. +public struct ThirdPartyMailClient { + + /// The name of the mail client. + public let name: String + + /// The custom URL scheme of the mail client. + public let URLScheme: String + + /// The URL “root” (after the URL scheme and the colon). + let URLRoot: String? + + /// The URL query items key for the recipient. + let URLRecipientKey: String? + + /// The URL query items key for the subject, or `nil` if this client doesn’t support setting the subject. + let URLSubjectKey: String? + + /// The URL query items key for the message body, or `nil` if this client doesn’t support setting the message body. + let URLBodyKey: String? + + let icon: Image? + + public init(name: String, icon: Image?, URLScheme: String, URLRoot: String? = nil, URLRecipientKey: String? = nil, URLSubjectKey: String? = "subject", URLBodyKey: String? = "body") { + self.name = name + self.icon = icon + self.URLScheme = URLScheme + self.URLRoot = URLRoot + self.URLRecipientKey = URLRecipientKey + self.URLSubjectKey = URLSubjectKey + self.URLBodyKey = URLBodyKey + } + + /// Returns the open URL for the mail client, based on its custom URL scheme. + /// - Returns: A `URL` opening the mail client. + public func openURL() -> URL { + var components = URLComponents() + components.scheme = URLScheme + return components.url! + } + + /// Returns the compose URL for the mail client, based on its custom URL scheme. + /// - Parameters: + /// - recipient: The recipient for the email message (optional). + /// - subject: The subject for the email message (optional). + /// - body: The body for the email message (optional). + /// - cc: The carbon copy recipient for the email message (optional). + /// - bcc: The blind carbon copy recipient for the email message (optional). + /// - Returns: A `URL` opening the mail client for the given parameters. + public func composeURL(to recipient: String? = nil, subject: String? = nil, body: String? = nil, cc: String? = nil, bcc: String? = nil) -> URL { + var components = URLComponents(string: "\(URLScheme):\(URLRoot ?? "")") + components?.scheme = self.URLScheme + + if URLRecipientKey == nil { + if let recipient = recipient { + components = URLComponents(string: "\(URLScheme):\(URLRoot ?? "")\(recipient)") + } + } + + var queryItems: [URLQueryItem] = [] + + if let recipient = recipient, let URLRecipientKey = URLRecipientKey { + if URLRecipientKey == ":" { + // Special format for ProtonMail + // https://github.com/vtourraine/ThirdPartyMailer/issues/32 + components = URLComponents(string: "\(URLScheme):\(URLRoot ?? ""):\(recipient)") + } + else { + queryItems.append(URLQueryItem(name: URLRecipientKey, value: recipient)) + } + } + + if let subject = subject, let URLSubjectKey = URLSubjectKey { + queryItems.append(URLQueryItem(name: URLSubjectKey, value: subject)) + } + + if let body = body, let URLBodyKey = URLBodyKey { + queryItems.append(URLQueryItem(name: URLBodyKey, value: body)) + } + + if let cc = cc { + queryItems.append(URLQueryItem(name: "cc", value: cc)) + } + + if let bcc = bcc { + queryItems.append(URLQueryItem(name: "bcc", value: bcc)) + } + + if queryItems.isEmpty == false { + components?.queryItems = queryItems + } + + return components!.url! + } +} + +public extension ThirdPartyMailClient { + static var systemDefault: ThirdPartyMailClient { + get { + // mailto: + return ThirdPartyMailClient(name: "System Default", icon: Image(.defaultMail), URLScheme: "mailto") + } + } + + /// Returns an array of predefined mail clients. + static var clients: [ThirdPartyMailClient] { + get { + return [ + // sparrow:[to]?subject=[subject]&body=[body] + ThirdPartyMailClient(name: "Sparrow", icon: nil, URLScheme: "sparrow"), + + // googlegmail:///co?to=[to]&subject=[subject]&body=[body] + ThirdPartyMailClient(name: "Gmail", icon: Image(.googlegmail), URLScheme: "googlegmail", URLRoot: "///co", URLRecipientKey: "to"), + + // x-dispatch:///compose?to=[to]&subject=[subject]&body=[body] + ThirdPartyMailClient(name: "Dispatch", icon: nil, URLScheme: "x-dispatch", URLRoot: "///compose", URLRecipientKey: "to"), + + // readdle-spark://compose?subject=[subject]&body=[body]&recipient=[recipient] + ThirdPartyMailClient(name: "Spark", icon: Image(.readdleSpark), URLScheme: "readdle-spark", URLRoot: "//compose", URLRecipientKey: "recipient"), + + // airmail://compose?subject=[subject]&from=[from]&to=[to]&cc=[cc]&bcc=[bcc]&plainBody=[plainBody]&htmlBody=[htmlBody] + ThirdPartyMailClient(name: "Airmail", icon: Image(.airmail), URLScheme: "airmail", URLRoot: "//compose", URLRecipientKey: "to", URLBodyKey: "plainBody"), + + // ms-outlook://compose?subject=[subject]&body=[body]&to=[to] + ThirdPartyMailClient(name: "Microsoft Outlook", icon: Image(.msOutlook), URLScheme: "ms-outlook", URLRoot: "//compose", URLRecipientKey: "to"), + + // ymail://mail/compose?subject=[subject]&body=[body]&to=[to] + ThirdPartyMailClient(name: "Yahoo Mail", icon: Image(.ymail), URLScheme: "ymail", URLRoot: "//mail/compose", URLRecipientKey: "to"), + + // fastmail://mail/compose?subject=[subject]&body=[body]&to=[to] + ThirdPartyMailClient(name: "Fastmail", icon: Image(.fastmail), URLScheme: "fastmail", URLRoot: "//mail/compose", URLRecipientKey: "to"), + + // protonmail://mailto:foobar@foobar.org?subject=SubjectTitleOfEMail&body=MessageBodyFooBar + ThirdPartyMailClient(name: "ProtonMail", icon: Image(.proton), URLScheme: "protonmail", URLRoot: "//mailto", URLRecipientKey: ":") + ] + } + } +} diff --git a/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailer.swift b/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailer.swift new file mode 100644 index 000000000..ded79fcda --- /dev/null +++ b/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailer.swift @@ -0,0 +1,53 @@ +// +// ThirdPartyMailClient.swift +// +// Copyright (c) 2016-2022 Vincent Tourraine (https://www.vtourraine.net) +// +// Licensed under MIT License + +import UIKit + +/// Tests third party mail clients availability, and opens third party mail clients in compose mode. +@available(iOSApplicationExtension, unavailable) +open class ThirdPartyMailer { + + /// Tests the availability of a third-party mail client. + /// - Parameters: + /// - client: The third-party client to test. + /// - Returns: `true` if the application can open the client; otherwise, `false`. + open class func isMailClientAvailable(_ client: ThirdPartyMailClient) -> Bool { + var components = URLComponents() + components.scheme = client.URLScheme + + guard let URL = components.url + else { return false } + + let application = UIApplication.shared + return application.canOpenURL(URL) + } + + /// Opens a third-party mail client. + /// - Parameters: + /// - client: The third-party client to open. + /// - completion: The block to execute with the results (optional, default value is `nil`). + open class func open(_ client: ThirdPartyMailClient = .systemDefault, completionHandler completion: ((Bool) -> Void)? = nil) { + let url = client.openURL() + let application = UIApplication.shared + application.open(url, options: [:], completionHandler: completion) + } + + /// Opens a third-party mail client in compose mode. + /// - Parameters: + /// - client: The third-party client to open. + /// - recipient: The email address of the recipient (optional, default value is `nil`). + /// - subject: The email subject (optional, default value is `nil`). + /// - body: The email body (optional, default value is `nil`). + /// - cc: The email address of the recipient carbon copy (optional, default value is `nil`). + /// - bcc: The email address of the recipient blind carbon copy (optional, default value is `nil`). + /// - completion: The block to execute with the results (optional, default value is `nil`). + open class func openCompose(_ client: ThirdPartyMailClient = .systemDefault, recipient: String? = nil, subject: String? = nil, body: String? = nil, cc: String? = nil, bcc: String? = nil, with application: UIApplication = .shared, completionHandler completion: ((Bool) -> Void)? = nil) { + let url = client.composeURL(to: recipient, subject: subject, body: body, cc: cc, bcc: bcc) + let application = UIApplication.shared + application.open(url, options: [:], completionHandler: completion) + } +} diff --git a/Discussion/Discussion/Presentation/CheckBoxView.swift b/Core/Core/View/Base/CheckBoxView.swift similarity index 55% rename from Discussion/Discussion/Presentation/CheckBoxView.swift rename to Core/Core/View/Base/CheckBoxView.swift index 61af59732..29253290a 100644 --- a/Discussion/Discussion/Presentation/CheckBoxView.swift +++ b/Core/Core/View/Base/CheckBoxView.swift @@ -1,25 +1,35 @@ // // CheckBoxView.swift -// Discussion +// Core // -// Created by  Stepanok Ivan on 12.11.2022. +// Created by Stepanok Ivan on 12.11.2022. // import SwiftUI -import Core +import Theme public struct CheckBoxView: View { - @Binding var checked: Bool - var text: String + + @Binding private var checked: Bool + private var text: String + private var font: Font + + public init(checked: Binding, text: String, font: Font = Theme.Fonts.labelLarge) { + self._checked = checked + self.text = text + self.font = font + } public var body: some View { HStack(spacing: 10) { - Image(systemName: checked ? "checkmark.square.fill" : "square") - .foregroundColor(checked - ? Theme.Colors.accentColor - : Theme.Colors.textPrimary) + Image( + systemName: checked ? "checkmark.square.fill" : "square" + ) + .foregroundColor( + checked ? Theme.Colors.accentColor : Theme.Colors.textPrimary + ) Text(text) - .font(Theme.Fonts.labelLarge) + .font(font) } .onTapGesture { withAnimation(.linear(duration: 0.1)) { @@ -34,7 +44,7 @@ struct CheckBoxView_Previews: PreviewProvider { CheckBoxView(checked: .constant(false), text: "Check it") .preferredColorScheme(.light) .previewDisplayName("CheckBoxView Light") - + CheckBoxView(checked: .constant(true), text: "Check it") .preferredColorScheme(.dark) .previewDisplayName("CheckBoxView Dark") diff --git a/Core/Core/View/Base/CourseButton.swift b/Core/Core/View/Base/CourseButton.swift index 4d9a468d9..428bf2920 100644 --- a/Core/Core/View/Base/CourseButton.swift +++ b/Core/Core/View/Base/CourseButton.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import Theme public struct CourseButton: View { diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index ae21b2e7f..e8ce6ca01 100644 --- a/Core/Core/View/Base/CourseCellView.swift +++ b/Core/Core/View/Base/CourseCellView.swift @@ -7,6 +7,7 @@ import SwiftUI import Kingfisher +import Theme public enum CellType { case dashboard @@ -48,6 +49,7 @@ public struct CourseCellView: View { .cornerRadius(8) .clipShape(RoundedRectangle(cornerRadius: Theme.Shapes.cardImageRadius)) .padding(.leading, 3) + .accessibilityElement(children: .ignore) VStack(alignment: .leading) { Text(courseOrg) @@ -90,6 +92,8 @@ public struct CourseCellView: View { .background(Theme.Colors.background) .opacity(showView ? 1 : 0) .offset(y: showView ? 0 : 20) + .accessibilityElement(children: .ignore) + .accessibilityLabel(courseName + " " + (type == .dashboard ? (courseEnd == "" ? courseStart : courseEnd) : "")) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now()) { withAnimation(.easeInOut(duration: (index <= 5 ? 0.3 : 0.1)) @@ -98,6 +102,7 @@ public struct CourseCellView: View { } } } + VStack { if Int(index) != cellsCount { Divider() diff --git a/Core/Core/View/Base/CustomDisclosureGroup.swift b/Core/Core/View/Base/CustomDisclosureGroup.swift new file mode 100644 index 000000000..c4a023ed3 --- /dev/null +++ b/Core/Core/View/Base/CustomDisclosureGroup.swift @@ -0,0 +1,49 @@ +// +// CustomDisclosureGroup.swift +// Core +// +// Created by Eugene Yatsenko on 09.11.2023. +// + +import SwiftUI + +public struct CustomDisclosureGroup: View { + + @Binding var isExpanded: Bool + + private var onClick: () -> Void + private var animation: Animation? + private let header: Header + private let content: Content + + public init( + animation: Animation?, + isExpanded: Binding, + onClick: @escaping () -> Void, + header: (_ isExpanded: Bool) -> Header, + content: () -> Content + ) { + self.onClick = onClick + self._isExpanded = isExpanded + self.animation = animation + self.header = header(isExpanded.wrappedValue) + self.content = content() + } + + public var body: some View { + VStack(spacing: 0) { + Button { + withAnimation(animation) { + onClick() + } + } label: { + header + .contentShape(Rectangle()) + } + if isExpanded { + content + } + } + .clipped() + } +} diff --git a/Core/Core/View/Base/DownloadView.swift b/Core/Core/View/Base/DownloadView.swift index 9956cc947..806aee51b 100644 --- a/Core/Core/View/Base/DownloadView.swift +++ b/Core/Core/View/Base/DownloadView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Theme public enum DownloadViewState { case available diff --git a/Core/Core/View/Base/FlexibleKeyboardInputView.swift b/Core/Core/View/Base/FlexibleKeyboardInputView.swift index 97bc11401..a9625d117 100644 --- a/Core/Core/View/Base/FlexibleKeyboardInputView.swift +++ b/Core/Core/View/Base/FlexibleKeyboardInputView.swift @@ -6,11 +6,13 @@ // import SwiftUI +import Theme public struct FlexibleKeyboardInputView: View { @State private var commentText: String = "" @State private var commentSize: CGFloat = .init(64) + @Environment (\.isHorizontal) private var isHorizontal public var sendText: ((String) -> Void) private let hint: String @@ -24,79 +26,82 @@ public struct FlexibleKeyboardInputView: View { public var body: some View { VStack { - Spacer() - VStack(alignment: .leading) { - - ScrollView { - HStack(alignment: .top, spacing: 6) { - Text("\(commentText) ").foregroundColor(.clear).padding(8) - .lineLimit(3) - .frame(maxWidth: .infinity) - .background( - GeometryReader { reader in - Color.clear.preference( - key: ViewSizePreferenceKey.self, - value: reader.size - ) + VStack { + Spacer() + VStack(alignment: .leading) { + + ScrollView { + HStack(alignment: .top, spacing: 6) { + Text("\(commentText) ").foregroundColor(.clear).padding(8) + .lineLimit(3) + .frame(maxWidth: .infinity) + .background( + GeometryReader { reader in + Color.clear.preference( + key: ViewSizePreferenceKey.self, + value: reader.size + ) + } + ) + .onPreferenceChange(ViewSizePreferenceKey.self) { size in + commentSize = size.height } - ) - .onPreferenceChange(ViewSizePreferenceKey.self) { size in - commentSize = size.height - } - .overlay( - TextEditor(text: $commentText) - .padding(.horizontal, 8) - .foregroundColor(Theme.Colors.textPrimary) - .hideScrollContentBackground() - .frame(maxHeight: commentSize) - .background( - ZStack(alignment: .leading) { + .overlay( + TextEditor(text: $commentText) + .padding(.horizontal, 8) + .foregroundColor(Theme.Colors.textPrimary) + .hideScrollContentBackground() + .frame(maxHeight: commentSize) + .background( + ZStack(alignment: .leading) { + Theme.Shapes.textInputShape + .fill(Theme.Colors.textInputBackground) + Text(commentText.count == 0 ? hint : "") + .foregroundColor(Theme.Colors.textSecondary) + .font(Theme.Fonts.labelLarge) + .padding(.leading, 14) + } + ) + .overlay( Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) - Text(commentText.count == 0 ? hint : "") - .foregroundColor(Theme.Colors.textSecondary) - .font(Theme.Fonts.labelLarge) - .padding(.leading, 14) - } - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill( - Theme.Colors.textInputStroke - ) - ) - ).padding(8) - Button(action: { - if commentText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 { - sendText(commentText) - self.commentText = "" - } - }, label: { - VStack { - commentText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 - ? CoreAssets.send.swiftUIImage - : CoreAssets.sendDisabled.swiftUIImage - } - .frame(width: 36, height: 36) - .foregroundColor(.white) - }).padding(.top, 8) - } - } - .padding(.leading, 6) - .padding(.trailing, 14) - }.frame(maxWidth: .infinity, maxHeight: commentSize + 16) - .background( - Theme.Colors.commentCellBackground - .ignoresSafeArea() - ) - .overlay( - GeometryReader { proxy in - Rectangle() - .size(width: proxy.size.width, height: 1) - .foregroundColor(Theme.Colors.cardViewStroke) + .stroke(lineWidth: 1) + .fill( + Theme.Colors.textInputStroke + ) + ) + ).padding(8) + Button(action: { + if commentText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 { + sendText(commentText) + self.commentText = "" + } + }, label: { + VStack { + commentText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 + ? CoreAssets.send.swiftUIImage + : CoreAssets.sendDisabled.swiftUIImage + } + .frame(width: 36, height: 36) + .foregroundColor(Theme.Colors.white) + }).padding(.top, 8) + + }.padding(.horizontal, isHorizontal ? 50 : 16) + } - ) + .padding(.leading, 6) + .padding(.trailing, 14) + }.frame(maxWidth: .infinity, maxHeight: commentSize + 16) + .background( + Theme.Colors.commentCellBackground + ) + .overlay( + GeometryReader { proxy in + Rectangle() + .size(width: proxy.size.width, height: 1) + .foregroundColor(Theme.Colors.cardViewStroke) + } + ) + } } } } diff --git a/Core/Core/View/Base/HTMLFormattedText.swift b/Core/Core/View/Base/HTMLFormattedText.swift index 6276b9f2d..08131bfe0 100644 --- a/Core/Core/View/Base/HTMLFormattedText.swift +++ b/Core/Core/View/Base/HTMLFormattedText.swift @@ -37,6 +37,7 @@ public struct HTMLFormattedText: UIViewRepresentable { textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 textView.delegate = context.coordinator + textView.accessibilityIdentifier = "honer_code_textarea" return textView } diff --git a/Core/Core/View/Base/LogistrationBottomView.swift b/Core/Core/View/Base/LogistrationBottomView.swift new file mode 100644 index 000000000..2272997e2 --- /dev/null +++ b/Core/Core/View/Base/LogistrationBottomView.swift @@ -0,0 +1,75 @@ +// +// LogistrationBottomView.swift +// Authorization +// +// Created by SaeedBashir on 10/26/23. +// + +import Foundation +import SwiftUI +import Theme + +public enum LogistrationSourceScreen: Equatable { + case `default` + case startup + case discovery + case courseDetail(String, String) + case programDetails(String) +} + +public enum LogistrationAction { + case signIn + case register +} + +public struct LogistrationBottomView: View { + private let action: (LogistrationAction) -> Void + + @Environment(\.isHorizontal) private var isHorizontal + + public init(_ action: @escaping (LogistrationAction) -> Void) { + self.action = action + } + + public var body: some View { + VStack(alignment: .leading) { + HStack(spacing: 24) { + StyledButton(CoreLocalization.SignIn.registerBtn) { + action(.register) + } + .frame(maxWidth: .infinity) + .accessibilityIdentifier("logistration_register_button") + + StyledButton( + CoreLocalization.SignIn.logInBtn, + action: { + action(.signIn) + }, + color: .white, + textColor: Theme.Colors.accentColor, + borderColor: Theme.Colors.textInputStroke + ) + .frame(width: 100) + .accessibilityIdentifier("logistration_signin_button") + } + .padding(.horizontal, isHorizontal ? 0 : 0) + } + .padding(.horizontal, isHorizontal ? 10 : 24) + } +} + +#if DEBUG +struct LogistrationBottomView_Previews: PreviewProvider { + static var previews: some View { + LogistrationBottomView {_ in } + .preferredColorScheme(.light) + .previewDisplayName("StartupView Light") + .loadFonts() + + LogistrationBottomView {_ in } + .preferredColorScheme(.dark) + .previewDisplayName("StartupView Dark") + .loadFonts() + } +} +#endif diff --git a/Core/Core/View/Base/NavigationBar.swift b/Core/Core/View/Base/NavigationBar.swift index 2af6581f9..e90c2d459 100644 --- a/Core/Core/View/Base/NavigationBar.swift +++ b/Core/Core/View/Base/NavigationBar.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Theme public struct NavigationBar: View { @@ -46,6 +47,7 @@ public struct NavigationBar: View { HStack { Text(title) .titleSettings(color: titleColor) + .accessibilityIdentifier("title_text") } .padding(.horizontal, 24) if leftButton { @@ -55,12 +57,15 @@ public struct NavigationBar: View { }, label: { CoreAssets.arrowLeft.swiftUIImage .backButtonStyle(color: leftButtonColor) + .padding(8) }) .foregroundColor(Theme.Colors.styledButtonText) + .accessibilityIdentifier("back_button") }.frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading) + } if rightButtonType != nil { VStack { @@ -92,6 +97,7 @@ public struct NavigationBar: View { .opacity(rightButtonIsActive ? 1 : 0.3) .padding(.trailing, 16) .foregroundColor(Theme.Colors.styledButtonText) + .accessibilityIdentifier("right_button") }.frame(minWidth: 0, maxWidth: .infinity, alignment: .topTrailing) diff --git a/Core/Core/View/Base/OfflineSnackBarView.swift b/Core/Core/View/Base/OfflineSnackBarView.swift index bc7a01279..2813d0cc9 100644 --- a/Core/Core/View/Base/OfflineSnackBarView.swift +++ b/Core/Core/View/Base/OfflineSnackBarView.swift @@ -7,6 +7,7 @@ import SwiftUI import Combine +import Theme public struct OfflineSnackBarView: View { diff --git a/Core/Core/View/Base/PickerMenu.swift b/Core/Core/View/Base/PickerMenu.swift index a967ffdde..849f99ae4 100644 --- a/Core/Core/View/Base/PickerMenu.swift +++ b/Core/Core/View/Base/PickerMenu.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Theme public struct PickerItem: Hashable { public let key: String @@ -25,6 +26,7 @@ public struct PickerMenu: View { @State private var search: String = "" @State public var selectedItem: PickerItem = PickerItem(key: "", value: "") + @Environment (\.isHorizontal) private var isHorizontal private let ipadPickerWidth: CGFloat = 300 private var items: [PickerItem] private let titleText: String @@ -79,9 +81,11 @@ public struct PickerMenu: View { VStack { Text(titleText) .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("picker_title_text") TextField(CoreLocalization.Picker.search, text: $search) .padding(.all, 8) .background(Theme.Colors.textInputStroke.cornerRadius(6)) + .accessibilityIdentifier("picker_search_textfield") Picker("", selection: $selectedItem) { ForEach(filteredItems, id: \.self) { item in Text(item.value) @@ -89,8 +93,13 @@ public struct PickerMenu: View { } } .pickerStyle(.wheel) + .accessibilityIdentifier("picker") } - .frame(minWidth: 0, maxWidth: idiom == .pad ? ipadPickerWidth : .infinity) + .frame(minWidth: 0, + maxWidth: (idiom == .pad || (idiom == .phone && isHorizontal)) + ? ipadPickerWidth + : .infinity) + .padding() .background(Theme.Colors.textInputBackground.cornerRadius(16)) .padding(.horizontal, 16) @@ -106,13 +115,18 @@ public struct PickerMenu: View { }) { Text(CoreLocalization.Picker.accept) .foregroundColor(Theme.Colors.textPrimary) - .frame(minWidth: 0, maxWidth: idiom == .pad ? ipadPickerWidth : .infinity) + .frame(minWidth: 0, + maxWidth: (idiom == .pad || (idiom == .phone && isHorizontal)) + ? ipadPickerWidth + : .infinity) .padding() .background(Theme.Colors.textInputBackground.cornerRadius(16)) .padding(.horizontal, 16) } .padding(.bottom, 4) .disabled(acceptButtonDisabled) + .accessibilityIdentifier("picker_accept_button") + } .avoidKeyboard(dismissKeyboardByTap: true) .transition(.move(edge: .bottom)) diff --git a/Core/Core/View/Base/PickerView.swift b/Core/Core/View/Base/PickerView.swift index fe8f10adc..c7437ac71 100644 --- a/Core/Core/View/Base/PickerView.swift +++ b/Core/Core/View/Base/PickerView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Theme public struct PickerView: View { @@ -25,6 +26,7 @@ public struct PickerView: View { .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.textPrimary) .padding(.top, 18) + .accessibilityIdentifier("\(config.field.name)_text") HStack { Button(action: { withAnimation( @@ -47,6 +49,7 @@ public struct PickerView: View { Spacer() Image(systemName: "chevron.down") }) + .accessibilityIdentifier("\(config.field.name)_picker_button") }.padding(.all, 14) .foregroundColor(Theme.Colors.textPrimary) .background( @@ -67,6 +70,7 @@ public struct PickerView: View { .foregroundColor(config.error == "" ? Theme.Colors.textPrimary : Color.red) + .accessibilityIdentifier("\(config.field.name)_instructions_text") } } } diff --git a/Core/Core/View/Base/ProgressBar.swift b/Core/Core/View/Base/ProgressBar.swift index 9e75e7985..cf1c12802 100644 --- a/Core/Core/View/Base/ProgressBar.swift +++ b/Core/Core/View/Base/ProgressBar.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Theme public struct ProgressBar: View { @@ -38,7 +39,7 @@ public struct ProgressBar: View { ZStack { Circle() .stroke(lineWidth: lineWidth) - .foregroundColor(Color.blue.opacity(0.3)) + .foregroundColor(Theme.Colors.accentColor.opacity(0.3)) .frame(width: size, height: size) Circle() diff --git a/Core/Core/View/Base/RegistrationTextField.swift b/Core/Core/View/Base/RegistrationTextField.swift index 1d68e8022..70467643f 100644 --- a/Core/Core/View/Base/RegistrationTextField.swift +++ b/Core/Core/View/Base/RegistrationTextField.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Theme public struct RegistrationTextField: View { @@ -37,6 +38,7 @@ public struct RegistrationTextField: View { .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.textPrimary) .padding(.top, 18) + .accessibilityIdentifier("\(config.field.name)_text") } if isTextArea { TextEditor(text: $config.text) @@ -59,6 +61,7 @@ public struct RegistrationTextField: View { ) ) .shake($config.shake) + .accessibilityIdentifier("\(config.field.name)_textarea") } else { if textContentType == .password { SecureField(placeholder, text: $config.text) @@ -81,6 +84,7 @@ public struct RegistrationTextField: View { ) ) .shake($config.shake) + .accessibilityIdentifier("\(config.field.name)_textfield") } else { TextField(placeholder, text: $config.text) .keyboardType(keyboardType) @@ -102,6 +106,7 @@ public struct RegistrationTextField: View { ) ) .shake($config.shake) + .accessibilityIdentifier("\(config.field.name)_textfield") } } @@ -110,6 +115,7 @@ public struct RegistrationTextField: View { .foregroundColor(config.error == "" ? Theme.Colors.textSecondary : Color.red) + .accessibilityIdentifier("\(config.field.name)_instructions_text") } } } diff --git a/Core/Core/View/Base/ScrollSlidingTabBar/FrameReader.swift b/Core/Core/View/Base/ScrollSlidingTabBar/FrameReader.swift new file mode 100644 index 000000000..5cc0ff18f --- /dev/null +++ b/Core/Core/View/Base/ScrollSlidingTabBar/FrameReader.swift @@ -0,0 +1,45 @@ +// +// FrameReader.swift +// +// +// Created by Eugene Yatsenko on 06/11/2023. +// + +import SwiftUI + +extension View { + /// - Parameters: + /// - id: used to differentiate a view and its ancestor if they both call `readFrame` + /// - Note: `onChange` maybe called with duplicated values + public func readFrame( + in space: CoordinateSpace, + id: String = "shared", + onChange: @escaping (CGRect) -> Void + ) -> some View { + background( + GeometryReader { proxy in + Color + .clear + .preference( + key: FramePreferenceKey.self, + value: [.init(space: space, id: id): proxy.frame(in: space)]) + } + ) + .onPreferenceChange(FramePreferenceKey.self) { + onChange($0[.init(space: space, id: id)] ?? .zero) + } + } +} + +private struct FramePreferenceKey: PreferenceKey { + static var defaultValue: [PreferenceValueKey: CGRect] = [:] + + static func reduce(value: inout [PreferenceValueKey: CGRect], nextValue: () -> [PreferenceValueKey: CGRect]) { + value.merge(nextValue()) { $1 } + } +} + +private struct PreferenceValueKey: Hashable { + let space: CoordinateSpace + let id: String +} diff --git a/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift new file mode 100644 index 000000000..0c89f0811 --- /dev/null +++ b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift @@ -0,0 +1,241 @@ +// +// SwiftUIView.swift +// +// +// Created by Eugene Yatsenko on 06/11/2023. +// + +import SwiftUI +import Theme + +public struct ScrollSlidingTabBar: View { + + @Binding private var selection: Int + @State private var buttonFrames: [Int: CGRect] = [:] + + private let tabs: [String] + private let style: Style + private let onTap: ((Int) -> Void)? + + private var containerSpace: String { + return "container" + } + + public init( + selection: Binding, + tabs: [String], + style: Style = .default, + onTap: ((Int) -> Void)? = nil) { + self._selection = selection + self.tabs = tabs + self.style = style + self.onTap = onTap + } + + public var body: some View { + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + buttons() + + ZStack(alignment: .leading) { + Rectangle() + .fill(style.borderColor) + .frame(height: style.borderHeight, alignment: .leading) + indicatorContainer() + } + } + .coordinateSpace(name: containerSpace) + } + .onChange(of: selection) { newValue in + withAnimation { + proxy.scrollTo(newValue, anchor: .center) + } + } + } + } + +} + +extension ScrollSlidingTabBar { + private func buttons() -> some View { + HStack(spacing: 0) { + ForEach(Array(tabs.enumerated()), id: \.offset) { obj in + Button { + selection = obj.offset + onTap?(obj.offset) + } label: { + HStack { + Text(obj.element) + .font(isSelected(index: obj.offset) ? style.selectedFont : style.font) + } + .padding(.horizontal, style.buttonHInset) + .padding(.vertical, style.buttonVInset) + } + .accentColor( + isSelected(index: obj.offset) ? style.activeAccentColor : style.inactiveAccentColor + ) + .readFrame(in: .named(containerSpace)) { + buttonFrames[obj.offset] = $0 + } + .id(obj.offset) + } + } + } + + private func indicatorContainer() -> some View { + Rectangle() + .fill(Color.clear) + .frame(width: tabWidth(), height: style.indicatorHeight) + .overlay(indicator(), alignment: .center) + .offset(x: selectionBarXOffset(), y: 0) + .animation(.default, value: selection) + } + + private func indicator() -> some View { + Rectangle() + .fill(style.activeAccentColor) + .frame(width: indicatorWidth(selection: selection), height: style.indicatorHeight) + } +} + +extension ScrollSlidingTabBar { + private func sanitizedSelection() -> Int { + return max(0, min(tabs.count - 1, selection)) + } + + private func isSelected(index: Int) -> Bool { + return sanitizedSelection() == index + } + + private func selectionBarXOffset() -> CGFloat { + return buttonFrames[sanitizedSelection()]?.minX ?? .zero + } + + private func indicatorWidth(selection: Int) -> CGFloat { + return max(tabWidth() - style.buttonHInset * 2, .zero) + } + + private func tabWidth() -> CGFloat { + return buttonFrames[sanitizedSelection()]?.width ?? .zero + } +} + +extension ScrollSlidingTabBar { + public struct Style { + public let font: Font + public let selectedFont: Font + + public let activeAccentColor: Color + public let inactiveAccentColor: Color + + public let indicatorHeight: CGFloat + + public let borderColor: Color + public let borderHeight: CGFloat + + public let buttonHInset: CGFloat + public let buttonVInset: CGFloat + + public init( + font: Font, + selectedFont: Font, + activeAccentColor: Color, + inactiveAccentColor: Color, + indicatorHeight: CGFloat, + borderColor: Color, + borderHeight: CGFloat, + buttonHInset: CGFloat, + buttonVInset: CGFloat + ) { + self.font = font + self.selectedFont = selectedFont + self.activeAccentColor = activeAccentColor + self.inactiveAccentColor = inactiveAccentColor + self.indicatorHeight = indicatorHeight + self.borderColor = borderColor + self.borderHeight = borderHeight + self.buttonHInset = buttonHInset + self.buttonVInset = buttonVInset + } + + public static let `default` = Style( + font: .body, + selectedFont: .body.bold(), + activeAccentColor: Theme.Colors.accentColor, + inactiveAccentColor: Theme.Colors.textSecondary, + indicatorHeight: 2, + borderColor: .gray.opacity(0.2), + borderHeight: 1, + buttonHInset: 16, + buttonVInset: 10 + ) + } +} + +#if DEBUG +private struct SlidingTabConsumerView: View { + @State + private var selection: Int = 0 + + var body: some View { + VStack(alignment: .leading) { + ScrollSlidingTabBar( + selection: $selection, + tabs: ["First", "Second", "Third", "Fourth", "Fifth", "Sixth"] + ) + TabView(selection: $selection) { + HStack { + Spacer() + Text("First View") + Spacer() + } + .tag(0) + + HStack { + Spacer() + Text("Second View") + Spacer() + } + .tag(1) + + HStack { + Spacer() + Text("Third View") + Spacer() + } + .tag(2) + + HStack { + Spacer() + Text("Fourth View") + Spacer() + } + .tag(3) + + HStack { + Spacer() + Text("Fifth View") + Spacer() + } + .tag(4) + + HStack { + Spacer() + Text("Sixth View") + Spacer() + } + .tag(5) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .animation(.default, value: selection) + } + } +} + +struct ScrollSlidingTabBar_Previews: PreviewProvider { + static var previews: some View { + SlidingTabConsumerView() + } +} +#endif diff --git a/Core/Core/View/Base/SnackBarView.swift b/Core/Core/View/Base/SnackBarView.swift index 0272c15ba..78b2af53c 100644 --- a/Core/Core/View/Base/SnackBarView.swift +++ b/Core/Core/View/Base/SnackBarView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Theme public struct SnackBarView: View { @@ -37,7 +38,7 @@ public struct SnackBarView: View { } }.shadowCardStyle(bgColor: Theme.Colors.snackbarErrorColor, - textColor: .white) + textColor: Theme.Colors.white) .padding(.bottom, 10) } } diff --git a/Core/Core/View/Base/SocialAuthButton.swift b/Core/Core/View/Base/SocialAuthButton.swift new file mode 100644 index 000000000..3ebd367e7 --- /dev/null +++ b/Core/Core/View/Base/SocialAuthButton.swift @@ -0,0 +1,75 @@ +// +// SocialAuthButton.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import SwiftUI +import Theme + +public struct SocialAuthButton: View { + + // MARK: - Properties + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + private var image: Image + private var title: String + private var textColor: Color + private var backgroundColor: Color + private var cornerRadius: CGFloat + private var action: () -> Void + + public init( + image: Image, + title: String, + textColor: Color = .white, + backgroundColor: Color = .accentColor, + cornerRadius: CGFloat = 8, + action: @escaping () -> Void + ) { + self.image = image + self.title = title + self.textColor = textColor + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + self.action = action + } + + // MARK: - Views + + public var body: some View { + Button { + action() + } label: { + Label { + Text(title) + .foregroundStyle(textColor) + .padding(.leading, 10) + Spacer() + } icon: { + image.padding(.leading, 10) + } + } + .frame(maxWidth: idiom == .pad ? 260: .infinity, minHeight: 42) + .background(backgroundColor) + .clipShape( + Theme.Shapes.buttonShape + ) + + } +} + +#if DEBUG +struct LabelButton_Previews: PreviewProvider { + static var previews: some View { + SocialAuthButton( + image: CoreAssets.iconApple.swiftUIImage, + title: "Apple", + backgroundColor: CoreAssets.appleButtonColor.swiftUIColor, + action: { } + ) + } +} +#endif diff --git a/Core/Core/View/Base/StyledButton.swift b/Core/Core/View/Base/StyledButton.swift index b16f61a1a..7190c2469 100644 --- a/Core/Core/View/Base/StyledButton.swift +++ b/Core/Core/View/Base/StyledButton.swift @@ -6,31 +6,38 @@ // import SwiftUI +import Theme public struct StyledButton: View { - private let title: String private let action: () -> Void private let isTransparent: Bool private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } private let buttonColor: Color private let textColor: Color + private let disabledTextColor: Color private let isActive: Bool + private let borderColor: Color public init(_ title: String, action: @escaping () -> Void, isTransparent: Bool = false, - color: Color = Theme.Colors.accentColor, + color: Color = Theme.Colors.accentButtonColor, + textColor: Color = Theme.Colors.styledButtonText, + disabledTextColor: Color = Theme.Colors.textPrimary, + borderColor: Color = .clear, isActive: Bool = true) { self.title = title self.action = action self.isTransparent = isTransparent + self.textColor = textColor + self.disabledTextColor = disabledTextColor + self.borderColor = borderColor + if isActive { self.buttonColor = color - self.textColor = Theme.Colors.styledButtonText } else { self.buttonColor = Theme.Colors.cardViewStroke - self.textColor = Theme.Colors.textPrimary } self.isActive = isActive } @@ -39,7 +46,7 @@ public struct StyledButton: View { Button(action: action) { Text(title) .tracking(isTransparent ? 0 : 1.3) - .foregroundColor(textColor) + .foregroundColor(isActive ? textColor : disabledTextColor) .font(Theme.Fonts.labelLarge) .frame(maxWidth: .infinity) .padding(.horizontal, 16) @@ -51,10 +58,13 @@ public struct StyledButton: View { .fill(isTransparent ? .clear : buttonColor) ) .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) - .foregroundColor(isTransparent ? .white : .clear) + Theme.Shapes.buttonShape + .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) + .foregroundColor(isTransparent ? Theme.Colors.white : borderColor) + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel(title) } } diff --git a/Core/Core/View/Base/UnitButtonView.swift b/Core/Core/View/Base/UnitButtonView.swift index 67a49d0da..3e6890d8e 100644 --- a/Core/Core/View/Base/UnitButtonView.swift +++ b/Core/Core/View/Base/UnitButtonView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Theme public enum UnitButtonType: Equatable { case first @@ -34,7 +35,7 @@ public enum UnitButtonType: Equatable { case .reload: return CoreLocalization.Error.reload case .continueLesson: - return CoreLocalization.Courseware.continue + return CoreLocalization.Courseware.resume case .nextSection: return CoreLocalization.Courseware.nextSection case let .custom(text): @@ -48,11 +49,22 @@ public struct UnitButtonView: View { private let action: () -> Void private let type: UnitButtonType private let bgColor: Color? + private let isVerticalNavigation: Bool - public init(type: UnitButtonType, bgColor: Color? = nil, action: @escaping () -> Void) { + private var nextButtonDegrees: Double { + isVerticalNavigation ? -90 : 180 + } + + public init( + type: UnitButtonType, + isVerticalNavigation: Bool = true, + bgColor: Color? = nil, + action: @escaping () -> Void + ) { self.type = type self.bgColor = bgColor self.action = action + self.isVerticalNavigation = isVerticalNavigation } public var body: some View { @@ -67,7 +79,7 @@ public struct UnitButtonView: View { .font(Theme.Fonts.labelLarge) CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) .foregroundColor(Theme.Colors.styledButtonText) - .rotationEffect(Angle.degrees(-90)) + .rotationEffect(Angle.degrees(nextButtonDegrees)) }.padding(.horizontal, 16) case .next, .nextBig: HStack { @@ -80,31 +92,41 @@ public struct UnitButtonView: View { } CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) .foregroundColor(Theme.Colors.styledButtonText) - .rotationEffect(Angle.degrees(-90)) + .rotationEffect(Angle.degrees(nextButtonDegrees)) .padding(.trailing, 20) } case .previous: HStack { - Text(type.stringValue()) - .foregroundColor(Theme.Colors.accentColor) - .font(Theme.Fonts.labelLarge) - .padding(.leading, 20) - CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .rotationEffect(Angle.degrees(90)) - .padding(.trailing, 20) - .foregroundColor(Theme.Colors.accentColor) - + if isVerticalNavigation { + Text(type.stringValue()) + .foregroundColor(Theme.Colors.accentColor) + .font(Theme.Fonts.labelLarge) + .padding(.leading, 20) + CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) + .rotationEffect(Angle.degrees(90)) + .padding(.trailing, 20) + .foregroundColor(Theme.Colors.accentColor) + } else { + CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) + .padding(.leading, 20) + .foregroundColor(Theme.Colors.accentColor) + Text(type.stringValue()) + .foregroundColor(Theme.Colors.accentColor) + .font(Theme.Fonts.labelLarge) + .padding(.trailing, 20) + } } case .last: HStack { Text(type.stringValue()) .foregroundColor(Theme.Colors.styledButtonText) - .padding(.leading, 16) + .padding(.leading, 8) .font(Theme.Fonts.labelLarge) + .scaledToFit() Spacer() CoreAssets.check.swiftUIImage.renderingMode(.template) .foregroundColor(Theme.Colors.styledButtonText) - .padding(.trailing, 16) + .padding(.trailing, 8) } case .finish: HStack { @@ -141,22 +163,22 @@ public struct UnitButtonView: View { Theme.Shapes.buttonShape .fill(type == .previous ? Theme.Colors.background - : Theme.Colors.accentColor) + : Theme.Colors.accentButtonColor) .shadow(color: Color.black.opacity(0.25), radius: 21, y: 4) .overlay( - RoundedRectangle(cornerRadius: 8) + Theme.Shapes.buttonShape .stroke(style: .init( lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1) ) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(Theme.Colors.accentButtonColor) ) case .continueLesson, .nextSection, .reload, .finish, .custom: Theme.Shapes.buttonShape - .fill(bgColor ?? Theme.Colors.accentColor) + .fill(bgColor ?? Theme.Colors.accentButtonColor) .shadow(color: (type == .first || type == .next @@ -166,14 +188,14 @@ public struct UnitButtonView: View { || type == .reload) ? Color.black.opacity(0.25) : .clear, radius: 21, y: 4) .overlay( - RoundedRectangle(cornerRadius: 8) + Theme.Shapes.buttonShape .stroke(style: .init( lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1 )) - .foregroundColor(Theme.Colors.accentColor) + .foregroundColor(Theme.Colors.accentButtonColor) ) } } diff --git a/Core/Core/View/Base/VideoDownloadQualityView.swift b/Core/Core/View/Base/VideoDownloadQualityView.swift new file mode 100644 index 000000000..c6e4a6148 --- /dev/null +++ b/Core/Core/View/Base/VideoDownloadQualityView.swift @@ -0,0 +1,166 @@ +// +// VideoDownloadQualityView.swift +// Core +// +// Created by Eugene Yatsenko on 19.01.2024. +// + +import SwiftUI +import Kingfisher +import Theme + +public final class VideoDownloadQualityViewModel: ObservableObject { + + var didSelect: ((DownloadQuality) -> Void)? + let downloadQuality = DownloadQuality.allCases + + @Published var selectedDownloadQuality: DownloadQuality { + willSet { + if newValue != selectedDownloadQuality { + didSelect?(newValue) + } + } + } + + public init(downloadQuality: DownloadQuality, didSelect: ((DownloadQuality) -> Void)?) { + self.selectedDownloadQuality = downloadQuality + self.didSelect = didSelect + } +} + +public struct VideoDownloadQualityView: View { + + @StateObject + private var viewModel: VideoDownloadQualityViewModel + + public init( + downloadQuality: DownloadQuality, + didSelect: ((DownloadQuality) -> Void)? + ) { + self._viewModel = StateObject( + wrappedValue: .init( + downloadQuality: downloadQuality, + didSelect: didSelect + ) + ) + } + + public var body: some View { + ZStack(alignment: .top) { + // MARK: - Page Body + ScrollView { + VStack(alignment: .leading, spacing: 24) { + ForEach(viewModel.downloadQuality, id: \.self) { quality in + Button { + viewModel.selectedDownloadQuality = quality + } label: { + HStack { + SettingsCell( + title: quality.title, + description: quality.description + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(quality.title) \(quality.description ?? "")") + Spacer() + CoreAssets.checkmark.swiftUIImage + .renderingMode(.template) + .foregroundColor(.accentColor) + .opacity(quality == viewModel.selectedDownloadQuality ? 1 : 0) + .accessibilityIdentifier("checkmark_image") + + } + .foregroundColor(Theme.Colors.textPrimary) + } + .accessibilityIdentifier("quality_button_cell") + Divider() + } + } + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading + ) + .padding(.horizontal, 24) + } + .frameLimit(sizePortrait: 420) + .padding(.top, 8) + } + .navigationBarHidden(false) + .navigationBarBackButtonHidden(false) + .navigationTitle(CoreLocalization.Settings.videoDownloadQualityTitle) + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + } +} + +public struct SettingsCell: View { + + private var title: String + private var description: String? + + public init(title: String, description: String?) { + self.title = title + self.description = description + } + + public var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(Theme.Fonts.titleMedium) + if let description { + Text(description) + .font(Theme.Fonts.labelMedium) + .foregroundColor(Theme.Colors.textSecondary) + } + }.foregroundColor(Theme.Colors.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +public extension DownloadQuality { + + var title: String { + switch self { + case .auto: + return CoreLocalization.Settings.downloadQualityAutoTitle + case .low_360: + return CoreLocalization.Settings.downloadQuality360Title + case .medium_540: + return CoreLocalization.Settings.downloadQuality540Title + case .high_720: + return CoreLocalization.Settings.downloadQuality720Title + } + } + + var description: String? { + switch self { + case .auto: + return CoreLocalization.Settings.downloadQualityAutoDescription + case .low_360: + return CoreLocalization.Settings.downloadQuality360Description + case .medium_540: + return nil + case .high_720: + return CoreLocalization.Settings.downloadQuality720Description + } + } + + var settingsDescription: String { + switch self { + case .auto: + return CoreLocalization.Settings.downloadQualityAutoTitle + " (" + + CoreLocalization.Settings.downloadQualityAutoDescription + ")" + case .low_360: + return CoreLocalization.Settings.downloadQuality360Title + " (" + + CoreLocalization.Settings.downloadQuality360Description + ")" + case .medium_540: + return CoreLocalization.Settings.downloadQuality540Title + case .high_720: + return CoreLocalization.Settings.downloadQuality720Title + " (" + + CoreLocalization.Settings.downloadQuality720Description + ")" + } + } +} + diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index ffea4335f..dafd884fc 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -7,44 +7,59 @@ import SwiftUI import WebKit +import Theme public struct WebBrowser: View { - - var url: String - var pageTitle: String - @State private var isShowProgress: Bool = true + + @State private var isLoading: Bool = true @Environment(\.presentationMode) var presentationMode + + private var url: String + private var pageTitle: String + private var showProgress: Bool - public init(url: String, pageTitle: String) { + public init(url: String, pageTitle: String, showProgress: Bool = false) { self.url = url self.pageTitle = pageTitle + self.showProgress = showProgress } public var body: some View { - ZStack(alignment: .top) { - CoreAssets.background.swiftUIColor.ignoresSafeArea() - // MARK: - Page name - VStack(alignment: .center) { - NavigationBar(title: pageTitle, - leftButtonAction: { presentationMode.wrappedValue.dismiss() }) - - // MARK: - Page Body - VStack { - ZStack(alignment: .top) { -// NavigationView { - WebView( - viewModel: .init(url: url, baseURL: ""), - isLoading: $isShowProgress, - refreshCookies: {} - ) - -// } - }.navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15 - .navigationBarHidden(true) - .ignoresSafeArea() + GeometryReader { proxy in + ZStack(alignment: .center) { + Theme.Colors.background.ignoresSafeArea() + webView(proxy: proxy) + if isLoading, showProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(20) + } + .frame(maxWidth: .infinity) } } + .navigationBarTitle(Text("")) + .navigationBarHidden(true) + .ignoresSafeArea() + } + } + + private func webView(proxy: GeometryProxy) -> some View { + VStack(alignment: .center) { + NavigationBar( + title: pageTitle, + leftButtonAction: { presentationMode.wrappedValue.dismiss() } + ) + WebView( + viewModel: .init(url: url, baseURL: ""), + isLoading: $isLoading, + refreshCookies: {} + ) } + .padding(.top, proxy.safeAreaInsets.top) + .padding(.bottom, proxy.safeAreaInsets.bottom) } } diff --git a/Core/Core/View/Base/WebUnitView.swift b/Core/Core/View/Base/WebUnitView.swift index ce8a9e098..a00b3a4b2 100644 --- a/Core/Core/View/Base/WebUnitView.swift +++ b/Core/Core/View/Base/WebUnitView.swift @@ -6,17 +6,27 @@ // import SwiftUI -import SwiftUIIntrospect +@_spi(Advanced) import SwiftUIIntrospect +import Theme public struct WebUnitView: View { - - private var url: String - @ObservedObject private var viewModel: WebUnitViewModel + + @StateObject private var viewModel: WebUnitViewModel @State private var isWebViewLoading = false - - public init(url: String, viewModel: WebUnitViewModel) { - self.viewModel = viewModel + + private var url: String + private var injections: [WebviewInjection]? + + public init( + url: String, + viewModel: WebUnitViewModel, + injections: [WebviewInjection]? + ) { + self._viewModel = .init( + wrappedValue: viewModel + ) self.url = url + self.injections = injections } @ViewBuilder @@ -54,14 +64,25 @@ public struct WebUnitView: View { ScrollView { if viewModel.cookiesReady { WebView( - viewModel: .init(url: url, baseURL: viewModel.config.baseURL.absoluteString), - isLoading: $isWebViewLoading, refreshCookies: { - await viewModel.updateCookies(force: true) - }) - .frame(width: reader.size.width, height: reader.size.height) + viewModel: .init( + url: url, + baseURL: viewModel.config.baseURL.absoluteString, + injections: injections + ), + isLoading: $isWebViewLoading, + refreshCookies: { + await viewModel.updateCookies( + force: true + ) + } + ) + .frame( + width: reader.size.width, + height: reader.size.height + ) } } - .introspect(.scrollView, on: .iOS(.v14, .v15, .v16, .v17), customize: { scrollView in + .introspect(.scrollView, on: .iOS(.v15...), customize: { scrollView in scrollView.isScrollEnabled = false }) if viewModel.updatingCookies || isWebViewLoading { diff --git a/Core/Core/View/Base/WebUnitViewModel.swift b/Core/Core/View/Base/WebUnitViewModel.swift index caa010a93..6a76a6ee2 100644 --- a/Core/Core/View/Base/WebUnitViewModel.swift +++ b/Core/Core/View/Base/WebUnitViewModel.swift @@ -8,17 +8,17 @@ import Foundation import SwiftUI -public class WebUnitViewModel: ObservableObject { +public class WebUnitViewModel: ObservableObject, WebviewCookiesUpdateProtocol { - let authInteractor: AuthInteractorProtocol - let config: Config + public let authInteractor: AuthInteractorProtocol + let config: ConfigProtocol - @Published var updatingCookies: Bool = false - @Published var cookiesReady: Bool = false - @Published var showError: Bool = false + @Published public var updatingCookies: Bool = false + @Published public var cookiesReady: Bool = false + @Published public var showError: Bool = false private var retryCount = 1 - var errorMessage: String? { + public var errorMessage: String? { didSet { withAnimation { showError = errorMessage != nil @@ -26,30 +26,8 @@ public class WebUnitViewModel: ObservableObject { } } - public init(authInteractor: AuthInteractorProtocol, config: Config) { + public init(authInteractor: AuthInteractorProtocol, config: ConfigProtocol) { self.authInteractor = authInteractor self.config = config } - - @MainActor - func updateCookies(force: Bool = false) async { - guard !updatingCookies else { return } - do { - updatingCookies = true - try await authInteractor.getCookies(force: force) - cookiesReady = true - updatingCookies = false - errorMessage = nil - } catch { - if error.isInternetError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else if retryCount > 0 { - retryCount -= 1 - await updateCookies(force: force) - } else { - errorMessage = CoreLocalization.Error.unknownError - } - updatingCookies = false - } - } } diff --git a/Core/Core/View/Base/Webview/Models/DragAndDropCssInjection.swift b/Core/Core/View/Base/Webview/Models/DragAndDropCssInjection.swift new file mode 100644 index 000000000..518c442d8 --- /dev/null +++ b/Core/Core/View/Base/Webview/Models/DragAndDropCssInjection.swift @@ -0,0 +1,34 @@ +// +// DragAndDropCssInjection.swift +// Core +// +// Created by Vadim Kuznetsov on 4.01.24. +// + +import WebKit + +public struct DragAndDropCssInjection: WebViewScriptInjectionProtocol, CSSInjectionProtocol { + public var id: String = "DragAndDropCSSInjection" + public var messages: [WebviewMessage]? + public var injectionTime: WKUserScriptInjectionTime = .atDocumentStart + public var forMainFrameOnly: Bool = true + public var script: String { + cssScript(with: css) + } + + var css: String { + """ + .xblock--drag-and-drop .drag-container { + -webkit-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; + } + """ + } + + public init() {} + + public static func == (lhs: DragAndDropCssInjection, rhs: DragAndDropCssInjection) -> Bool { + lhs.script == rhs.script + } +} diff --git a/Core/Core/View/Base/Webview/Models/SurveyCssInjection.swift b/Core/Core/View/Base/Webview/Models/SurveyCssInjection.swift new file mode 100644 index 000000000..9931767c9 --- /dev/null +++ b/Core/Core/View/Base/Webview/Models/SurveyCssInjection.swift @@ -0,0 +1,36 @@ +// +// SurveyCssInjection.swift +// Core +// +// Created by Vadim Kuznetsov on 4.01.24. +// + +import WebKit + +public struct SurveyCssInjection: WebViewScriptInjectionProtocol, CSSInjectionProtocol { + public var id: String = "SurveyCSSInjection" + public var messages: [WebviewMessage]? + public var injectionTime: WKUserScriptInjectionTime = .atDocumentStart + public var forMainFrameOnly: Bool = true + public var script: String { + cssScript(with: css) + } + + var css: String { + """ + .survey-table:not(.poll-results) .survey-option .visible-mobile-only { + width: calc(100% - 21px) !important; + } + + .survey-percentage .percentage { + width: 54px !important; + } + """ + } + + public init() {} + + public static func == (lhs: SurveyCssInjection, rhs: SurveyCssInjection) -> Bool { + lhs.script == rhs.script + } +} diff --git a/Core/Core/View/Base/Webview/Models/WebviewInjection.swift b/Core/Core/View/Base/Webview/Models/WebviewInjection.swift new file mode 100644 index 000000000..8ac215625 --- /dev/null +++ b/Core/Core/View/Base/Webview/Models/WebviewInjection.swift @@ -0,0 +1,54 @@ +// +// WebviewInjection.swift +// Core +// +// Created by Vadim Kuznetsov on 4.01.24. +// + +import WebKit +public struct WebviewInjection: WebViewScriptInjectionProtocol { + public var id: String + public var script: String + public var messages: [WebviewMessage]? + public var injectionTime: WKUserScriptInjectionTime + public var forMainFrameOnly: Bool + init( + id: String, + script: String, + messages: [WebviewMessage]? = nil, + injectionTime: WKUserScriptInjectionTime = .atDocumentEnd, + forMainFrameOnly: Bool = true + ) { + self.id = id + self.script = script + self.messages = messages + self.injectionTime = injectionTime + self.forMainFrameOnly = forMainFrameOnly + } + + public static func == (lhs: WebviewInjection, rhs: WebviewInjection) -> Bool { + lhs.id == rhs.id && + lhs.script == rhs.script && + lhs.injectionTime == rhs.injectionTime && + lhs.messages == rhs.messages && + lhs.forMainFrameOnly == rhs.forMainFrameOnly + } +} + +public extension WebviewInjection { + + static var surveyCSS: WebviewInjection { + SurveyCssInjection() + .webviewInjection() + } + + static var dragAndDropCss: WebviewInjection { + DragAndDropCssInjection() + .webviewInjection() + } + + static var ajaxCallback: WebviewInjection { + AjaxInjection() + .webviewInjection() + } +} diff --git a/Core/Core/View/Base/Webview/Models/WebviewMessage.swift b/Core/Core/View/Base/Webview/Models/WebviewMessage.swift new file mode 100644 index 000000000..ba5467398 --- /dev/null +++ b/Core/Core/View/Base/Webview/Models/WebviewMessage.swift @@ -0,0 +1,16 @@ +// +// WebviewMessage.swift +// Core +// +// Created by Vadim Kuznetsov on 4.01.24. +// + +import WebKit +public struct WebviewMessage: Equatable { + var name: String + var handler: (Any, WKWebView?) -> Void + + public static func == (lhs: WebviewMessage, rhs: WebviewMessage) -> Bool { + lhs.name == rhs.name + } +} diff --git a/Core/Core/View/Base/Webview/Protocols/CSSInjectionProtocol.swift b/Core/Core/View/Base/Webview/Protocols/CSSInjectionProtocol.swift new file mode 100644 index 000000000..0bbc621d3 --- /dev/null +++ b/Core/Core/View/Base/Webview/Protocols/CSSInjectionProtocol.swift @@ -0,0 +1,31 @@ +// +// CSSInjectionProtocol.swift +// Core +// +// Created by Vadim Kuznetsov on 4.01.24. +// + +import Foundation + +public protocol CSSInjectionProtocol { + func cssScript(with css: String) -> String +} + +extension CSSInjectionProtocol { + public func cssScript(with css: String) -> String { + """ + window.addEventListener("load", () => { + var css = `\(css)`, + head = document.head || document.getElementsByTagName('head')[0], + style = document.createElement('style'); + head.appendChild(style); + style.type = 'text/css'; + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + }) + """ + } +} diff --git a/Core/Core/View/Base/Webview/Protocols/WebViewScriptInjectionProtocol.swift b/Core/Core/View/Base/Webview/Protocols/WebViewScriptInjectionProtocol.swift new file mode 100644 index 000000000..8be2468d0 --- /dev/null +++ b/Core/Core/View/Base/Webview/Protocols/WebViewScriptInjectionProtocol.swift @@ -0,0 +1,27 @@ +// +// WebViewScriptInjectionProtocol.swift +// Core +// +// Created by Vadim Kuznetsov on 4.01.24. +// + +import WebKit +public protocol WebViewScriptInjectionProtocol: Equatable, Identifiable { + var id: String { get } + var script: String { get } + var messages: [WebviewMessage]? { get } + var injectionTime: WKUserScriptInjectionTime { get } + var forMainFrameOnly: Bool { get } +} + +extension WebViewScriptInjectionProtocol { + public func webviewInjection() -> WebviewInjection { + WebviewInjection( + id: self.id, + script: self.script, + messages: self.messages, + injectionTime: self.injectionTime, + forMainFrameOnly: self.forMainFrameOnly + ) + } +} diff --git a/Core/Core/View/Base/WebView.swift b/Core/Core/View/Base/Webview/WebView.swift similarity index 59% rename from Core/Core/View/Base/WebView.swift rename to Core/Core/View/Base/Webview/WebView.swift index 9534f7529..db2e754ed 100644 --- a/Core/Core/View/Base/WebView.swift +++ b/Core/Core/View/Base/Webview/WebView.swift @@ -8,6 +8,15 @@ import Foundation import WebKit import SwiftUI +import Theme + +public protocol WebViewNavigationDelegate: AnyObject { + func webView( + _ webView: WKWebView, + shouldLoad request: URLRequest, + navigationAction: WKNavigationAction + ) async -> Bool +} public struct WebView: UIViewRepresentable { @@ -15,62 +24,134 @@ public struct WebView: UIViewRepresentable { @Published var url: String let baseURL: String + let injections: [WebviewInjection]? - public init(url: String, baseURL: String) { + public init(url: String, baseURL: String, injections: [WebviewInjection]? = nil) { self.url = url self.baseURL = baseURL + self.injections = injections } } @ObservedObject var viewModel: ViewModel @Binding public var isLoading: Bool - var refreshCookies: () async -> Void + var webViewNavDelegate: WebViewNavigationDelegate? - public init(viewModel: ViewModel, isLoading: Binding, refreshCookies: @escaping () async -> Void) { + var refreshCookies: () async -> Void + + public init( + viewModel: ViewModel, + isLoading: Binding, + refreshCookies: @escaping () async -> Void, + navigationDelegate: WebViewNavigationDelegate? = nil + ) { self.viewModel = viewModel self._isLoading = isLoading self.refreshCookies = refreshCookies + self.webViewNavDelegate = navigationDelegate + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) } - public class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate { - var parent: WebView + public func makeUIView(context: UIViewRepresentableContext) -> WKWebView { + let webViewConfig = WKWebViewConfiguration() + let webView = WKWebView(frame: .zero, configuration: webViewConfig) + #if DEBUG + if #available(iOS 16.4, *) { + webView.isInspectable = true + } + #endif + webView.navigationDelegate = context.coordinator + webView.uiDelegate = context.coordinator + + context.coordinator.webview = webView + + webView.scrollView.bounces = false + webView.scrollView.alwaysBounceHorizontal = false + webView.scrollView.showsHorizontalScrollIndicator = false + webView.scrollView.isScrollEnabled = true + webView.configuration.suppressesIncrementalRendering = true + webView.isOpaque = false + webView.backgroundColor = .clear + webView.scrollView.backgroundColor = Theme.Colors.white.uiColor() + webView.scrollView.alwaysBounceVertical = false + webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 200, right: 0) + + for injection in viewModel.injections ?? [] { + let script = WKUserScript( + source: injection.script, + injectionTime: injection.injectionTime, + forMainFrameOnly: injection.forMainFrameOnly + ) + webView.configuration.userContentController.addUserScript(script) + + for message in injection.messages ?? [] { + webView.configuration.userContentController.add(context.coordinator, name: message.name) + } + } + + webView.customUserAgent = userAgent + + return webView + } + + public func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext) { + if let url = URL(string: viewModel.url) { + if webview.url?.absoluteString != url.absoluteString { + DispatchQueue.main.async { + isLoading = true + } + let request = URLRequest(url: url) + webview.load(request) + } + } + } + + public class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler { + var parent: WebView + init(_ parent: WebView) { self.parent = parent + super.init() + + addObserver() } - + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { DispatchQueue.main.async { self.parent.isLoading = false } } - + public func webView( _ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void ) { - + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) - + alertController.addAction(UIAlertAction( title: CoreLocalization.Webview.Alert.ok, style: .default, handler: { _ in completionHandler(true) })) - + alertController.addAction(UIAlertAction( title: CoreLocalization.Webview.Alert.cancel, style: .cancel, handler: { _ in completionHandler(false) })) - + UIApplication.topViewController()?.present(alertController, animated: true, completion: nil) } - + public func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction @@ -78,6 +159,17 @@ public struct WebView: UIViewRepresentable { guard let url = navigationAction.request.url else { return .cancel } + let isWebViewDelegateHandled = await ( + parent.webViewNavDelegate?.webView( + webView, + shouldLoad: navigationAction.request, + navigationAction: navigationAction) ?? false + ) + + if isWebViewDelegateHandled { + return .cancel + } + let baseURL = await parent.viewModel.baseURL if !baseURL.isEmpty, !url.absoluteString.starts(with: baseURL) { if navigationAction.navigationType == .other { @@ -94,7 +186,7 @@ public struct WebView: UIViewRepresentable { return .allow } - + public func webView( _ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse @@ -104,7 +196,7 @@ public struct WebView: UIViewRepresentable { return .cancel } let baseURL = await parent.viewModel.baseURL - + if (401...404).contains(response.statusCode) || url.absoluteString.hasPrefix(baseURL + "/login") { await parent.refreshCookies() DispatchQueue.main.async { @@ -116,44 +208,51 @@ public struct WebView: UIViewRepresentable { } return .allow } - } - - public func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - public func makeUIView(context: UIViewRepresentableContext) -> WKWebView { - let webViewConfig = WKWebViewConfiguration() - let webView = WKWebView(frame: .zero, configuration: webViewConfig) - webView.navigationDelegate = context.coordinator - webView.uiDelegate = context.coordinator + private func addObserver() { + NotificationCenter.default.addObserver( + self, + selector: #selector(reload), + name: .webviewReloadNotification, + object: nil + ) + } + + fileprivate var webview: WKWebView? - webView.scrollView.bounces = false - webView.scrollView.alwaysBounceHorizontal = false - webView.scrollView.showsHorizontalScrollIndicator = false - webView.scrollView.isScrollEnabled = true - webView.configuration.suppressesIncrementalRendering = true - webView.isOpaque = false - webView.backgroundColor = .clear - webView.scrollView.backgroundColor = .white - webView.scrollView.alwaysBounceVertical = false - webView.scrollView.layer.cornerRadius = 24 - webView.scrollView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 200, right: 0) + @objc private func reload() { + parent.isLoading = true + webview?.reload() + } - return webView - } - - public func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext) { - if let url = URL(string: viewModel.url) { - if webview.url?.absoluteString != url.absoluteString { - DispatchQueue.main.async { - isLoading = true - } - let request = URLRequest(url: url) - webview.load(request) + deinit { + NotificationCenter.default.removeObserver(self) + } + + public func userContentController( + _ userContentController: WKUserContentController, + didReceive message: WKScriptMessage + ) { + let messages = parent.viewModel.injections?.compactMap({$0.messages}).flatMap({$0}) ?? [] + if let currentMessage = messages.first(where: { $0.name == message.name }) { + currentMessage.handler(message.body, message.webView) } } } + + private var userAgent: String { + let info = Bundle.main.infoDictionary + return [ + info?[kCFBundleExecutableKey as String], + info?[kCFBundleIdentifierKey as String], + info?["CFBundleShortVersionString"] + ] + .compactMap { $0 as? String ?? "" } + .joined(separator: "/") + } + + public static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) { + uiView.configuration.userContentController.removeAllUserScripts() + uiView.configuration.userContentController.removeAllScriptMessageHandlers() + } } diff --git a/Core/Core/View/Base/WebViewHTML.swift b/Core/Core/View/Base/Webview/WebViewHTML.swift similarity index 100% rename from Core/Core/View/Base/WebViewHTML.swift rename to Core/Core/View/Base/Webview/WebViewHTML.swift diff --git a/Core/Core/View/Base/Webview/WebviewCookiesUpdateProtocol.swift b/Core/Core/View/Base/Webview/WebviewCookiesUpdateProtocol.swift new file mode 100644 index 000000000..1e36c4d4e --- /dev/null +++ b/Core/Core/View/Base/Webview/WebviewCookiesUpdateProtocol.swift @@ -0,0 +1,40 @@ +// +// WebviewCookiesUpdateProtocol.swift +// Core +// +// Created by Saeed Bashir on 2/7/24. +// + +import Foundation + +//sourcery: AutoMockable +public protocol WebviewCookiesUpdateProtocol: AnyObject { + var authInteractor: AuthInteractorProtocol { get } + var cookiesReady: Bool { get set } + var updatingCookies: Bool { get set } + var errorMessage: String? { get set } + func updateCookies(force: Bool, retryCount: Int) async +} + +public extension WebviewCookiesUpdateProtocol { + @MainActor + func updateCookies(force: Bool = false, retryCount: Int = 1) async { + guard !updatingCookies else { return } + do { + updatingCookies = true + try await authInteractor.getCookies(force: force) + cookiesReady = true + updatingCookies = false + errorMessage = nil + } catch { + if error.isInternetError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else if retryCount > 0 { + await updateCookies(force: force, retryCount: retryCount - 1) + } else { + errorMessage = CoreLocalization.Error.unknownError + } + updatingCookies = false + } + } +} diff --git a/Core/Core/View/Validator.swift b/Core/Core/View/Validator.swift index f44913db2..7ffb57401 100644 --- a/Core/Core/View/Validator.swift +++ b/Core/Core/View/Validator.swift @@ -17,16 +17,20 @@ public class Validator { public init() { } - public func isValidEmail(_ email: String) -> Bool { - return emailPredicate.evaluate(with: email) + public func isValidEmail(_ string: String) -> Bool { + return emailPredicate.evaluate(with: string) } public func isValidPassword(_ password: String) -> Bool { return password.count >= 2 } - public func isValidUsername(_ username: String) -> Bool { - return username.count >= 2 && username.count <= 30 + public func isValidUsername(_ string: String) -> Bool { + let trimmedString = string.trimmingCharacters(in: .whitespaces) + if trimmedString.contains("@") { + return emailPredicate.evaluate(with: trimmedString) + } else { + return !trimmedString.isEmpty + } } - } diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index f16f781dc..2c982c851 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -20,6 +20,7 @@ "ERROR.USER_NOT_ACTIVE" = "User account is not activated. Please activate your account first."; "ERROR.UNKNOWN_ERROR" = "Something went wrong"; "ERROR.WIFI" = "You can only download files over Wi-Fi. You can change this in the settings."; +"ERROR.AUTHORIZATION_FAILED" = "Authorization failed."; "COURSEWARE.COURSE_CONTENT" = "Course content"; "COURSEWARE.COURSE_UNITS" = "Course units"; @@ -31,7 +32,8 @@ "COURSEWARE.SECTION" = "Section “"; "COURSEWARE.IS_FINISHED" = "“ is finished."; "COURSEWARE.CONTINUE" = "Continue"; -"COURSEWARE.CONTINUE_WITH" = "Continue with:"; +"COURSEWARE.RESUME" = "Resume"; +"COURSEWARE.RESUME_WITH" = "Resume with:"; "COURSEWARE.NEXT_SECTION" = "Next section"; "COURSEWARE.NEXT_SECTION_DESCRIPTION_FIRST" = "To proceed with “"; @@ -49,6 +51,7 @@ "ALERT.LOGOUT" = "Log out"; "ALERT.LEAVE" = "Leave"; "ALERT.KEEP_EDITING" = "Keep editing"; +"ALERT.DELETE" = "DELETE"; "NO_INTERNET.OFFLINE" = "Offline"; "NO_INTERNET.DISMISS" = "Dismiss"; @@ -61,6 +64,15 @@ "DOWNLOAD_MANAGER.DOWNLOADED" = "Downloaded"; "DOWNLOAD_MANAGER.COMPLETED" = "Completed"; +"SETTINGS.VIDEO_DOWNLOAD_QUALITY_TITLE" = "Video download quality"; +"SETTINGS.DOWNLOAD_QUALITY_AUTO_TITLE" = "Auto"; +"SETTINGS.DOWNLOAD_QUALITY_AUTO_DESCRIPTION" = "Recommended"; +"SETTINGS.DOWNLOAD_QUALITY_360_TITLE" = "360p"; +"SETTINGS.DOWNLOAD_QUALITY_360_DESCRIPTION" = "Lower data usage"; +"SETTINGS.DOWNLOAD_QUALITY_540_TITLE" = "540p"; +"SETTINGS.DOWNLOAD_QUALITY_720_TITLE" = "720p"; +"SETTINGS.DOWNLOAD_QUALITY_720_DESCRIPTION" = "Best quality"; + "DONE" = "Done"; "PICKER.SEARCH" = "Search"; @@ -68,3 +80,36 @@ "WEBVIEW.ALERT.OK" = "Ok"; "WEBVIEW.ALERT.CANCEL" = "Cancel"; +"WEBVIEW.ALERT.CONTINUE" = "Continue"; + +"REVIEW.VOTE_TITLE" = "Enjoying Open edX?"; +"REVIEW.VOTE_DESCRIPTION" = "Your feedback matters to us. Would you take a moment to rate the app by tapping a star below? Thanks for your support!"; +"REVIEW.FEEDBACK_TITLE" = "Leave Us Feedback"; +"REVIEW.FEEDBACK_DESCRIPTION" = "We’re sorry to hear your learning experience has had some issues. We appreciate all feedback."; +"REVIEW.THANKS_FOR_VOTE_TITLE" = "Thank You"; +"REVIEW.THANKS_FOR_VOTE_DESCRIPTION" = "Thank you for sharing your feedback with us. Would you like to share your review of this app with other users on the app store?"; +"REVIEW.THANKS_FOR_FEEDBACK_TITLE" = "Thank You"; +"REVIEW.THANKS_FOR_FEEDBACK_DESCRIPTION" = "We received your feedback and will use it to help improve your learning experience going forward. Thank you for sharing!"; +"REVIEW.BETTER" = "What could have been better?"; +"REVIEW.NOT_NOW" = "Not now"; + +"REVIEW.BUTTON.SUBMIT" = "Submit"; +"REVIEW.BUTTON.SHARE_FEEDBACK" = "Share Feedback"; +"REVIEW.BUTTON.RATE_US" = "Rate Us"; +"REVIEW.EMAIL.TITLE" = "Select email client:"; + +"COURSE_DATES.TODAY" = "Today"; +"COURSE_DATES.COMPLETED" = "Completed"; +"COURSE_DATES.PAST_DUE" = "Past due"; +"COURSE_DATES.DUE_NEXT" = "Due next"; +"COURSE_DATES.UNRELEASED" = "Unreleased"; +"COURSE_DATES.VERIFIED_ONLY" = "Verified Only"; +"COURSE_DATES.TOMORROW" = "Tomorrow"; +"COURSE_DATES.YESTERDAY" = "Yesterday"; +"COURSE_DATES.ITEMS_HIDDEN" = "Items Hidden"; +"COURSE_DATES.ITEM_HIDDEN" = "Item Hidden"; + +"SOCIAL_SIGN_CANCELED" = "The user canceled the sign-in flow."; + +"SIGN_IN.LOG_IN_BTN" = "Sign in"; +"SIGN_IN.REGISTER_BTN" = "Register"; diff --git a/Core/Core/uk.lproj/Localizable.strings b/Core/Core/uk.lproj/Localizable.strings index e06937311..1f0ecb198 100644 --- a/Core/Core/uk.lproj/Localizable.strings +++ b/Core/Core/uk.lproj/Localizable.strings @@ -31,7 +31,8 @@ "COURSEWARE.SECTION" = "Секція “"; "COURSEWARE.IS_FINISHED" = "“ завершена."; "COURSEWARE.CONTINUE" = "Продовжити"; -"COURSEWARE.CONTINUE_WITH" = "Продовжити далі:"; +"COURSEWARE.RESUME" = "Resume"; +"COURSEWARE.RESUME_WITH" = "Продовжити далі:"; "COURSEWARE.NEXT_SECTION" = "Наступний розділ"; "COURSEWARE.NEXT_SECTION_DESCRIPTION_FIRST" = "Щоб перейти до “"; @@ -61,6 +62,15 @@ "DOWNLOAD_MANAGER.DOWNLOADED" = "Скачано"; "DOWNLOAD_MANAGER.COMPLETED" = "Завершено"; +"SETTINGS.VIDEO_DOWNLOAD_QUALITY_TITLE" = "Video download quality"; +"SETTINGS.DOWNLOAD_QUALITY_AUTO_TITLE" = "Auto"; +"SETTINGS.DOWNLOAD_QUALITY_AUTO_DESCRIPTION" = "Recommended"; +"SETTINGS.DOWNLOAD_QUALITY_360_TITLE" = "360p"; +"SETTINGS.DOWNLOAD_QUALITY_360_DESCRIPTION" = "Lower data usage"; +"SETTINGS.DOWNLOAD_QUALITY_540_TITLE" = "540p"; +"SETTINGS.DOWNLOAD_QUALITY_720_TITLE" = "720p"; +"SETTINGS.DOWNLOAD_QUALITY_720_DESCRIPTION" = "Best quality"; + "DONE" = "Зберегти"; "PICKER.SEARCH" = "Знайти"; @@ -68,3 +78,38 @@ "WEBVIEW.ALERT.OK" = "Так"; "WEBVIEW.ALERT.CANCEL" = "Скасувати"; +"WEBVIEW.ALERT.CONTINUE" = "Continue"; + + +"REVIEW.VOTE_TITLE" = "Вам подобається Open edX?"; +"REVIEW.VOTE_DESCRIPTION" = "Ваш відгук важливий для нас. Можливо, ви візьмете хвилинку, щоб оцінити додаток, натиснувши на зірку нижче? Дякуємо за вашу підтримку!"; +"REVIEW.FEEDBACK_TITLE" = "Залиште відгук"; +"REVIEW.FEEDBACK_DESCRIPTION" = "Нам шкода чути, що ваше навчання мало деякі проблеми. Ми вдячні за будь-який відгук."; +"REVIEW.THANKS_FOR_VOTE_TITLE" = "Дякуємо"; +"REVIEW.THANKS_FOR_VOTE_DESCRIPTION" = "Дякуємо, що поділилися своїми враженнями з нами. Бажаєте залишити свій відгук про цей додаток для інших користувачів в магазині додатків?"; +"REVIEW.THANKS_FOR_FEEDBACK_TITLE" = "Дякуємо"; +"REVIEW.THANKS_FOR_FEEDBACK_DESCRIPTION" = "Ми отримали ваш відгук і використовуватимемо його для покращення вашого навчального досвіду в майбутньому!"; +"REVIEW.BETTER" = "Що можна було б зробити краще?"; +"REVIEW.NOT_NOW" = "Не зараз"; + +"REVIEW.BUTTON.SUBMIT" = "Надіслати"; +"REVIEW.BUTTON.SHARE_FEEDBACK" = "Поділитися відгуком"; +"REVIEW.BUTTON.RATE_US" = "Оцінити нас"; +"REVIEW.EMAIL.TITLE" = "Виберіть поштового клієнта:"; + +"COURSE_DATES.TODAY" = "Today"; +"COURSE_DATES.COMPLETED" = "Completed"; +"COURSE_DATES.PAST_DUE" = "Past due"; +"COURSE_DATES.DUE_NEXT" = "Due next"; +"COURSE_DATES.UNRELEASED" = "Unreleased"; +"COURSE_DATES.VERIFIED_ONLY" = "Verified Only"; +"COURSE_DATES.TOMORROW" = "Tomorrow"; +"COURSE_DATES.YESTERDAY" = "Yesterday"; +"COURSE_DATES.ITEMS_HIDDEN" = "Items Hidden"; +"COURSE_DATES.ITEM_HIDDEN" = "Item Hidden"; + +"SOCIAL_SIGN_CANCELED" = "The user canceled the sign-in flow."; +"AUTHORIZATION_FAILED" = "Authorization failed."; + +"SIGN_IN.LOG_IN_BTN" = "Увійти"; +"SIGN_IN.REGISTER_BTN" = "Реєстрація"; diff --git a/Core/CoreTests/Configuration/AgreementConfigTests.swift b/Core/CoreTests/Configuration/AgreementConfigTests.swift new file mode 100644 index 000000000..9a30e84fb --- /dev/null +++ b/Core/CoreTests/Configuration/AgreementConfigTests.swift @@ -0,0 +1,38 @@ +// +// AgreementConfigTests.swift +// CoreTests +// +// Created by Eugene Yatsenko on 14.12.2023. +// + +import XCTest +@testable import Core + +class AgreementConfigTests: XCTestCase { + + private let privacy = "https://www.example.com/privacy" + private let tos = "https://www.example.com/tos" + private let dataSellContent = "https://www.example.com/sell" + private let cookie = "https://www.example.com/cookie" + private let supportedLanguages = ["es"] + + private lazy var properties: [String: Any] = [ + "AGREEMENT_URLS": [ + "PRIVACY_POLICY_URL": privacy, + "TOS_URL": tos, + "DATA_SELL_CONSENT_URL": dataSellContent, + "COOKIE_POLICY_URL": cookie, + "SUPPORTED_LANGUAGES": supportedLanguages + ] + ] + + func testAgreementConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertEqual(config.agreement.privacyPolicyURL, URL(string: privacy)) + XCTAssertEqual(config.agreement.tosURL, URL(string: tos)) + XCTAssertEqual(config.agreement.cookiePolicyURL, URL(string: cookie)) + XCTAssertEqual(config.agreement.dataSellContentURL, URL(string: dataSellContent)) + XCTAssertEqual(config.agreement.supportedLanguages, supportedLanguages) + } +} diff --git a/Core/CoreTests/Configuration/ConfigTests.swift b/Core/CoreTests/Configuration/ConfigTests.swift new file mode 100644 index 000000000..32638e484 --- /dev/null +++ b/Core/CoreTests/Configuration/ConfigTests.swift @@ -0,0 +1,118 @@ +// +// ConfigTests.swift +// CoreTests +// +// Created by Muhammad Umer on 11/13/23. +// + +import XCTest +@testable import Core + +class ConfigTests: XCTestCase { + + private lazy var properties: [String: Any] = [ + "API_HOST_URL": "https://www.example.com", + "OAUTH_CLIENT_ID": "oauth_client_id", + "FEEDBACK_EMAIL_ADDRESS": "example@mail.com", + "TOKEN_TYPE": "JWT", + "WHATS_NEW_ENABLED": true, + "AGREEMENT_URLS": [ + "PRIVACY_POLICY_URL": "https://www.example.com/privacy", + "TOS_URL": "https://www.example.com/tos", + "DATA_SELL_CONSENT_URL": "https://www.example.com/sell", + "COOKIE_POLICY_URL": "https://www.example.com/cookie", + "SUPPORTED_LANGUAGES": ["es"] + ], + "FIREBASE": [ + "ENABLED": true, + "API_KEY": "testApiKey", + "BUNDLE_ID": "testBundleID", + "CLIENT_ID": "testClientID", + "DATABASE_URL": "https://test.database.url", + "GCM_SENDER_ID": "testGCMSenderID", + "GOOGLE_APP_ID": "testGoogleAppID", + "PROJECT_ID": "testProjectID", + "REVERSED_CLIENT_ID": "testReversedClientID", + "STORAGE_BUCKET": "testStorageBucket", + "ANALYTICS_SOURCE": "firebase", + "CLOUD_MESSAGING_ENABLED": true + ], + "GOOGLE": [ + "ENABLED": true, + "CLIENT_ID": "clientId" + ], + "FACEBOOK": [ + "ENABLED": true, + "FACEBOOK_APP_ID": "facebookAppId", + "CLIENT_TOKEN": "client_token" + ], + "MICROSOFT": [ + "ENABLED": true, + "APP_ID": "appId" + ], + "APPLE_SIGNIN": [ + "ENABLED": true + ] + ] + + func testConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertEqual(config.baseURL.absoluteString, "https://www.example.com") + XCTAssertEqual(config.oAuthClientId, "oauth_client_id") + XCTAssertEqual(config.feedbackEmail, "example@mail.com") + XCTAssertEqual(config.tokenType.rawValue, "JWT") + XCTAssertTrue(config.features.whatNewEnabled) + } + + func testFeaturesConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.features.whatNewEnabled) + } + + func testFirebaseConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.firebase.enabled) + XCTAssertEqual(config.firebase.apiKey, "testApiKey") + XCTAssertEqual(config.firebase.bundleID, "testBundleID") + XCTAssertEqual(config.firebase.clientID, "testClientID") + XCTAssertEqual(config.firebase.databaseURL, "https://test.database.url") + XCTAssertEqual(config.firebase.gcmSenderID, "testGCMSenderID") + XCTAssertEqual(config.firebase.googleAppID, "testGoogleAppID") + XCTAssertEqual(config.firebase.projectID, "testProjectID") + XCTAssertEqual(config.firebase.reversedClientID, "testReversedClientID") + XCTAssertEqual(config.firebase.storageBucket, "testStorageBucket") + XCTAssertEqual(config.firebase.isAnalyticsSourceFirebase, true) + XCTAssertEqual(config.firebase.cloudMessagingEnabled, true) + } + + func testGoogleConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.google.enabled) + XCTAssertEqual(config.google.clientID, "clientId") + } + + func testFacebookConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.facebook.enabled) + XCTAssertEqual(config.facebook.appID, "facebookAppId") + XCTAssertEqual(config.facebook.clientToken, "client_token") + } + + func testMicrosoftConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.microsoft.enabled) + XCTAssertEqual(config.microsoft.appID, "appId") + } + + func testAppleConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.appleSignIn.enabled) + } +} diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index c80d63032..8d28ee87e 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -20,7 +20,6 @@ 022F8E182A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022F8E172A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift */; }; 0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0231124C28EDA804002588FB /* CourseUnitView.swift */; }; 0231124F28EDA811002588FB /* CourseUnitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0231124E28EDA811002588FB /* CourseUnitViewModel.swift */; }; - 023812E7297AC8EB0087098F /* CourseDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023812E6297AC8EB0087098F /* CourseDetailsViewModelTests.swift */; }; 023812E8297AC8EB0087098F /* Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0289F8EE28E1C3510064F8F3 /* Course.framework */; platformFilter = ios; }; 023812F3297AC9ED0087098F /* CourseMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023812F2297AC9EC0087098F /* CourseMock.generated.swift */; }; 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454C9F2A2618E70043052A /* YouTubeView.swift */; }; @@ -45,11 +44,7 @@ 02B6B3B728E1D11E00232911 /* CourseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3B628E1D11E00232911 /* CourseInteractor.swift */; }; 02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3BB28E1D14F00232911 /* CourseRepository.swift */; }; 02B6B3BE28E1D15C00232911 /* CourseEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3BD28E1D15C00232911 /* CourseEndpoint.swift */; }; - 02B6B3C128E1DBA100232911 /* Data_CourseDetailsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3C028E1DBA100232911 /* Data_CourseDetailsResponse.swift */; }; - 02B6B3C328E1DCD100232911 /* CourseDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3C228E1DCD100232911 /* CourseDetails.swift */; }; 02B6B3C928E1E68100232911 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02B6B3C828E1E68100232911 /* Core.framework */; }; - 02E685BE28E4B60A000AE015 /* CourseDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E685BD28E4B60A000AE015 /* CourseDetailsView.swift */; }; - 02E685C028E4B629000AE015 /* CourseDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E685BF28E4B629000AE015 /* CourseDetailsViewModel.swift */; }; 02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F0144E28F46474002E513D /* CourseContainerView.swift */; }; 02F0145728F4A2FF002E513D /* CourseContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F0145628F4A2FF002E513D /* CourseContainerViewModel.swift */; }; 02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F066E729DC71750073E13B /* SubtittlesView.swift */; }; @@ -58,12 +53,36 @@ 02F78AEB29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F78AEA29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift */; }; 02F98A8128F8224200DE94C0 /* Discussion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02F98A8028F8224200DE94C0 /* Discussion.framework */; }; 02FFAD0D29E4347300140E46 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */; }; + 060E8BCA2B5FD68C0080C952 /* UnitStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060E8BC92B5FD68C0080C952 /* UnitStack.swift */; }; + 068DDA5F2B1E198700FF8CCB /* CourseUnitDropDownList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5B2B1E198700FF8CCB /* CourseUnitDropDownList.swift */; }; + 068DDA602B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5C2B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift */; }; + 068DDA612B1E198700FF8CCB /* CourseUnitDropDownCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5D2B1E198700FF8CCB /* CourseUnitDropDownCell.swift */; }; + 068DDA622B1E198700FF8CCB /* CourseUnitDropDownTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5E2B1E198700FF8CCB /* CourseUnitDropDownTitle.swift */; }; + 06FD7EDF2B1F29F3008D632B /* CourseVerticalImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FD7EDE2B1F29F3008D632B /* CourseVerticalImageView.swift */; }; + 06FD7EE32B1F3FF6008D632B /* DropdownAnimationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FD7EE22B1F3FF6008D632B /* DropdownAnimationModifier.swift */; }; 073512E229C0E400005CFA41 /* BaseCourseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073512E129C0E400005CFA41 /* BaseCourseViewModel.swift */; }; 0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0766DFCB299AA7A600EBEF6A /* YouTubeVideoPlayer.swift */; }; 0766DFCE299AB26D00EBEF6A /* EncodedVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0766DFCD299AB26D00EBEF6A /* EncodedVideoPlayer.swift */; }; 0766DFD0299AB29000EBEF6A /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0766DFCF299AB29000EBEF6A /* PlayerViewController.swift */; }; 197FB8EA8F92F00A8F383D82 /* Pods_App_Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E795BD160CDA7D9C120DE6 /* Pods_App_Course.framework */; }; + 975F475E2B6151FD00E5B031 /* CourseDatesMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975F475D2B6151FD00E5B031 /* CourseDatesMock.swift */; }; + 975F47602B615DA700E5B031 /* CourseStructureMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975F475F2B615DA700E5B031 /* CourseStructureMock.swift */; }; B8F50317B6B830A0E520C954 /* Pods_App_Course_CourseTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50E59D2B81E12610964282C5 /* Pods_App_Course_CourseTests.framework */; }; + BAD9CA2D2B2736BB00DE790A /* LessonLineProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA2C2B2736BB00DE790A /* LessonLineProgressView.swift */; }; + BA58CF5D2B3D804D005B102E /* CourseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF5C2B3D804D005B102E /* CourseStorage.swift */; }; + BA58CF612B471041005B102E /* VideoDownloadQualityBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF602B471041005B102E /* VideoDownloadQualityBarView.swift */; }; + BA58CF642B471363005B102E /* VideoDownloadQualityContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF632B471363005B102E /* VideoDownloadQualityContainerView.swift */; }; + BAAD62C82AFD00EE000E6103 /* CourseStructureNestedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */; }; + BAC0E0D82B32EF03006B68A9 /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC0E0D72B32EF03006B68A9 /* DownloadsView.swift */; }; + BAC0E0DB2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC0E0DA2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift */; }; + BAC0E0DE2B32F0F3006B68A9 /* DownloadsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC0E0DD2B32F0F3006B68A9 /* DownloadsViewModel.swift */; }; + BAD9CA442B2C87A200DE790A /* CourseStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */; }; + BAD9CA4A2B2C88E000DE790A /* CourseVideoDownloadBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA492B2C88E000DE790A /* CourseVideoDownloadBarView.swift */; }; + DB205BFB2AE81B1200136EC2 /* CourseDateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */; }; + DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */; }; + DB7D6EAE2ADFCB4A0036BB13 /* CourseDatesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */; }; + DB7D6EB02ADFDA0E0036BB13 /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAF2ADFDA0E0036BB13 /* CourseDates.swift */; }; + DB7D6EB22ADFE9510036BB13 /* Data_CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EB12ADFE9510036BB13 /* Data_CourseDates.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -91,7 +110,6 @@ 0231124C28EDA804002588FB /* CourseUnitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUnitView.swift; sourceTree = ""; }; 0231124E28EDA811002588FB /* CourseUnitViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUnitViewModel.swift; sourceTree = ""; }; 023812E4297AC8EA0087098F /* CourseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CourseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 023812E6297AC8EB0087098F /* CourseDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetailsViewModelTests.swift; sourceTree = ""; }; 023812F2297AC9EC0087098F /* CourseMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseMock.generated.swift; sourceTree = ""; }; 02454C9F2A2618E70043052A /* YouTubeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeView.swift; sourceTree = ""; }; 02454CA12A26190A0043052A /* EncodedVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedVideoView.swift; sourceTree = ""; }; @@ -116,11 +134,8 @@ 02B6B3B628E1D11E00232911 /* CourseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInteractor.swift; sourceTree = ""; }; 02B6B3BB28E1D14F00232911 /* CourseRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRepository.swift; sourceTree = ""; }; 02B6B3BD28E1D15C00232911 /* CourseEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseEndpoint.swift; sourceTree = ""; }; - 02B6B3C028E1DBA100232911 /* Data_CourseDetailsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseDetailsResponse.swift; sourceTree = ""; }; 02B6B3C228E1DCD100232911 /* CourseDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetails.swift; sourceTree = ""; }; 02B6B3C828E1E68100232911 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 02E685BD28E4B60A000AE015 /* CourseDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CourseDetailsView.swift; path = Course/Presentation/Details/CourseDetailsView.swift; sourceTree = SOURCE_ROOT; }; - 02E685BF28E4B629000AE015 /* CourseDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CourseDetailsViewModel.swift; path = Course/Presentation/Details/CourseDetailsViewModel.swift; sourceTree = SOURCE_ROOT; }; 02ED50CF29A64BB6008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02F0144E28F46474002E513D /* CourseContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerView.swift; sourceTree = ""; }; 02F0145628F4A2FF002E513D /* CourseContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerViewModel.swift; sourceTree = ""; }; @@ -130,6 +145,13 @@ 02F78AEA29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerViewModelTests.swift; path = CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift; sourceTree = SOURCE_ROOT; }; 02F98A8028F8224200DE94C0 /* Discussion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discussion.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; + 060E8BC92B5FD68C0080C952 /* UnitStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitStack.swift; sourceTree = ""; }; + 068DDA5B2B1E198700FF8CCB /* CourseUnitDropDownList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitDropDownList.swift; sourceTree = ""; }; + 068DDA5C2B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitVerticalsDropdownView.swift; sourceTree = ""; }; + 068DDA5D2B1E198700FF8CCB /* CourseUnitDropDownCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitDropDownCell.swift; sourceTree = ""; }; + 068DDA5E2B1E198700FF8CCB /* CourseUnitDropDownTitle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitDropDownTitle.swift; sourceTree = ""; }; + 06FD7EDE2B1F29F3008D632B /* CourseVerticalImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVerticalImageView.swift; sourceTree = ""; }; + 06FD7EE22B1F3FF6008D632B /* DropdownAnimationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownAnimationModifier.swift; sourceTree = ""; }; 073512E129C0E400005CFA41 /* BaseCourseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCourseViewModel.swift; sourceTree = ""; }; 0766DFCB299AA7A600EBEF6A /* YouTubeVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeVideoPlayer.swift; sourceTree = ""; }; 0766DFCD299AB26D00EBEF6A /* EncodedVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedVideoPlayer.swift; sourceTree = ""; }; @@ -145,11 +167,28 @@ 600A464AEDA7BEFEC1A7FE99 /* Pods-App-Course-CourseTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course-CourseTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests.debugdev.xcconfig"; sourceTree = ""; }; 69A261790A0B4C724CCFA4F2 /* Pods-App-CourseDetails.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.release.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.release.xcconfig"; sourceTree = ""; }; 831B08139890634968EDD44D /* Pods-App-Course-CourseTests.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course-CourseTests.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests.debugstage.xcconfig"; sourceTree = ""; }; + 975F475D2B6151FD00E5B031 /* CourseDatesMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDatesMock.swift; sourceTree = ""; }; + 975F475F2B615DA700E5B031 /* CourseStructureMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStructureMock.swift; sourceTree = ""; }; 99AEF08FD75F1509863D3302 /* Pods-App-CourseDetails.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.debugprod.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.debugprod.xcconfig"; sourceTree = ""; }; 9B5D3D31A9CFA08B6C4347BD /* Pods-App-CourseDetails.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.releasedev.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.releasedev.xcconfig"; sourceTree = ""; }; A47C63D9EB0D866F303D4588 /* Pods-App-Course.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Course/Pods-App-Course.releasestage.xcconfig"; sourceTree = ""; }; ADC2A1B8183A674705F5F7E2 /* Pods-App-Course.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course.debug.xcconfig"; path = "Target Support Files/Pods-App-Course/Pods-App-Course.debug.xcconfig"; sourceTree = ""; }; B196A14555D0E006995A5683 /* Pods-App-CourseDetails.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.releaseprod.xcconfig"; sourceTree = ""; }; + BAD9CA2C2B2736BB00DE790A /* LessonLineProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonLineProgressView.swift; sourceTree = ""; }; + BA58CF5C2B3D804D005B102E /* CourseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStorage.swift; sourceTree = ""; }; + BA58CF602B471041005B102E /* VideoDownloadQualityBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQualityBarView.swift; sourceTree = ""; }; + BA58CF632B471363005B102E /* VideoDownloadQualityContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQualityContainerView.swift; sourceTree = ""; }; + BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStructureNestedListView.swift; sourceTree = ""; }; + BAC0E0D72B32EF03006B68A9 /* DownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsView.swift; sourceTree = ""; }; + BAC0E0DA2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVideoDownloadBarViewModel.swift; sourceTree = ""; }; + BAC0E0DD2B32F0F3006B68A9 /* DownloadsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsViewModel.swift; sourceTree = ""; }; + BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStructureView.swift; sourceTree = ""; }; + BAD9CA492B2C88E000DE790A /* CourseVideoDownloadBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVideoDownloadBarView.swift; sourceTree = ""; }; + DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDateViewModelTests.swift; sourceTree = ""; }; + DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseDatesView.swift; sourceTree = ""; }; + DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDatesViewModel.swift; sourceTree = ""; }; + DB7D6EAF2ADFDA0E0036BB13 /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; + DB7D6EB12ADFE9510036BB13 /* Data_CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseDates.swift; sourceTree = ""; }; DBE05972CB5115D4535C6B8A /* Pods-App-Course-CourseTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course-CourseTests.debug.xcconfig"; path = "Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests.debug.xcconfig"; sourceTree = ""; }; E5E795BD160CDA7D9C120DE6 /* Pods_App_Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E6BDAE887ED8A46860B3F6D3 /* Pods-App-Course-CourseTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course-CourseTests.release.xcconfig"; path = "Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests.release.xcconfig"; sourceTree = ""; }; @@ -204,12 +243,15 @@ 02454C9E2A2618D40043052A /* Subviews */ = { isa = PBXGroup; children = ( + 068DDA5A2B1E198700FF8CCB /* DropdownList */, 02454C9F2A2618E70043052A /* YouTubeView.swift */, 02454CA12A26190A0043052A /* EncodedVideoView.swift */, 02454CA32A26193F0043052A /* WebView.swift */, 02454CA52A26196C0043052A /* UnknownView.swift */, 02454CA72A2619890043052A /* DiscussionView.swift */, 02454CA92A2619B40043052A /* LessonProgressView.swift */, + BAD9CA2C2B2736BB00DE790A /* LessonLineProgressView.swift */, + 060E8BC92B5FD68C0080C952 /* UnitStack.swift */, ); path = Subviews; sourceTree = ""; @@ -270,7 +312,9 @@ 0208666A29CC6D8000BC05B2 /* Persistence */, 02B6B3BF28E1DB8800232911 /* Model */, 02B6B3B928E1D13500232911 /* Network */, + 975F475C2B61517A00E5B031 /* Mock */, 02B6B3BB28E1D14F00232911 /* CourseRepository.swift */, + BA58CF5C2B3D804D005B102E /* CourseStorage.swift */, ); path = Data; sourceTree = ""; @@ -286,11 +330,11 @@ 02B6B3BF28E1DB8800232911 /* Model */ = { isa = PBXGroup; children = ( - 02B6B3C028E1DBA100232911 /* Data_CourseDetailsResponse.swift */, 0276D75A29DDA3890004CDF8 /* Data_ResumeBlock.swift */, 027020FB28E7362100F54332 /* Data_CourseOutlineResponse.swift */, 022C64DB29ACFDEE000F532B /* Data_HandoutsResponse.swift */, 022C64DF29ADEA9B000F532B /* Data_UpdatesResponse.swift */, + DB7D6EB12ADFE9510036BB13 /* Data_CourseDates.swift */, ); path = Model; sourceTree = ""; @@ -319,44 +363,50 @@ 02EAE2CA28E1F0A700529644 /* Presentation */ = { isa = PBXGroup; children = ( + DB7D6EAA2ADFCAA00036BB13 /* Dates */, 070019A828F6F33600D5FC78 /* Container */, - 070019A628F6F2CB00D5FC78 /* Details */, 070019A728F6F2D600D5FC78 /* Outline */, 02DFC65029ACC20A00EA4BB9 /* Handouts */, 070019A928F6F59D00D5FC78 /* Unit */, 070019AA28F6F79E00D5FC78 /* Video */, + BAC0E0DC2B32F0EA006B68A9 /* Downloads */, + BAD9CA482B2C88D500DE790A /* Subviews */, 02F3BFDC29252E900051930C /* CourseRouter.swift */, 02F175362A4DAFD20019CD70 /* CourseAnalytics.swift */, ); path = Presentation; sourceTree = ""; }; - 070019A328F6EFC100D5FC78 /* Model */ = { + 068DDA5A2B1E198700FF8CCB /* DropdownList */ = { isa = PBXGroup; children = ( - 02B6B3C228E1DCD100232911 /* CourseDetails.swift */, - 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */, - 022C64E129ADEB83000F532B /* CourseUpdate.swift */, + 068DDA5B2B1E198700FF8CCB /* CourseUnitDropDownList.swift */, + 068DDA5C2B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift */, + 068DDA5D2B1E198700FF8CCB /* CourseUnitDropDownCell.swift */, + 068DDA5E2B1E198700FF8CCB /* CourseUnitDropDownTitle.swift */, + 06FD7EE22B1F3FF6008D632B /* DropdownAnimationModifier.swift */, ); - path = Model; + path = DropdownList; sourceTree = ""; }; - 070019A628F6F2CB00D5FC78 /* Details */ = { + 070019A328F6EFC100D5FC78 /* Model */ = { isa = PBXGroup; children = ( - 02E685BD28E4B60A000AE015 /* CourseDetailsView.swift */, - 02E685BF28E4B629000AE015 /* CourseDetailsViewModel.swift */, + 02B6B3C228E1DCD100232911 /* CourseDetails.swift */, + 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */, + 022C64E129ADEB83000F532B /* CourseUpdate.swift */, + DB7D6EAF2ADFDA0E0036BB13 /* CourseDates.swift */, ); - path = Details; + path = Model; sourceTree = ""; }; 070019A728F6F2D600D5FC78 /* Outline */ = { isa = PBXGroup; children = ( + BAD9CA462B2C888600DE790A /* CourseVertical */, + BAD9CA472B2C88AA00DE790A /* CourseStructure */, 02635AC62A24F181008062F2 /* ContinueWithView.swift */, 0270210128E736E700F54332 /* CourseOutlineView.swift */, - 02A8076729474831007F53AB /* CourseVerticalView.swift */, - 0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */, ); path = Outline; sourceTree = ""; @@ -400,7 +450,6 @@ isa = PBXGroup; children = ( 0766DFD5299ADA4700EBEF6A /* Container */, - 0766DFD6299ADA4F00EBEF6A /* Details */, 0766DFD7299ADA6C00EBEF6A /* Unit */, ); path = Presentation; @@ -415,21 +464,78 @@ path = Container; sourceTree = ""; }; - 0766DFD6299ADA4F00EBEF6A /* Details */ = { + 0766DFD7299ADA6C00EBEF6A /* Unit */ = { + isa = PBXGroup; + children = ( + 0262148E29AE17C4008BD75A /* HandoutsViewModelTests.swift */, + 0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */, + DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */, + ); + path = Unit; + sourceTree = ""; + }; + BA58CF622B471047005B102E /* VideoDownloadQualityBarView */ = { isa = PBXGroup; children = ( - 023812E6297AC8EB0087098F /* CourseDetailsViewModelTests.swift */, + BA58CF602B471041005B102E /* VideoDownloadQualityBarView.swift */, + BA58CF632B471363005B102E /* VideoDownloadQualityContainerView.swift */, ); - path = Details; + path = VideoDownloadQualityBarView; sourceTree = ""; }; - 0766DFD7299ADA6C00EBEF6A /* Unit */ = { + BAC0E0D92B32F0A2006B68A9 /* CourseVideoDownloadBarView */ = { isa = PBXGroup; children = ( - 0262148E29AE17C4008BD75A /* HandoutsViewModelTests.swift */, - 0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */, + BAD9CA492B2C88E000DE790A /* CourseVideoDownloadBarView.swift */, + BAC0E0DA2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift */, ); - path = Unit; + path = CourseVideoDownloadBarView; + sourceTree = ""; + }; + BAC0E0DC2B32F0EA006B68A9 /* Downloads */ = { + isa = PBXGroup; + children = ( + BAC0E0D72B32EF03006B68A9 /* DownloadsView.swift */, + BAC0E0DD2B32F0F3006B68A9 /* DownloadsViewModel.swift */, + ); + path = Downloads; + sourceTree = ""; + }; + BAD9CA462B2C888600DE790A /* CourseVertical */ = { + isa = PBXGroup; + children = ( + 06FD7EDE2B1F29F3008D632B /* CourseVerticalImageView.swift */, + 02A8076729474831007F53AB /* CourseVerticalView.swift */, + 0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */, + ); + path = CourseVertical; + sourceTree = ""; + }; + BAD9CA472B2C88AA00DE790A /* CourseStructure */ = { + isa = PBXGroup; + children = ( + BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */, + BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */, + ); + path = CourseStructure; + sourceTree = ""; + }; + BAD9CA482B2C88D500DE790A /* Subviews */ = { + isa = PBXGroup; + children = ( + BAC0E0D92B32F0A2006B68A9 /* CourseVideoDownloadBarView */, + BA58CF622B471047005B102E /* VideoDownloadQualityBarView */, + ); + path = Subviews; + sourceTree = ""; + }; + 975F475C2B61517A00E5B031 /* Mock */ = { + isa = PBXGroup; + children = ( + 975F475D2B6151FD00E5B031 /* CourseDatesMock.swift */, + 975F475F2B615DA700E5B031 /* CourseStructureMock.swift */, + ); + path = Mock; sourceTree = ""; }; D52670044E8768425E23C627 /* Pods */ = { @@ -462,6 +568,15 @@ path = ../Pods; sourceTree = ""; }; + DB7D6EAA2ADFCAA00036BB13 /* Dates */ = { + isa = PBXGroup; + children = ( + DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */, + DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */, + ); + path = Dates; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -662,8 +777,8 @@ files = ( 0295B1D9297E6DF8003B0C65 /* CourseUnitViewModelTests.swift in Sources */, 02F78AEB29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift in Sources */, - 023812E7297AC8EB0087098F /* CourseDetailsViewModelTests.swift in Sources */, 023812F3297AC9ED0087098F /* CourseMock.generated.swift in Sources */, + DB205BFB2AE81B1200136EC2 /* CourseDateViewModelTests.swift in Sources */, 022EA8CB297AD63B0014A8F7 /* CourseContainerViewModelTests.swift in Sources */, 0262148F29AE17C4008BD75A /* HandoutsViewModelTests.swift in Sources */, ); @@ -673,37 +788,58 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 06FD7EE32B1F3FF6008D632B /* DropdownAnimationModifier.swift in Sources */, 02FFAD0D29E4347300140E46 /* VideoPlayerViewModel.swift in Sources */, 02454CA42A26193F0043052A /* WebView.swift in Sources */, 022C64DA29ACEC50000F532B /* HandoutsViewModel.swift in Sources */, 02635AC72A24F181008062F2 /* ContinueWithView.swift in Sources */, + 068DDA622B1E198700FF8CCB /* CourseUnitDropDownTitle.swift in Sources */, + BAD9CA4A2B2C88E000DE790A /* CourseVideoDownloadBarView.swift in Sources */, 022C64DE29AD167A000F532B /* HandoutsUpdatesDetailView.swift in Sources */, + BA58CF612B471041005B102E /* VideoDownloadQualityBarView.swift in Sources */, 0270210328E736E700F54332 /* CourseOutlineView.swift in Sources */, + 068DDA602B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift in Sources */, 022C64E029ADEA9B000F532B /* Data_UpdatesResponse.swift in Sources */, 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */, 02454CA22A26190A0043052A /* EncodedVideoView.swift in Sources */, + 068DDA612B1E198700FF8CCB /* CourseUnitDropDownCell.swift in Sources */, + BAC0E0DE2B32F0F3006B68A9 /* DownloadsViewModel.swift in Sources */, 02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */, 02280F60294B50030032823A /* CoursePersistenceProtocol.swift in Sources */, 02454CAA2A2619B40043052A /* LessonProgressView.swift in Sources */, + 975F475E2B6151FD00E5B031 /* CourseDatesMock.swift in Sources */, + DB7D6EB22ADFE9510036BB13 /* Data_CourseDates.swift in Sources */, + 975F47602B615DA700E5B031 /* CourseStructureMock.swift in Sources */, 02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */, 0766DFCE299AB26D00EBEF6A /* EncodedVideoPlayer.swift in Sources */, 0276D75B29DDA3890004CDF8 /* Data_ResumeBlock.swift in Sources */, 0276D75D29DDA3F80004CDF8 /* ResumeBlock.swift in Sources */, 02F3BFDD29252E900051930C /* CourseRouter.swift in Sources */, + 068DDA5F2B1E198700FF8CCB /* CourseUnitDropDownList.swift in Sources */, 022F8E182A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift in Sources */, 0248C92729C097EB00DC8402 /* CourseVerticalViewModel.swift in Sources */, + BAD9CA442B2C87A200DE790A /* CourseStructureView.swift in Sources */, 02F0145728F4A2FF002E513D /* CourseContainerViewModel.swift in Sources */, 02B6B3B728E1D11E00232911 /* CourseInteractor.swift in Sources */, 073512E229C0E400005CFA41 /* BaseCourseViewModel.swift in Sources */, 0231124F28EDA811002588FB /* CourseUnitViewModel.swift in Sources */, 02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */, + BAAD62C82AFD00EE000E6103 /* CourseStructureNestedListView.swift in Sources */, 02A8076829474831007F53AB /* CourseVerticalView.swift in Sources */, 0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */, 027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */, + DB7D6EB02ADFDA0E0036BB13 /* CourseDates.swift in Sources */, + BAD9CA2D2B2736BB00DE790A /* LessonLineProgressView.swift in Sources */, 02E685C028E4B629000AE015 /* CourseDetailsViewModel.swift in Sources */, + 060E8BCA2B5FD68C0080C952 /* UnitStack.swift in Sources */, 0295C889299BBE8200ABE571 /* CourseNavigationView.swift in Sources */, + 06FD7EDF2B1F29F3008D632B /* CourseVerticalImageView.swift in Sources */, + BAC0E0DB2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift in Sources */, + DB7D6EAE2ADFCB4A0036BB13 /* CourseDatesViewModel.swift in Sources */, 02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */, 022C64E229ADEB83000F532B /* CourseUpdate.swift in Sources */, + BA58CF642B471363005B102E /* VideoDownloadQualityContainerView.swift in Sources */, + BA58CF5D2B3D804D005B102E /* CourseStorage.swift in Sources */, 02454CA62A26196C0043052A /* UnknownView.swift in Sources */, 0766DFD0299AB29000EBEF6A /* PlayerViewController.swift in Sources */, 022C64DC29ACFDEE000F532B /* Data_HandoutsResponse.swift in Sources */, @@ -711,12 +847,12 @@ 02454CA82A2619890043052A /* DiscussionView.swift in Sources */, 0265B4B728E2141D00E6EAFD /* Strings.swift in Sources */, 02B6B3C128E1DBA100232911 /* Data_CourseDetailsResponse.swift in Sources */, + BAC0E0D82B32EF03006B68A9 /* DownloadsView.swift in Sources */, + DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */, 0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */, 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */, - 02E685BE28E4B60A000AE015 /* CourseDetailsView.swift in Sources */, 02F175372A4DAFD20019CD70 /* CourseAnalytics.swift in Sources */, 02B6B3BE28E1D15C00232911 /* CourseEndpoint.swift in Sources */, - 02B6B3C328E1DCD100232911 /* CourseDetails.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Course/Course.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Course/Course.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/Course/Course.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Course/Course.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Course/Course.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Course/Course.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index 68e833255..d5f540a0d 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -9,73 +9,53 @@ import Foundation import Core public protocol CourseRepositoryProtocol { - func getCourseDetails(courseID: String) async throws -> CourseDetails func getCourseBlocks(courseID: String) async throws -> CourseStructure - func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails - func getCourseBlocksOffline(courseID: String) throws -> CourseStructure - func enrollToCourse(courseID: String) async throws -> Bool + func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure func blockCompletionRequest(courseID: String, blockID: String) async throws func getHandouts(courseID: String) async throws -> String? func getUpdates(courseID: String) async throws -> [CourseUpdate] func resumeBlock(courseID: String) async throws -> ResumeBlock func getSubtitles(url: String, selectedLanguage: String) async throws -> String + func getCourseDates(courseID: String) async throws -> CourseDates + func getCourseDatesOffline(courseID: String) async throws -> CourseDates } public class CourseRepository: CourseRepositoryProtocol { private let api: API - private let appStorage: CoreStorage - private let config: Config + private let coreStorage: CoreStorage + private let config: ConfigProtocol private let persistence: CoursePersistenceProtocol - public init(api: API, - appStorage: CoreStorage, - config: Config, - persistence: CoursePersistenceProtocol) { + public init( + api: API, + coreStorage: CoreStorage, + config: ConfigProtocol, + persistence: CoursePersistenceProtocol + ) { self.api = api - self.appStorage = appStorage + self.coreStorage = coreStorage self.config = config self.persistence = persistence } - - public func getCourseDetails(courseID: String) async throws -> CourseDetails { - let response = try await api.requestData(CourseEndpoint.getCourseDetail(courseID: courseID)) - .mapResponse(DataLayer.CourseDetailsResponse.self) - .domain(baseURL: config.baseURL.absoluteString) - persistence.saveCourseDetails(course: response) - return response - } - - public func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails { - return try persistence.loadCourseDetails(courseID: courseID) - } public func getCourseBlocks(courseID: String) async throws -> CourseStructure { let course = try await api.requestData( - CourseEndpoint.getCourseBlocks(courseID: courseID, userName: appStorage.user?.username ?? "") + CourseEndpoint.getCourseBlocks(courseID: courseID, userName: coreStorage.user?.username ?? "") ).mapResponse(DataLayer.CourseStructure.self) persistence.saveCourseStructure(structure: course) let parsedStructure = parseCourseStructure(course: course) return parsedStructure } - public func getCourseBlocksOffline(courseID: String) throws -> CourseStructure { + public func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure { let localData = try persistence.loadCourseStructure(courseID: courseID) return parseCourseStructure(course: localData) } - public func enrollToCourse(courseID: String) async throws -> Bool { - let enroll = try await api.request(CourseEndpoint.enrollToCourse(courseID: courseID)) - if enroll.statusCode == 200 { - return true - } else { - return false - } - } - public func blockCompletionRequest(courseID: String, blockID: String) async throws { try await api.requestData(CourseEndpoint.blockCompletionRequest( - username: appStorage.user?.username ?? "", + username: coreStorage.user?.username ?? "", courseID: courseID, blockID: blockID) ) @@ -94,7 +74,7 @@ public class CourseRepository: CourseRepositoryProtocol { public func resumeBlock(courseID: String) async throws -> ResumeBlock { return try await api.requestData(CourseEndpoint - .resumeBlock(userName: appStorage.user?.username ?? "", courseID: courseID)) + .resumeBlock(userName: coreStorage.user?.username ?? "", courseID: courseID)) .mapResponse(DataLayer.ResumeBlock.self).domain } @@ -112,6 +92,18 @@ public class CourseRepository: CourseRepositoryProtocol { } } + public func getCourseDates(courseID: String) async throws -> CourseDates { + let courseDates = try await api.requestData( + CourseEndpoint.getCourseDates(courseID: courseID) + ).mapResponse(DataLayer.CourseDates.self).domain + persistence.saveCourseDates(courseID: courseID, courseDates: courseDates) + return courseDates + } + + public func getCourseDatesOffline(courseID: String) async throws -> CourseDates { + return try persistence.loadCourseDates(courseID: courseID) + } + private func parseCourseStructure(course: DataLayer.CourseStructure) -> CourseStructure { let blocks = Array(course.dict.values) let courseBlock = blocks.first(where: {$0.type == BlockType.course.rawValue })! @@ -209,17 +201,37 @@ public class CourseRepository: CourseRepositoryProtocol { displayName: block.displayName, studentUrl: block.studentUrl, subtitles: subtitles, - videoUrl: block.userViewData?.encodedVideo?.fallback?.url, - youTubeUrl: block.userViewData?.encodedVideo?.youTube?.url + encodedVideo: .init( + fallback: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.fallback), + youtube: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.youTube), + desktopMP4: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.desktopMP4), + mobileHigh: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.mobileHigh), + mobileLow: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.mobileLow), + hls: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.hls) + ) ) } + private func parseVideo(encodedVideo: DataLayer.EncodedVideoData?) -> CourseBlockVideo? { + guard let encodedVideo else { + return nil + } + return .init( + url: encodedVideo.url, + fileSize: encodedVideo.fileSize, + streamPriority: encodedVideo.streamPriority + ) + } } // Mark - For testing and SwiftUI preview // swiftlint:disable all #if DEBUG class CourseRepositoryMock: CourseRepositoryProtocol { + func getCourseDatesOffline(courseID: String) async throws -> CourseDates { + throw NoCachedDataError() + } + func resumeBlock(courseID: String) async throws -> ResumeBlock { ResumeBlock(blockID: "123") } @@ -232,61 +244,33 @@ class CourseRepositoryMock: CourseRepositoryProtocol { return [CourseUpdate(id: 1, date: "Date", content: "content", status: "status")] } - - func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails { - return CourseDetails( - courseID: "courseID", - org: "Organization", - courseTitle: "Course title", - courseDescription: "Course description", - courseStart: Date(iso8601: "2021-05-26T12:13:14Z"), - courseEnd: Date(iso8601: "2022-05-26T12:13:14Z"), - enrollmentStart: nil, - enrollmentEnd: nil, - isEnrolled: false, - overviewHTML: "Course description

Lorem ipsum", - courseBannerURL: "courseBannerURL", - courseVideoURL: nil - ) + func getCourseDates(courseID: String) async throws -> CourseDates { + do { + let courseDates = try + CourseRepository.courseDatesJSON.data(using: .utf8)!.mapResponse(DataLayer.CourseDates.self) + return courseDates.domain + } catch { + throw error + } } - func getCourseBlocksOffline(courseID: String) throws -> CourseStructure { + func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure { let decoder = JSONDecoder() - let jsonData = Data(courseStructureJson.utf8) + let jsonData = Data(CourseRepository.courseStructureJson.utf8) let courseBlocks = try decoder.decode(DataLayer.CourseStructure.self, from: jsonData) return parseCourseStructure(course: courseBlocks) } - - public func getCourseDetails(courseID: String) async throws -> CourseDetails { - return CourseDetails( - courseID: "courseID", - org: "Organization", - courseTitle: "Course title", - courseDescription: "Course description", - courseStart: Date(iso8601: "2021-05-26T12:13:14Z"), - courseEnd: Date(iso8601: "2022-05-26T12:13:14Z"), - enrollmentStart: nil, - enrollmentEnd: nil, - isEnrolled: false, - overviewHTML: "Course description

Lorem ipsum", - courseBannerURL: "courseBannerURL", - courseVideoURL: nil - ) - } public func getCourseBlocks(courseID: String) async throws -> CourseStructure { do { - let courseBlocks = try courseStructureJson.data(using: .utf8)!.mapResponse(DataLayer.CourseStructure.self) + let courseBlocks = try + CourseRepository.courseStructureJson.data(using: .utf8)!.mapResponse(DataLayer.CourseStructure.self) return parseCourseStructure(course: courseBlocks) } catch { throw error } } - public func enrollToCourse(courseID: String) async throws -> Bool { - return true - } - public func blockCompletionRequest(courseID: String, blockID: String) { } @@ -399,20 +383,39 @@ And there are various ways of describing it-- call it oral poetry or return SubtitleUrl(language: $0.key, url: url) } - return CourseBlock(blockId: block.blockId, - id: block.id, - courseId: courseId, - topicId: block.userViewData?.topicID, - graded: block.graded, - completion: block.completion ?? 0, - type: BlockType(rawValue: block.type) ?? .unknown, - displayName: block.displayName, - studentUrl: block.studentUrl, - subtitles: subtitles, - videoUrl: block.userViewData?.encodedVideo?.fallback?.url, - youTubeUrl: block.userViewData?.encodedVideo?.youTube?.url) + return CourseBlock( + blockId: block.blockId, + id: block.id, + courseId: courseId, + topicId: block.userViewData?.topicID, + graded: block.graded, + completion: block.completion ?? 0, + type: BlockType(rawValue: block.type) ?? .unknown, + displayName: block.displayName, + studentUrl: block.studentUrl, + subtitles: subtitles, + encodedVideo: .init( + fallback: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.fallback), + youtube: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.youTube), + desktopMP4: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.desktopMP4), + mobileHigh: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.mobileHigh), + mobileLow: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.mobileLow), + hls: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.hls) + ) + ) } - + + private func parseVideo(encodedVideo: DataLayer.EncodedVideoData?) -> CourseBlockVideo? { + guard let encodedVideo else { + return nil + } + return .init( + url: encodedVideo.url, + fileSize: encodedVideo.fileSize, + streamPriority: encodedVideo.streamPriority + ) + } + private let courseStructureJson: String = """ {"root": "block-v1:QA+comparison+2022+type@course+block@course", "blocks": { @@ -1034,6 +1037,373 @@ And there are various ways of describing it-- call it oral poetry or "is_self_paced": false } """ + + private let courseDatesJSON: String = """ + { + "dates_banner_info": { + "missed_deadlines": false, + "content_type_gating_enabled": true, + "missed_gated_content": false, + "verified_upgrade_link": null + }, + "course_date_blocks": [ + { + "assignment_type": null, + "complete": null, + "date": "2023-08-30T15:00:00Z", + "date_type": "course-start-date", + "description": "", + "learner_has_access": true, + "link": "", + "link_text": "", + "title": "Course starts", + "extra_info": null, + "first_component_block_id": "" + }, + { + "assignment_type": "Problem Set", + "complete": true, + "date": "2023-09-14T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@ca19e125470846f2a36ad1225410e39a", + "link_text": "", + "title": "Problem Set 1", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@problem+block@bd89c1dd129240f99bb8c5cbe3f56530" + }, + { + "assignment_type": "Problem Set", + "complete": false, + "date": "2023-09-14T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "", + "link_text": "", + "title": "Problem Set 1.1", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@problem+block@bd89c1dd129240f99bb8c5cbe3f56530a" + }, + { + "assignment_type": "Problem Set", + "complete": false, + "date": "2023-09-21T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@e137765987514da7851a59dedeb5ecec", + "link_text": "", + "title": "Problem Set 2", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@c99e81ffff4546e28fecab0a0c381abd" + }, + { + "assignment_type": "Problem Set", + "complete": true, + "date": "2023-09-21T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@e137765987514da7851a59dedeb5ececc", + "link_text": "", + "title": "Problem Set 2.1", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@c99e81ffff4546e28fecab0a0c381abdc" + }, + { + "assignment_type": "Problem Set", + "complete": false, + "date": "2023-09-21T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": false, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@e137765987514da7851a59dedeb5ececcc", + "link_text": "", + "title": "Problem Set 2.2", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@c99e81ffff4546e28fecab0a0c381abdcc" + }, + { + "assignment_type": "Problem Set", + "complete": false, + "date": "2023-09-28T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@bfe9eb02884a4812883ff9e543887968", + "link_text": "", + "title": "Problem Set 3", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@5e117d71433647eaa6de63434641c011" + }, + { + "assignment_type": "Midterm", + "complete": false, + "date": "2023-10-04T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": false, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@bb284b9c4ff04091951f77b50e3b72f4", + "link_text": "", + "title": "Midterm Exam (time limit removed due to grader issues)", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@vertical+block@ec1c5d83de6244d38b1f3ff4d32b6e17" + }, + { + "assignment_type": "Problem Set", + "complete": false, + "date": "2023-10-12T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@64f4d344ecdc48d2bef514882e6236ab", + "link_text": "", + "title": "Problem Set 4", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@eeb64a67e52e4f3e80656b9233204f25" + }, + { + "assignment_type": "Problem Set", + "complete": false, + "date": "2023-10-19T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@79d22d4ab4f740158930fca4e80d67db", + "link_text": "", + "title": "Problem Set 5", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@html+block@3dde572871fc4b6ebdb47722a184a514" + }, + { + "assignment_type": "Problem Set", + "complete": false, + "date": "2023-10-26T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@3d419098708e4bcd9209ffa31a4cb3dc", + "link_text": "", + "title": "Problem Set 6", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@problem+block@9b2a0176bf6a4c21ad4a63c2fce2d0cb" + }, + { + "assignment_type": "Final Exam", + "complete": false, + "date": "2023-10-31T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": false, + "link": "", + "link_text": "", + "title": "Final Exam (time limit removed due to grader issues)", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@vertical+block@e7b4f091d7ad457097d0bbda9d9af267" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@221a4c17dba341d6a970a0d80343253c", + "link_text": "", + "title": "1. Introduction to Python (TIME: 1:03:12)", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@ad9387910b7e47069c452efebd7b36dd" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@35f82f6c3ecb4e9e913dc279a9b73a9f", + "link_text": "", + "title": "2. Core Elements of Programs (TIME: 54:14)", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@8fb4fa767a204d41a6366c2bc53bea22" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@62f08cc899344863a1ab678aee506dec", + "link_text": "", + "title": "3. Simple Algorithms (TIME: 41:06)", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@1f2b055948c9467492649b59e24e8fdc" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@38007cdb67c44b46b124cdbce33510b5", + "link_text": "", + "title": "4. Functions (TIME: 1:08:06)", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@9dc4c11c46274b87964c7534b449d50a" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@01df98c1e74a459b8fb20d2d785622cd", + "link_text": "", + "title": "5. Tuples and Lists", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@3464df78190b43948ba0507ef4287290" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@8a590293a22e46dd9760ec917d122ec1", + "link_text": "", + "title": "6. Dictionaries", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@d2abc5b3db0d43ba90c5d3a25e95e2d5" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@78648402e8bf4738ade97101cc1ba263", + "link_text": "", + "title": "7. Testing and Debugging", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@dd0621fbfe594e789b187a1e4f8406eb" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@c81c3de20ec54c37a04a8b3d1806e82c", + "link_text": "", + "title": "8. Exceptions and Assertions", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@6038a1b2f8a340eb8cdb41c021d62234" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@37cb9a5012e443bbaa776a80afd9c87a", + "link_text": "", + "title": "9. Classes and Inheritance", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@b87e596b827142f09e9664fac3ab0be0" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@54cd6b1bbbbe40f294ac0b5664c03f1e", + "link_text": "", + "title": "10. An Extended Example", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@6bc79b1a29ac46a7857caa53a8e203d0" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@1334ab336b1b4458b5c2972c50e903b2", + "link_text": "", + "title": "11. Computational Complexity", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@be73e5a3ee7847d98805a257189b9fad" + }, + { + "assignment_type": "Finger Exercises", + "complete": false, + "date": "2023-11-01T23:30:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:MITx+6.00.1x+2T2023a/jump_to/block-v1:MITx+6.00.1x+2T2023a+type@sequential+block@a7387dbd3728491c8f834e29a73e0cf4", + "link_text": "", + "title": "12. Searching and Sorting Algorithms", + "extra_info": null, + "first_component_block_id": "block-v1:MITx+6.00.1x+2T2023a+type@video+block@fa7e29b3b95b4a3b963d1c5dfdd4e8f8" + }, + { + "assignment_type": null, + "complete": null, + "date": "2023-11-01T23:30:00Z", + "date_type": "course-end-date", + "description": "After the course ends, the course content will be archived and no longer active.", + "learner_has_access": true, + "link": "", + "link_text": "", + "title": "Course ends", + "extra_info": null, + "first_component_block_id": "" + }, + { + "assignment_type": null, + "complete": null, + "date": "2023-11-03T00:00:00Z", + "date_type": "certificate-available-date", + "description": "Day certificates will become available for passing verified learners.", + "learner_has_access": true, + "link": "", + "link_text": "", + "title": "Certificate Available", + "extra_info": null, + "first_component_block_id": "" + }, + { + "assignment_type": null, + "complete": null, + "date": "2023-11-23T12:34:28Z", + "date_type": "course-expired-date", + "description": "You lose all access to this course, including your progress.", + "learner_has_access": true, + "link": "", + "link_text": "", + "title": "Audit Access Expires", + "extra_info": null, + "first_component_block_id": "" + } + ], + "has_ended": false, + "learner_is_full_access": false, + "user_timezone": null + } + """ } #endif // swiftlint:enable all diff --git a/Course/Course/Data/CourseStorage.swift b/Course/Course/Data/CourseStorage.swift new file mode 100644 index 000000000..1bfc64704 --- /dev/null +++ b/Course/Course/Data/CourseStorage.swift @@ -0,0 +1,25 @@ +// +// CourseStorage.swift +// Course +// +// Created by Eugene Yatsenko on 28.12.2023. +// + +import Foundation +import Core + +public protocol CourseStorage { + var allowedDownloadLargeFile: Bool? { get set } + var userSettings: UserSettings? { get set } +} + +#if DEBUG +public class CourseStorageMock: CourseStorage { + + public var userSettings: UserSettings? + + public var allowedDownloadLargeFile: Bool? + + public init() {} +} +#endif diff --git a/Course/Course/Data/Mock/CourseDatesMock.swift b/Course/Course/Data/Mock/CourseDatesMock.swift new file mode 100644 index 000000000..66dcce53c --- /dev/null +++ b/Course/Course/Data/Mock/CourseDatesMock.swift @@ -0,0 +1,616 @@ +// +// CourseDatesMock.swift +// Course +// +// Created by Shafqat Muneer on 1/24/24. +// + +import Foundation + +// Mark - For testing and SwiftUI preview +// swiftlint:disable all +#if DEBUG +extension CourseRepository { + static let courseDatesJSON: String = """ + { + "dates_banner_info": { + "missed_deadlines": true, + "content_type_gating_enabled": false, + "missed_gated_content": false, + "verified_upgrade_link": "https://ecommerce.edx.org/basket/add/?sku=87701A8" + }, + "course_date_blocks": [ + { + "assignment_type": null, + "complete": null, + "date": "2023-09-26T04:41:52Z", + "date_type": "course-start-date", + "description": "", + "learner_has_access": true, + "link": "", + "link_text": "", + "title": "Course starts", + "extra_info": null, + "first_component_block_id": "" + }, + { + "assignment_type": "Module 0", + "complete": true, + "date": "2024-01-11T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@fcc82b68eb944b83962be3fdaa004ac0", + "link_text": "", + "title": "Course Policies and Expectations", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@problem+block@c0e15c6127404c6d9f21b588342067ac" + }, + { + "assignment_type": "Module 0", + "complete": false, + "date": "2024-01-11T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@2a3ed0b868cd429ebdfbd1fe416d9c5a", + "link_text": "", + "title": "Syllabus Quiz", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@987158ea28e24f13855ff6abfb3e63e2" + }, + { + "assignment_type": "Module 1", + "complete": false, + "date": "2024-01-15T20:19:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@84141e77957c45cca6b5039da8ee679d", + "link_text": "", + "title": "1.2 Why Should we Invest in ECD Interventions?", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@bf278c64a4034e88a33e0904fe03e256" + }, + { + "assignment_type": "Module 1", + "complete": false, + "date": "2024-01-16T09:19:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@e2d228da088648479e5fda7478c006b8", + "link_text": "", + "title": "1.6 Closing", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@c97497458d30419ea0d41f6b3688e584" + }, + { + "assignment_type": "Module 2", + "complete": false, + "date": "2024-01-16T21:59:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@ade8a7cfbf724bf5b58902bd27b613b0", + "link_text": "", + "title": "2.2 What can we learn about active ingredients from case studies of ECD interventions around the world?", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@e14daeca668d4c77a6f918b41b486195" + }, + { + "assignment_type": "Module 3", + "complete": false, + "date": "2024-01-17T17:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@57ee148192f046859d6c8a2c3531ab49", + "link_text": "", + "title": "3.1 Delivery and Dose", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@ded1a7f220cb403887d3f894d0171762" + }, + { + "assignment_type": "Module 3", + "complete": false, + "date": "2024-01-22T06:59:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@5be015e209ee4a44b71a10682ddb3eee", + "link_text": "", + "title": "3.2 Demand", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@e4f7b5f3cf9c498cb9d41a665e420ac8" + }, + { + "assignment_type": "Module 3", + "complete": false, + "date": "2024-01-18T17:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@1da589ea848740628d173cba2c6833c8", + "link_text": "", + "title": "3.4 Closure", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@problem+block@eae438670a9844818518d52fff4d8aa4" + }, + { + "assignment_type": "Module 4", + "complete": false, + "date": "2024-01-19T04:19:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@47f62ec6b1744ba5a1350a9f69e28cdc", + "link_text": "", + "title": "4.5 Closure", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@aff4b3b4b4ad4301a6293be7f626287a" + }, + { + "assignment_type": "Module 5", + "complete": false, + "date": "2024-01-27T04:19:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@d28b2ee7d1a4425b872d62bfaeb56128", + "link_text": "", + "title": "5.1 Measuring Progress in ECD Interventions", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@b2f2e069242945728fd7e7fbc401eed5" + }, + { + "assignment_type": "Module 5", + "complete": false, + "date": "2024-01-28T14:59:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@e9875420bbb9405e95c18186718d7529", + "link_text": "", + "title": "5.2 Measuring Progress in ECD Policies", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@0d0111f05f974c1ebaf66e8262f39da4" + }, + { + "assignment_type": "Module 5", + "complete": false, + "date": "2024-01-30T14:59:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@504b2e012ec143d2a45ca5a6d04f8b22", + "link_text": "", + "title": "5.3 Challenges and Data-Driven Solutions", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@2b94eda3fe694579b941a055f3fb963a" + }, + { + "assignment_type": "Module 6", + "complete": false, + "date": "2024-01-31T01:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@3aed70e1f3354a4eb5d21d63ec44618c", + "link_text": "", + "title": "6.2 Examples of Innovations", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@c23ad5f08cd842b49d30a1c8b7ce176a" + }, + { + "assignment_type": null, + "complete": false, + "date": "2024-02-01T12:00:00Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@217e302038da4a638cc5c0eb8aa6a239", + "link_text": "", + "title": "Open Response Assessment (Self Assessment)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@217e302038da4a638cc5c0eb8aa6a239" + }, + { + "assignment_type": "Module 0", + "complete": false, + "date": "2024-02-03T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@fcc82b68eb944b83962be3fdaa004ac0", + "link_text": "", + "title": "Course Policies and Expectations", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@problem+block@c0e15c6127404c6d9f21b588342067ac" + }, + { + "assignment_type": "Module 0", + "complete": false, + "date": "2024-01-15T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@2a3ed0b868cd429ebdfbd1fe416d9c5a", + "link_text": "", + "title": "Syllabus Quiz", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@987158ea28e24f13855ff6abfb3e63e2" + }, + { + "assignment_type": "Module 1", + "complete": true, + "date": "2024-01-14T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@84141e77957c45cca6b5039da8ee679d", + "link_text": "", + "title": "1.2 Why Should we Invest in ECD Interventions?", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@bf278c64a4034e88a33e0904fe03e256" + }, + { + "assignment_type": "Module 1", + "complete": false, + "date": "2024-01-13T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@d11c050c03de4cb48b80faf011162195", + "link_text": "", + "title": "1.5: What Implementation Features Matter to Implement ECD Interventions?", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@1655c4b7d4934734b401e70babc14d28" + }, + { + "assignment_type": "Module 1", + "complete": true, + "date": "2024-01-12T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@e2d228da088648479e5fda7478c006b8", + "link_text": "", + "title": "1.6 Closing", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@c97497458d30419ea0d41f6b3688e584" + }, + { + "assignment_type": "Module 2", + "complete": true, + "date": "2024-01-11T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@ed3b874a340d4dfa9245a3d2deaee5da", + "link_text": "", + "title": "2.1 What are the active ingredients of effective ECD interventions?", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@286da9b199c24445b1685040ded994d1" + }, + { + "assignment_type": "Module 2", + "complete": false, + "date": "2024-01-10T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@ade8a7cfbf724bf5b58902bd27b613b0", + "link_text": "", + "title": "2.2 What can we learn about active ingredients from case studies of ECD interventions around the world?", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@e14daeca668d4c77a6f918b41b486195" + }, + { + "assignment_type": "Module 3", + "complete": false, + "date": "2024-01-09T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@57ee148192f046859d6c8a2c3531ab49", + "link_text": "", + "title": "3.1 Delivery and Dose", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@ded1a7f220cb403887d3f894d0171762" + }, + { + "assignment_type": "Module 3", + "complete": false, + "date": "2024-01-08T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@5be015e209ee4a44b71a10682ddb3eee", + "link_text": "", + "title": "3.2 Demand", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@e4f7b5f3cf9c498cb9d41a665e420ac8" + }, + { + "assignment_type": "Module 3", + "complete": false, + "date": "2020-01-07T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@1da589ea848740628d173cba2c6833c8", + "link_text": "", + "title": "3.4 Closure", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@problem+block@eae438670a9844818518d52fff4d8aa4" + }, + { + "assignment_type": "Module 4", + "complete": false, + "date": "2024-01-16T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@47f62ec6b1744ba5a1350a9f69e28cdc", + "link_text": "", + "title": "4.5 Closure", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@aff4b3b4b4ad4301a6293be7f626287a" + }, + { + "assignment_type": "Module 5", + "complete": false, + "date": "2024-01-17T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@d28b2ee7d1a4425b872d62bfaeb56128", + "link_text": "", + "title": "5.1 Measuring Progress in ECD Interventions", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@b2f2e069242945728fd7e7fbc401eed5" + }, + { + "assignment_type": "Module 5", + "complete": false, + "date": "2024-01-18T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@e9875420bbb9405e95c18186718d7529", + "link_text": "", + "title": "5.2 Measuring Progress in ECD Policies", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@0d0111f05f974c1ebaf66e8262f39da4" + }, + { + "assignment_type": "Module 5", + "complete": false, + "date": "2024-01-19T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@504b2e012ec143d2a45ca5a6d04f8b22", + "link_text": "", + "title": "5.3 Challenges and Data-Driven Solutions", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@2b94eda3fe694579b941a055f3fb963a" + }, + { + "assignment_type": "Module 6", + "complete": false, + "date": "2024-01-20T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@3aed70e1f3354a4eb5d21d63ec44618c", + "link_text": "", + "title": "6.2 Examples of Innovations", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@c23ad5f08cd842b49d30a1c8b7ce176a" + }, + { + "assignment_type": "Module 7", + "complete": false, + "date": "2024-01-23T15:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/course-v1:HarvardX+ECD01+1T2023/jump_to/block-v1:HarvardX+ECD01+1T2023+type@sequential+block@1aea57e69f2a40f8a25c8edd389e3608", + "link_text": "", + "title": "7.0 Course Summary", + "extra_info": null, + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@html+block@625c5399993344fdaa852a967be6f690" + }, + { + "assignment_type": null, + "complete": false, + "date": "2024-01-23T12:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@217e302038da4a638cc5c0eb8aa6a239", + "link_text": "", + "title": "Open Response Assessment (Submission)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@217e302038da4a638cc5c0eb8aa6a239" + }, + { + "assignment_type": null, + "complete": false, + "date": "2024-01-23T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103", + "link_text": "", + "title": "Practice Creating a Theory of Change (Submission)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103" + }, + { + "assignment_type": null, + "complete": false, + "date": "2024-01-24T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9", + "link_text": "", + "title": "Activity 1: Logic Model (Submission)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9" + }, + { + "assignment_type": null, + "complete": false, + "date": "2024-01-25T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@217e302038da4a638cc5c0eb8aa6a239", + "link_text": "", + "title": "Open Response Assessment (Peer Assessment)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@217e302038da4a638cc5c0eb8aa6a239" + }, + { + "assignment_type": null, + "complete": false, + "date": "2024-02-26T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@217e302038da4a638cc5c0eb8aa6a239", + "link_text": "", + "title": "Open Response Assessment (Self Assessment)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@217e302038da4a638cc5c0eb8aa6a239" + }, + { + "assignment_type": null, + "complete": false, + "date": "2024-03-27T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103", + "link_text": "", + "title": "Practice Creating a Theory of Change (Peer Assessment)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103" + }, + { + "assignment_type": null, + "complete": false, + "date": "2024-04-28T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103", + "link_text": "", + "title": "Practice Creating a Theory of Change (Self Assessment)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103" + }, + { + "assignment_type": null, + "complete": false, + "date": "2025-01-29T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": false, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9", + "link_text": "", + "title": "Activity 1: Logic Model (Peer Assessment)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9" + }, + { + "assignment_type": null, + "complete": false, + "date": "2026-01-30T09:39:37.467099Z", + "date_type": "assignment-due-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9", + "link_text": "", + "title": "Activity 1: Logic Model (Self Assessment)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9" + }, + { + "assignment_type": null, + "complete": false, + "date": "2027-02-21T12:00:00Z", + "date_type": "verified-upgrade-deadline", + "description": "You must successfully complete verification before this date to qualify for a Verified Certificate.", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103", + "link_text": "", + "title": "Practice Creating a Theory of Change (Peer Assessment)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103" + }, + { + "assignment_type": null, + "complete": false, + "date": "2027-02-21T12:00:00Z", + "date_type": "verified-upgrade-deadline", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103", + "link_text": "", + "title": "Practice Creating a Theory of Change (Self Assessment)", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@c3880270b8c7471287dd72a4b4b7e103" + }, + { + "assignment_type": null, + "complete": false, + "date": "2027-02-21T12:00:00Z", + "date_type": "certificate-available-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9", + "link_text": "", + "title": "Certificate", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9" + }, + { + "assignment_type": null, + "complete": false, + "date": "2027-02-21T12:00:00Z", + "date_type": "course-expired-date", + "description": "", + "learner_has_access": true, + "link": "https://courses.edx.org/courses/block-v1:HarvardX+ECD01+1T2023+type@course+block@course/jump_to/block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9", + "link_text": "", + "title": "Course Access Expires", + "extra_info": "This Open Response Assessment's due dates are set by your instructor and can't be shifted.", + "first_component_block_id": "block-v1:HarvardX+ECD01+1T2023+type@openassessment+block@7f7924ba31fe4dfba5aee3304fd56cc9" + }, + { + "assignment_type": null, + "complete": null, + "date": "2027-02-22T12:00:00Z", + "date_type": "course-end-date", + "description": "After this date, the course will be archived, which means you can review the course content but can no longer participate in graded assignments or work towards earning a certificate.", + "learner_has_access": true, + "link": "", + "link_text": "", + "title": "Course ends", + "extra_info": null, + "first_component_block_id": "" + } + ], + "has_ended": false, + "learner_is_full_access": true, + "user_timezone": null + } + """ +} +#endif +// swiftlint:enable all diff --git a/Course/Course/Data/Mock/CourseStructureMock.swift b/Course/Course/Data/Mock/CourseStructureMock.swift new file mode 100644 index 000000000..fe2f39e37 --- /dev/null +++ b/Course/Course/Data/Mock/CourseStructureMock.swift @@ -0,0 +1,637 @@ +// +// CourseStructureMock.swift +// Course +// +// Created by Shafqat Muneer on 1/24/24. +// + +import Foundation + +// Mark - For testing and SwiftUI preview +// swiftlint:disable all +#if DEBUG +extension CourseRepository { + static let courseStructureJson: String = """ + {"root": "block-v1:QA+comparison+2022+type@course+block@course", + "blocks": { + "block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78": { + "id": "block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78", + "block_id": "be1704c576284ba39753c6f0ea4a4c78", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78", + "type": "comparison", + "display_name": "Comparison", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296": { + "id": "block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296", + "block_id": "93acc543871e4c73bc20a72a64e93296", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296", + "type": "problem", + "display_name": "Dropdown with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b": { + "id": "block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b", + "block_id": "06c17035106e48328ebcd042babcf47b", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b", + "type": "comparison", + "display_name": "Comparison", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58": { + "id": "block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58", + "block_id": "c19e41b61db14efe9c45f1354332ae58", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58", + "type": "problem", + "display_name": "Text Input with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb": { + "id": "block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb", + "block_id": "0d96732f577b4ff68799faf8235d1bfb", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb", + "type": "problem", + "display_name": "Numerical Input with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96": { + "id": "block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96", + "block_id": "dd2e22fdf0724bd88c8b2e6b68dedd96", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96", + "type": "problem", + "display_name": "Blank Common Problem", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd": { + "id": "block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd", + "block_id": "d1e091aa305741c5bedfafed0d269efd", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd", + "type": "problem", + "display_name": "Blank Common Problem", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7": { + "id": "block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7", + "block_id": "23e10dea806345b19b77997b4fc0eea7", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7", + "type": "comparison", + "display_name": "Comparison", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904", + "block_id": "29e7eddbe8964770896e4036748c9904", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904", + "type": "vertical", + "display_name": "Unit", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@comparison+block@be1704c576284ba39753c6f0ea4a4c78", + "block-v1:QA+comparison+2022+type@problem+block@93acc543871e4c73bc20a72a64e93296", + "block-v1:QA+comparison+2022+type@comparison+block@06c17035106e48328ebcd042babcf47b", + "block-v1:QA+comparison+2022+type@problem+block@c19e41b61db14efe9c45f1354332ae58", + "block-v1:QA+comparison+2022+type@problem+block@0d96732f577b4ff68799faf8235d1bfb", + "block-v1:QA+comparison+2022+type@problem+block@dd2e22fdf0724bd88c8b2e6b68dedd96", + "block-v1:QA+comparison+2022+type@problem+block@d1e091aa305741c5bedfafed0d269efd", + "block-v1:QA+comparison+2022+type@comparison+block@23e10dea806345b19b77997b4fc0eea7" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6": { + "id": "block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6", + "block_id": "f468bb5c6e8641179e523c7fcec4e6d6", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6", + "type": "sequential", + "display_name": "Subsection", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@vertical+block@29e7eddbe8964770896e4036748c9904" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234": { + "id": "block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234", + "block_id": "eaf91d8fc70547339402043ba1a1c234", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234", + "type": "problem", + "display_name": "Dropdown with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751": { + "id": "block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751", + "block_id": "fac531c3f1f3400cb8e3b97eb2c3d751", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751", + "type": "comparison", + "display_name": "Comparison", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de": { + "id": "block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de", + "block_id": "74a1074024fe401ea305534f2241e5de", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de", + "type": "html", + "display_name": "Raw HTML", + "graded": false, + "student_view_data": { + "last_modified": "2023-05-04T19:08:07Z", + "html_data": "https://s3.eu-central-1.amazonaws.com/vso-dev-edx-sorage/htmlxblock/QA/comparison/html/74a1074024fe401ea305534f2241e5de/content_html.zip", + "size": 576, + "index_page": "index.html", + "icon_class": "other" + }, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca", + "block_id": "e5b2e105f4f947c5b76fb12c35da1eca", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca", + "type": "vertical", + "display_name": "Unit", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@problem+block@eaf91d8fc70547339402043ba1a1c234", + "block-v1:QA+comparison+2022+type@comparison+block@fac531c3f1f3400cb8e3b97eb2c3d751", + "block-v1:QA+comparison+2022+type@html+block@74a1074024fe401ea305534f2241e5de" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2": { + "id": "block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2", + "block_id": "d37cb0c5c2d24ddaacf3494760a055f2", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2", + "type": "sequential", + "display_name": "Another one subsection", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@vertical+block@e5b2e105f4f947c5b76fb12c35da1eca" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846": { + "id": "block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846", + "block_id": "abecaefe203c4c93b441d16cea3b7846", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846", + "type": "chapter", + "display_name": "Section", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@sequential+block@f468bb5c6e8641179e523c7fcec4e6d6", + "block-v1:QA+comparison+2022+type@sequential+block@d37cb0c5c2d24ddaacf3494760a055f2" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de", + "block_id": "a0c3ac29daab425f92a34b34eb2af9de", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de", + "type": "pdf", + "display_name": "PDF title", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9", + "block_id": "bcd1b0f3015b4d3696b12f65a5d682f9", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59", + "block_id": "67d805daade34bd4b6ace607e6d48f59", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974", + "block_id": "828606a51f4e44198e92f86a45be7974", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee", + "block_id": "8646c3bc2184467b86e5ef01ecd452ee", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1", + "block_id": "e2faa0e62223489e91a41700865c5fc1", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1", + "type": "vertical", + "display_name": "Unit", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@pdf+block@a0c3ac29daab425f92a34b34eb2af9de", + "block-v1:QA+comparison+2022+type@pdf+block@bcd1b0f3015b4d3696b12f65a5d682f9", + "block-v1:QA+comparison+2022+type@pdf+block@67d805daade34bd4b6ace607e6d48f59", + "block-v1:QA+comparison+2022+type@pdf+block@828606a51f4e44198e92f86a45be7974", + "block-v1:QA+comparison+2022+type@pdf+block@8646c3bc2184467b86e5ef01ecd452ee" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52": { + "id": "block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52", + "block_id": "0c5e89fa6d7a4fac8f7b26f2ca0bbe52", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52", + "type": "problem", + "display_name": "Checkboxes with Hints and Feedback", + "graded": false, + "student_view_multi_device": true, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4", + "block_id": "8ba437d8b20d416d91a2d362b0c940a4", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4", + "type": "vertical", + "display_name": "Unit", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@problem+block@0c5e89fa6d7a4fac8f7b26f2ca0bbe52" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d": { + "id": "block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d", + "block_id": "021f70794f7349998e190b060260b70d", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d", + "type": "pdf", + "display_name": "PDF", + "graded": false, + "student_view_data": { + "last_modified": "2023-04-26T08:43:45Z", + "url": "https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf", + }, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ], + "completion": 0.0 + }, + "block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1": { + "id": "block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1", + "block_id": "2c344115d3554ac58c140ec86e591aa1", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1", + "type": "vertical", + "display_name": "Unit", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@pdf+block@021f70794f7349998e190b060260b70d" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe": { + "id": "block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe", + "block_id": "6c9c6ba663b54c0eb9cbdcd0c6b4bebe", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe", + "type": "sequential", + "display_name": "Subsection", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@vertical+block@e2faa0e62223489e91a41700865c5fc1", + "block-v1:QA+comparison+2022+type@vertical+block@8ba437d8b20d416d91a2d362b0c940a4", + "block-v1:QA+comparison+2022+type@vertical+block@2c344115d3554ac58c140ec86e591aa1" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7": { + "id": "block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7", + "block_id": "d5a4f1f2f5314288aae400c270fb03f7", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7", + "type": "chapter", + "display_name": "PDF", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@sequential+block@6c9c6ba663b54c0eb9cbdcd0c6b4bebe" + ], + "completion": 0 + }, + "block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf": { + "id": "block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf", + "block_id": "7ab45affb80f4846a60648ec6aff9fbf", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf", + "type": "chapter", + "display_name": "Section", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + + ] + }, + "block-v1:QA+comparison+2022+type@course+block@course": { + "id": "block-v1:QA+comparison+2022+type@course+block@course", + "block_id": "course", + "lms_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@course+block@course", + "legacy_web_url": "https://lms.lilac-vso-dev.raccoongang.com/courses/course-v1:QA+comparison+2022/jump_to/block-v1:QA+comparison+2022+type@course+block@course?experience=legacy", + "student_view_url": "https://lms.lilac-vso-dev.raccoongang.com/xblock/block-v1:QA+comparison+2022+type@course+block@course", + "type": "course", + "display_name": "Comparison xblock test coursre", + "graded": false, + "student_view_multi_device": false, + "block_counts": { + "video": 0 + }, + "descendants": [ + "block-v1:QA+comparison+2022+type@chapter+block@abecaefe203c4c93b441d16cea3b7846", + "block-v1:QA+comparison+2022+type@chapter+block@d5a4f1f2f5314288aae400c270fb03f7", + "block-v1:QA+comparison+2022+type@chapter+block@7ab45affb80f4846a60648ec6aff9fbf" + ], + "completion": 0 + } + }, + "id": "course-v1:QA+comparison+2022", + "name": "Comparison xblock test coursre", + "number": "comparison", + "org": "QA", + "start": "2022-01-01T00:00:00Z", + "start_display": "01 january 2022 р.", + "start_type": "timestamp", + "end": null, + "courseware_access": { + "has_access": true, + "error_code": null, + "developer_message": null, + "user_message": null, + "additional_context_user_message": null, + "user_fragment": null + }, + "media": { + "image": { + "raw": "/asset-v1:QA+comparison+2022+type@asset+block@images_course_image.jpg", + "small": "/asset-v1:QA+comparison+2022+type@asset+block@images_course_image.jpg", + "large": "/asset-v1:QA+comparison+2022+type@asset+block@images_course_image.jpg" + } + }, + "certificate": { + + }, + "is_self_paced": false + } + """ +} +#endif +// swiftlint:enable all diff --git a/Course/Course/Data/Model/Data_CourseDates.swift b/Course/Course/Data/Model/Data_CourseDates.swift new file mode 100644 index 000000000..b78691020 --- /dev/null +++ b/Course/Course/Data/Model/Data_CourseDates.swift @@ -0,0 +1,90 @@ +// +// Data_CourseDates.swift +// Course +// +// Created by Muhammad Umer on 10/18/23. +// + +import Foundation +import Core + +public extension DataLayer { + struct CourseDates: Codable { + let datesBannerInfo: DatesBannerInfo? + let courseDateBlocks: [CourseDateBlock] + let hasEnded, learnerIsFullAccess: Bool + let userTimezone: String? + + enum CodingKeys: String, CodingKey { + case datesBannerInfo = "dates_banner_info" + case courseDateBlocks = "course_date_blocks" + case hasEnded = "has_ended" + case learnerIsFullAccess = "learner_is_full_access" + case userTimezone = "user_timezone" + } + } + + struct CourseDateBlock: Codable { + let assignmentType: String? + let complete: Bool? + let date, dateType, description: String + let learnerHasAccess: Bool + let link, title: String + let linkText: String? + let extraInfo: String? + let firstComponentBlockID: String + + enum CodingKeys: String, CodingKey { + case assignmentType = "assignment_type" + case complete, date + case dateType = "date_type" + case description + case learnerHasAccess = "learner_has_access" + case link + case linkText = "link_text" + case title + case extraInfo = "extra_info" + case firstComponentBlockID = "first_component_block_id" + } + } + + struct DatesBannerInfo: Codable { + let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool + let verifiedUpgradeLink: String? + + enum CodingKeys: String, CodingKey { + case missedDeadlines = "missed_deadlines" + case contentTypeGatingEnabled = "content_type_gating_enabled" + case missedGatedContent = "missed_gated_content" + case verifiedUpgradeLink = "verified_upgrade_link" + } + } +} + +public extension DataLayer.CourseDates { + var domain: CourseDates { + return CourseDates( + datesBannerInfo: DatesBannerInfo( + missedDeadlines: datesBannerInfo?.missedDeadlines ?? false, + contentTypeGatingEnabled: datesBannerInfo?.contentTypeGatingEnabled ?? false, + missedGatedContent: datesBannerInfo?.missedGatedContent ?? false, + verifiedUpgradeLink: datesBannerInfo?.verifiedUpgradeLink), + courseDateBlocks: courseDateBlocks.map { block in + CourseDateBlock( + assignmentType: block.assignmentType, + complete: block.complete, + date: Date(iso8601: block.date), + dateType: block.dateType, + description: block.description, + learnerHasAccess: block.learnerHasAccess, + link: block.link, + linkText: block.linkText ?? nil, + title: block.title, + extraInfo: block.extraInfo, + firstComponentBlockID: block.firstComponentBlockID) + }, + hasEnded: hasEnded, + learnerIsFullAccess: learnerIsFullAccess, + userTimezone: userTimezone) + } +} diff --git a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift index 4e67cdfc3..302625837 100644 --- a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift +++ b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift @@ -129,29 +129,55 @@ public extension DataLayer { } struct CourseDetailEncodedVideoData: Decodable { - public let youTube: CourseDetailYouTubeData? - public let fallback: CourseDetailYouTubeData? - + public let youTube: EncodedVideoData? + public let fallback: EncodedVideoData? + public let desktopMP4: EncodedVideoData? + public let mobileHigh: EncodedVideoData? + public let mobileLow: EncodedVideoData? + public let hls: EncodedVideoData? + public init( - youTube: CourseDetailYouTubeData?, - fallback: CourseDetailYouTubeData? + youTube: EncodedVideoData?, + fallback: EncodedVideoData?, + desktopMP4: EncodedVideoData? = nil, + mobileHigh: EncodedVideoData? = nil, + mobileLow: EncodedVideoData? = nil, + hls: EncodedVideoData? = nil ) { self.youTube = youTube self.fallback = fallback + self.desktopMP4 = desktopMP4 + self.mobileHigh = mobileHigh + self.mobileLow = mobileLow + self.hls = hls } enum CodingKeys: String, CodingKey { case youTube = "youtube" case fallback + case desktopMP4 = "desktop_mp4" + case mobileHigh = "mobile_high" + case mobileLow = "mobile_low" + case hls } } - struct CourseDetailYouTubeData: Decodable { + struct EncodedVideoData: Decodable { public let url: String? - - public init(url: String?) { + public let fileSize: Int? + public let streamPriority: Int? + + public init(url: String?, fileSize: Int?, streamPriority: Int? = nil) { self.url = url + self.fileSize = fileSize + self.streamPriority = streamPriority } - + + enum CodingKeys: String, CodingKey { + case url + case fileSize = "file_size" + case streamPriority = "stream_priority" + } + } } diff --git a/Course/Course/Data/Network/CourseEndpoint.swift b/Course/Course/Data/Network/CourseEndpoint.swift index 7b3109a9c..cdeedcd2d 100644 --- a/Course/Course/Data/Network/CourseEndpoint.swift +++ b/Course/Course/Data/Network/CourseEndpoint.swift @@ -10,49 +10,42 @@ import Core import Alamofire enum CourseEndpoint: EndPointType { - case getCourseDetail(courseID: String) case getCourseBlocks(courseID: String, userName: String) case pageHTML(pageUrlString: String) - case enrollToCourse(courseID: String) case blockCompletionRequest(username: String, courseID: String, blockID: String) case getHandouts(courseID: String) case getUpdates(courseID: String) case resumeBlock(userName: String, courseID: String) case getSubtitles(url: String, selectedLanguage: String) + case getCourseDates(courseID: String) var path: String { switch self { - case .getCourseDetail(let courseID): - return "/mobile_api_extensions/v1/courses/\(courseID)" case .getCourseBlocks: - return "/mobile_api_extensions/v1/blocks/" - case .pageHTML(pageUrlString: let url): + return "/api/mobile/v3/course_info/blocks/" + case .pageHTML(let url): return "/xblock/\(url)" - case .enrollToCourse: - return "/api/enrollment/v1/enrollment" case .blockCompletionRequest: return "/api/completion/v1/completion-batch" case let .getHandouts(courseID): return "/api/mobile/v1/course_info/\(courseID)/handouts" - case .getUpdates(courseID: let courseID): + case .getUpdates(let courseID): return "/api/mobile/v1/course_info/\(courseID)/updates" case let .resumeBlock(userName, courseID): return "/api/mobile/v1/users/\(userName)/course_status_info/\(courseID)" case let .getSubtitles(url, _): return url + case .getCourseDates(let courseID): + return "/api/course_home/v1/dates/\(courseID)" } } var httpMethod: HTTPMethod { switch self { - case .getCourseDetail: - return .get case .getCourseBlocks: return .get case .pageHTML: return .get - case .enrollToCourse: - return .post case .blockCompletionRequest: return .post case .getHandouts: @@ -63,6 +56,8 @@ enum CourseEndpoint: EndPointType { return .get case .getSubtitles: return .get + case .getCourseDates: + return .get } } @@ -72,8 +67,6 @@ enum CourseEndpoint: EndPointType { var task: HTTPTask { switch self { - case .getCourseDetail: - return .requestParameters(encoding: URLEncoding.queryString) case let .getCourseBlocks(courseID, userName): let params: [String: Encodable] = [ "username": userName, @@ -90,14 +83,6 @@ enum CourseEndpoint: EndPointType { return .requestParameters(parameters: params, encoding: URLEncoding.queryString) case .pageHTML: return .request - case .enrollToCourse(courseID: let courseID): - let params: [String: Any] = [ - "course_details": [ - "course_id": courseID, - "email_opt_in": true - ] - ] - return .requestParameters(parameters: params, encoding: JSONEncoding.default) case let .blockCompletionRequest(username, courseID, blockID): let params: [String: Any] = [ "username": username, @@ -112,11 +97,13 @@ enum CourseEndpoint: EndPointType { case .resumeBlock: return .requestParameters(encoding: JSONEncoding.default) case let .getSubtitles(_, subtitleLanguage): -// let languageCode = Locale.current.languageCode ?? "en" + // let languageCode = Locale.current.languageCode ?? "en" let params: [String: Any] = [ "lang": subtitleLanguage ] return .requestParameters(parameters: params, encoding: URLEncoding.queryString) + case .getCourseDates: + return .requestParameters(encoding: JSONEncoding.default) } } } diff --git a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents index f83d58906..2ec79a638 100644 --- a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents +++ b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -7,35 +7,51 @@ - - + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -80,4 +96,4 @@ - \ No newline at end of file + diff --git a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift b/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift index b17874645..35f8328c2 100644 --- a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift +++ b/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift @@ -9,17 +9,17 @@ import CoreData import Core public protocol CoursePersistenceProtocol { - func loadCourseDetails(courseID: String) throws -> CourseDetails - func saveCourseDetails(course: CourseDetails) func loadEnrollments() throws -> [Core.CourseItem] func saveEnrollments(items: [Core.CourseItem]) func loadCourseStructure(courseID: String) throws -> DataLayer.CourseStructure func saveCourseStructure(structure: DataLayer.CourseStructure) func saveSubtitles(url: String, subtitlesString: String) func loadSubtitles(url: String) -> String? + func saveCourseDates(courseID: String, courseDates: CourseDates) + func loadCourseDates(courseID: String) throws -> CourseDates } public final class CourseBundle { private init() {} } - \ No newline at end of file + diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift index df58f05a9..aa2acabac 100644 --- a/Course/Course/Domain/CourseInteractor.swift +++ b/Course/Course/Domain/CourseInteractor.swift @@ -10,17 +10,15 @@ import Core //sourcery: AutoMockable public protocol CourseInteractorProtocol { - func getCourseDetails(courseID: String) async throws -> CourseDetails func getCourseBlocks(courseID: String) async throws -> CourseStructure func getCourseVideoBlocks(fullStructure: CourseStructure) -> CourseStructure - func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails - func getCourseBlocksOffline(courseID: String) async throws -> CourseStructure - func enrollToCourse(courseID: String) async throws -> Bool + func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure func blockCompletionRequest(courseID: String, blockID: String) async throws func getHandouts(courseID: String) async throws -> String? func getUpdates(courseID: String) async throws -> [CourseUpdate] func resumeBlock(courseID: String) async throws -> ResumeBlock func getSubtitles(url: String, selectedLanguage: String) async throws -> [Subtitle] + func getCourseDates(courseID: String) async throws -> CourseDates } public class CourseInteractor: CourseInteractorProtocol { @@ -31,10 +29,6 @@ public class CourseInteractor: CourseInteractorProtocol { self.repository = repository } - public func getCourseDetails(courseID: String) async throws -> CourseDetails { - return try await repository.getCourseDetails(courseID: courseID) - } - public func getCourseBlocks(courseID: String) async throws -> CourseStructure { return try await repository.getCourseBlocks(courseID: courseID) } @@ -61,16 +55,8 @@ public class CourseInteractor: CourseInteractorProtocol { ) } - public func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails { - return try await repository.getCourseDetailsOffline(courseID: courseID) - } - - public func getCourseBlocksOffline(courseID: String) async throws -> CourseStructure { - return try repository.getCourseBlocksOffline(courseID: courseID) - } - - public func enrollToCourse(courseID: String) async throws -> Bool { - return try await repository.enrollToCourse(courseID: courseID) + public func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure { + return try repository.getLoadedCourseBlocks(courseID: courseID) } public func blockCompletionRequest(courseID: String, blockID: String) async throws { @@ -94,6 +80,10 @@ public class CourseInteractor: CourseInteractorProtocol { return parseSubtitles(from: result) } + public func getCourseDates(courseID: String) async throws -> CourseDates { + return try await repository.getCourseDates(courseID: courseID) + } + private func filterChapter(chapter: CourseChapter) -> CourseChapter { var newChilds = [CourseSequential]() for sequential in chapter.childs { diff --git a/Course/Course/Domain/Model/CourseDates.swift b/Course/Course/Domain/Model/CourseDates.swift new file mode 100644 index 000000000..a796a944b --- /dev/null +++ b/Course/Course/Domain/Model/CourseDates.swift @@ -0,0 +1,276 @@ +// +// CourseDates.swift +// Course +// +// Created by Muhammad Umer on 10/18/23. +// + +import Foundation +import Core + +public struct CourseDates { + let datesBannerInfo: DatesBannerInfo + let courseDateBlocks: [CourseDateBlock] + let hasEnded, learnerIsFullAccess: Bool + let userTimezone: String? + + var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { + var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] + var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] + + for block in courseDateBlocks { + let date = block.date + switch true { + case block.complete ?? false || block.blockStatus == .courseStartDate: + statusBlocks[.completed, default: []].append(block) + case date.isInPast: + statusBlocks[.pastDue, default: []].append(block) + case date.isToday: + if date < Date() { + statusBlocks[.pastDue, default: []].append(block) + } else { + statusBlocks[.today, default: []].append(block) + } + case date.isThisWeek: + statusBlocks[.thisWeek, default: []].append(block) + case date.isNextWeek: + statusBlocks[.nextWeek, default: []].append(block) + case date.isUpcoming: + statusBlocks[.upcoming, default: []].append(block) + default: + statusBlocks[.upcoming, default: []].append(block) + } + } + + for status in statusBlocks.keys { + let courseDateBlocks = statusBlocks[status] + var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:] + + for block in courseDateBlocks ?? [] { + let date = block.date + dateToCourseDateBlockDict[date, default: []].append(block) + } + statusDatesBlocks[status] = dateToCourseDateBlockDict + } + + return statusDatesBlocks + } +} + +extension Date { + static var today: Date { + return Calendar.current.startOfDay(for: Date()) + } + + static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult { + if fromDate > toDate { + return .orderedDescending + } else if fromDate < toDate { + return .orderedAscending + } + return .orderedSame + } + + var isInPast: Bool { + return Date.compare(self, to: .today) == .orderedAscending + } + + var isToday: Bool { + let calendar = Calendar.current + let selfComponents = calendar.dateComponents([.year, .month, .day], from: self) + let todayComponents = calendar.dateComponents([.year, .month, .day], from: .today) + return selfComponents == todayComponents + } + + var isInFuture: Bool { + return Date.compare(self, to: .today) == .orderedDescending + } + + var isThisWeek: Bool { + // Items due within the next 7 days (7*24 hours from now) + let calendar = Calendar.current + let nextDay = calendar.date(byAdding: .day, value: 1, to: .today) ?? .distantPast + let nextSeventhDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast + return (nextDay...nextSeventhDay).contains(self) + } + + var isNextWeek: Bool { + // Items due within the next 14 days (14*24 hours from now) + let calendar = Calendar.current + let nextEighthDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast + let nextFourteenthDay = calendar.date(byAdding: .day, value: 15, to: .today) ?? .distantPast + return (nextEighthDay...nextFourteenthDay).contains(self) + } + + var isUpcoming: Bool { + // Items due after the next 14 days (14*24 hours from now) + let calendar = Calendar.current + let nextFourteenthDay = calendar.date(byAdding: .day, value: 14, to: .today) ?? .distantPast + return Date.compare(self, to: nextFourteenthDay) == .orderedDescending + } +} + +public struct CourseDateBlock: Identifiable { + public let id: UUID = UUID() + + let assignmentType: String? + let complete: Bool? + let date: Date + let dateType, description: String + let learnerHasAccess: Bool + let link: String + let linkText: String? + let title: String + let extraInfo: String? + let firstComponentBlockID: String + + var formattedDate: String { + return date.dateToString(style: .shortWeekdayMonthDayYear) + } + + var isInPast: Bool { + return date.isInPast + } + + var isToday: Bool { + if dateType.isEmpty { + return true + } else { + return date.isToday + } + } + + var isInFuture: Bool { + return date.isInFuture + } + + var isThisWeek: Bool { + return date.isThisWeek + } + + var isNextWeek: Bool { + return date.isNextWeek + } + + var isUpcoming: Bool { + return date.isUpcoming + } + + var isAssignment: Bool { + return BlockStatus.status(of: dateType) == .assignment + } + + var isVerifiedOnly: Bool { + return !learnerHasAccess + } + + var isComplete: Bool { + return complete ?? false + } + + var isLearnerAssignment: Bool { + return learnerHasAccess && isAssignment + } + + var isPastDue: Bool { + return !isComplete && (date < .today) + } + + var isUnreleased: Bool { + return link.isEmpty + } + + var canShowLink: Bool { + return !isUnreleased && isLearnerAssignment + } + + var isAvailable: Bool { + return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) + } + + var blockStatus: BlockStatus { + if isComplete { + return .completed + } + + if !learnerHasAccess { + return .verifiedOnly + } + + if isAssignment { + if isInPast { + return isUnreleased ? .unreleased : .pastDue + } else if isToday || isInFuture { + return isUnreleased ? .unreleased : .dueNext + } + } + + return BlockStatus.status(of: dateType) + } + + var blockImage: ImageAsset? { + if !learnerHasAccess { + return CoreAssets.lockIcon + } + + if isAssignment { + return CoreAssets.assignmentIcon + } + + switch blockStatus { + case .courseStartDate, .courseEndDate: + return CoreAssets.schoolCapIcon + case .verifiedUpgradeDeadline, .verificationDeadlineDate: + return CoreAssets.calendarIcon + case .courseExpiredDate: + return CoreAssets.lockWithWatchIcon + case .certificateAvailbleDate: + return CoreAssets.certificateIcon + default: + return CoreAssets.calendarIcon + } + } +} + +public struct DatesBannerInfo { + let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool + let verifiedUpgradeLink: String? +} + +public enum BlockStatus { + case completed + case pastDue + case dueNext + case unreleased + case verifiedOnly + case assignment + case verifiedUpgradeDeadline + case courseExpiredDate + case verificationDeadlineDate + case certificateAvailbleDate + case courseStartDate + case courseEndDate + case event + + static func status(of type: String) -> BlockStatus { + switch type { + case "assignment-due-date": return .assignment + case "verified-upgrade-deadline": return .verifiedUpgradeDeadline + case "course-expired-date": return .courseExpiredDate + case "verification-deadline-date": return .verificationDeadlineDate + case "certificate-available-date": return .certificateAvailbleDate + case "course-start-date": return .courseStartDate + case "course-end-date": return .courseEndDate + default: return .event + } + } +} + +public enum CompletionStatus: String { + case completed = "Completed" + case pastDue = "Past Due" + case today = "Today" + case thisWeek = "This Week" + case nextWeek = "Next Week" + case upcoming = "Upcoming" +} diff --git a/Course/Course/Presentation/Container/BaseCourseViewModel.swift b/Course/Course/Presentation/Container/BaseCourseViewModel.swift index b1fa3f069..04cd5a05a 100644 --- a/Course/Course/Presentation/Container/BaseCourseViewModel.swift +++ b/Course/Course/Presentation/Container/BaseCourseViewModel.swift @@ -18,13 +18,4 @@ open class BaseCourseViewModel: ObservableObject { init(manager: DownloadManagerProtocol) { self.manager = manager } - - func onBackground() { - manager.pauseDownloading() - } - - func onForeground() { - try? manager.resumeDownloading() - } - } diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 574f71b81..2e154ba69 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -9,19 +9,57 @@ import SwiftUI import Core import Discussion import Swinject +import Theme public struct CourseContainerView: View { - enum CourseTab { + enum CourseTab: Int, CaseIterable, Identifiable { + var id: Int { + rawValue + } + case course case videos + case dates case discussion case handounds + + var title: String { + switch self { + case .course: + return CourseLocalization.CourseContainer.course + case .videos: + return CourseLocalization.CourseContainer.videos + case .dates: + return CourseLocalization.CourseContainer.dates + case .discussion: + return CourseLocalization.CourseContainer.discussions + case .handounds: + return CourseLocalization.CourseContainer.handouts + } + } + + var image: Image { + switch self { + case .course: + return CoreAssets.bookCircle.swiftUIImage.renderingMode(.template) + case .videos: + return CoreAssets.videoCircle.swiftUIImage.renderingMode(.template) + case .dates: + return Image(systemName: "calendar").renderingMode(.template) + case .discussion: + return CoreAssets.bubbleLeftCircle.swiftUIImage.renderingMode(.template) + case .handounds: + return CoreAssets.docCircle.swiftUIImage.renderingMode(.template) + } + } + } @ObservedObject private var viewModel: CourseContainerViewModel - @State private var selection: CourseTab = .course + @State private var selection: Int = CourseTab.course.rawValue + @State private var isAnimatingForTap: Bool = false private var courseID: String private var title: String @@ -40,88 +78,154 @@ public struct CourseContainerView: View { public var body: some View { ZStack(alignment: .top) { - if let courseStart = viewModel.courseStart { - if courseStart > Date() { + content + } + .navigationBarHidden(false) + .navigationBarBackButtonHidden(false) + .navigationTitle(titleBar()) + .onChange(of: selection, perform: didSelect) + .background(Theme.Colors.background) + } + + @ViewBuilder + private var content: some View { + if let courseStart = viewModel.courseStart { + if courseStart > Date() { + CourseOutlineView( + viewModel: viewModel, + title: title, + courseID: courseID, + isVideo: false + ) + } else { + VStack(spacing: 0) { + if viewModel.config.uiComponents.courseTopTabBarEnabled { + topTabBar + } + tabs + } + } + } + } + + private var topTabBar: some View { + ScrollSlidingTabBar( + selection: $selection, + tabs: CourseTab.allCases.map { $0.title } + ) { newValue in + isAnimatingForTap = true + selection = newValue + DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .milliseconds(300))) { + isAnimatingForTap = false + } + } + } + + private var tabs: some View { + TabView(selection: $selection) { + ForEach(CourseTab.allCases) { tab in + switch tab { + case .course: CourseOutlineView( viewModel: viewModel, title: title, courseID: courseID, isVideo: false ) - } else { - TabView(selection: $selection) { - CourseOutlineView( - viewModel: self.viewModel, - title: title, - courseID: courseID, - isVideo: false - ) - .tabItem { - CoreAssets.bookCircle.swiftUIImage.renderingMode(.template) - Text(CourseLocalization.CourseContainer.course) - } - .tag(CourseTab.course) - - CourseOutlineView( - viewModel: self.viewModel, - title: title, - courseID: courseID, - isVideo: true - ) - .tabItem { - CoreAssets.videoCircle.swiftUIImage.renderingMode(.template) - Text(CourseLocalization.CourseContainer.videos) - } - .tag(CourseTab.videos) - - DiscussionTopicsView(courseID: courseID, - viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self, - argument: title)!, - router: Container.shared.resolve(DiscussionRouter.self)!) - .tabItem { - CoreAssets.bubbleLeftCircle.swiftUIImage.renderingMode(.template) - Text(CourseLocalization.CourseContainer.discussion) - } - .tag(CourseTab.discussion) - - HandoutsView(courseID: courseID, - viewModel: Container.shared.resolve(HandoutsViewModel.self, argument: courseID)!) - .tabItem { - CoreAssets.docCircle.swiftUIImage.renderingMode(.template) - Text(CourseLocalization.CourseContainer.handouts) - } - .tag(CourseTab.handounds) + .tabItem { + tab.image + Text(tab.title) } - .onFirstAppear { - Task { - await viewModel.tryToRefreshCookies() - } + .tag(tab) + .accentColor(Theme.Colors.accentColor) + case .videos: + CourseOutlineView( + viewModel: viewModel, + title: title, + courseID: courseID, + isVideo: true + ) + .tabItem { + tab.image + Text(tab.title) } + .tag(tab) + .accentColor(Theme.Colors.accentColor) + case .dates: + CourseDatesView( + courseID: courseID, + viewModel: Container.shared.resolve(CourseDatesViewModel.self, + argument: courseID)! + ) + .tabItem { + tab.image + Text(tab.title) + } + .tag(tab) + .accentColor(Theme.Colors.accentColor) + case .discussion: + DiscussionTopicsView( + courseID: courseID, + viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self, + argument: title)!, + router: Container.shared.resolve(DiscussionRouter.self)! + ) + .tabItem { + tab.image + Text(tab.title) + } + .tag(tab) + .accentColor(Theme.Colors.accentColor) + case .handounds: + HandoutsView( + courseID: courseID, + viewModel: Container.shared.resolve(HandoutsViewModel.self, argument: courseID)! + ) + .tabItem { + tab.image + Text(tab.title) + } + .tag(tab) + .accentColor(Theme.Colors.accentColor) } } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(titleBar()) - .onChange(of: selection, perform: { selection in + .if(viewModel.config.uiComponents.courseTopTabBarEnabled) { view in + view + .tabViewStyle(.page(indexDisplayMode: .never)) + .animation(.default, value: selection) + } + .onFirstAppear { + Task { + await viewModel.tryToRefreshCookies() + } + } + } + + private func didSelect(_ selection: Int) { + CourseTab(rawValue: selection).flatMap { viewModel.trackSelectedTab( - selection: selection, + selection: $0, courseId: courseID, courseName: title ) - }) + } } - + private func titleBar() -> String { - switch selection { + switch CourseTab(rawValue: selection) { case .course: return self.title case .videos: return self.title + case .dates: + return CourseLocalization.CourseContainer.dates case .discussion: return DiscussionLocalization.title case .handounds: return CourseLocalization.CourseContainer.handouts + default: + return "" } } } @@ -138,6 +242,7 @@ struct CourseScreensView_Previews: PreviewProvider { config: ConfigMock(), connectivity: Connectivity(), manager: DownloadManagerMock(), + storage: CourseStorageMock(), isActive: true, courseStart: nil, courseEnd: nil, diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 938c187a0..ce5463cd9 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -12,13 +12,16 @@ import Combine public class CourseContainerViewModel: BaseCourseViewModel { + @Published private(set) var isShowProgress = false @Published var courseStructure: CourseStructure? @Published var courseVideosStructure: CourseStructure? - @Published private(set) var isShowProgress = false @Published var showError: Bool = false - @Published var downloadState: [String: DownloadViewState] = [:] + @Published var sequentialsDownloadState: [String: DownloadViewState] = [:] + @Published private(set) var downloadableVerticals: Set = [] @Published var continueWith: ContinueWith? - + @Published var userSettings: UserSettings? + @Published var isInternetAvaliable: Bool = true + var errorMessage: String? { didSet { withAnimation { @@ -28,7 +31,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { } let router: CourseRouter - let config: Config + let config: ConfigProtocol let connectivity: ConnectivityProtocol let isActive: Bool? @@ -36,19 +39,24 @@ public class CourseContainerViewModel: BaseCourseViewModel { let courseEnd: Date? let enrollmentStart: Date? let enrollmentEnd: Date? - + + var courseDownloadTasks: [DownloadDataTask] = [] + private(set) var waitingDownloads: [CourseBlock]? + private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol private let analytics: CourseAnalytics - + private(set) var storage: CourseStorage + public init( interactor: CourseInteractorProtocol, authInteractor: AuthInteractorProtocol, router: CourseRouter, analytics: CourseAnalytics, - config: Config, + config: ConfigProtocol, connectivity: ConnectivityProtocol, manager: DownloadManagerProtocol, + storage: CourseStorage, isActive: Bool?, courseStart: Date?, courseEnd: Date?, @@ -66,17 +74,13 @@ public class CourseContainerViewModel: BaseCourseViewModel { self.courseEnd = courseEnd self.enrollmentStart = enrollmentStart self.enrollmentEnd = enrollmentEnd - + self.storage = storage + self.userSettings = storage.userSettings + self.isInternetAvaliable = connectivity.isInternetAvaliable + super.init(manager: manager) - - manager.publisher() - .sink(receiveValue: { [weak self] _ in - guard let self else { return } - DispatchQueue.main.async { - self.setDownloadsStates() - } - }) - .store(in: &cancellables) + + addObservers() } @MainActor @@ -85,7 +89,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { if courseStart < Date() { isShowProgress = withProgress do { - if connectivity.isInternetAvaliable { + if isInternetAvaliable { courseStructure = try await interactor.getCourseBlocks(courseID: courseID) isShowProgress = false if let courseStructure { @@ -98,10 +102,10 @@ public class CourseContainerViewModel: BaseCourseViewModel { } } } else { - courseStructure = try await interactor.getCourseBlocksOffline(courseID: courseID) + courseStructure = try await interactor.getLoadedCourseBlocks(courseID: courseID) } courseVideosStructure = interactor.getCourseVideoBlocks(fullStructure: courseStructure!) - setDownloadsStates() + await setDownloadsStates() isShowProgress = false } catch let error { @@ -115,7 +119,12 @@ public class CourseContainerViewModel: BaseCourseViewModel { } } } - + + func update(downloadQuality: DownloadQuality) { + storage.userSettings?.downloadQuality = downloadQuality + userSettings = storage.userSettings + } + @MainActor func tryToRefreshCookies() async { try? await authInteractor.getCookies(force: false) @@ -129,32 +138,46 @@ public class CourseContainerViewModel: BaseCourseViewModel { courseStructure: courseStructure ) } - - func onDownloadViewTap(chapter: CourseChapter, blockId: String, state: DownloadViewState) { - let blocks = chapter.childs - .first(where: { $0.id == blockId })?.childs - .flatMap { $0.childs } - .filter { $0.isDownloadable } ?? [] - + + @MainActor + func onDownloadViewTap(chapter: CourseChapter, blockId: String, state: DownloadViewState) async { + guard let sequential = chapter.childs + .first(where: { $0.id == blockId }) else { + return + } + + let blocks = sequential.childs.flatMap { $0.childs } + .filter { $0.isDownloadable } + + if state == .available, isShowedAllowLargeDownloadAlert(blocks: blocks) { + return + } + + await download(state: state, blocks: blocks) + } + + func verticalsBlocksDownloadable(by courseSequential: CourseSequential) -> [CourseBlock] { + let verticals = downloadableVerticals.filter { verticalState in + courseSequential.childs.contains(where: { item in + return verticalState.vertical.id == item.id + }) + } + return verticals.flatMap { $0.vertical.childs.filter { $0.isDownloadable } } + } + + func continueDownload() { + guard let blocks = waitingDownloads else { + return + } do { - switch state { - case .available: - try manager.addToDownloadQueue(blocks: blocks) - downloadState[blockId] = .downloading - case .downloading: - try manager.cancelDownloading(courseId: courseStructure?.id ?? "", blocks: blocks) - downloadState[blockId] = .available - case .finished: - manager.deleteFile(blocks: blocks) - downloadState[blockId] = .available - } + try manager.addToDownloadQueue(blocks: blocks) } catch let error { if error is NoWiFiError { errorMessage = CoreLocalization.Error.wifi } } } - + func trackSelectedTab( selection: CourseContainerView.CourseTab, courseId: String, @@ -165,13 +188,28 @@ public class CourseContainerViewModel: BaseCourseViewModel { analytics.courseOutlineCourseTabClicked(courseId: courseId, courseName: courseName) case .videos: analytics.courseOutlineVideosTabClicked(courseId: courseId, courseName: courseName) + case .dates: + analytics.courseOutlineDatesTabClicked(courseId: courseId, courseName: courseName) case .discussion: analytics.courseOutlineDiscussionTabClicked(courseId: courseId, courseName: courseName) case .handounds: analytics.courseOutlineHandoutsTabClicked(courseId: courseId, courseName: courseName) } } - + + func trackVerticalClicked( + courseId: String, + courseName: String, + vertical: CourseVertical + ) { + analytics.verticalClicked( + courseId: courseId, + courseName: courseName, + blockId: vertical.blockId, + blockName: vertical.displayName + ) + } + func trackSequentialClicked(_ sequential: CourseSequential) { guard let course = courseStructure else { return } analytics.sequentialClicked( @@ -190,40 +228,158 @@ public class CourseContainerViewModel: BaseCourseViewModel { blockId: blockId ) } - + + func completeBlock( + chapterID: String, + sequentialID: String, + verticalID: String, + blockID: String + ) { + guard let chapterIndex = courseStructure? + .childs.firstIndex(where: { $0.id == chapterID }) else { + return + } + guard let sequentialIndex = courseStructure? + .childs[chapterIndex] + .childs.firstIndex(where: { $0.id == sequentialID }) else { + return + } + + guard let verticalIndex = courseStructure? + .childs[chapterIndex] + .childs[sequentialIndex] + .childs.firstIndex(where: { $0.id == verticalID }) else { + return + } + + guard let blockIndex = courseStructure? + .childs[chapterIndex] + .childs[sequentialIndex] + .childs[verticalIndex] + .childs.firstIndex(where: { $0.id == blockID }) else { + return + } + + courseStructure? + .childs[chapterIndex] + .childs[sequentialIndex] + .childs[verticalIndex] + .childs[blockIndex].completion = 1 + courseStructure.map { + courseVideosStructure = interactor.getCourseVideoBlocks(fullStructure: $0) + } + } + + func hasVideoForDowbloads() -> Bool { + guard let courseVideosStructure = courseVideosStructure else { + return false + } + return courseVideosStructure.childs + .flatMap { $0.childs } + .contains(where: { $0.isDownloadable }) + } + + func isAllDownloading() -> Bool { + let totalCount = downloadableVerticals.count + let downloadingCount = downloadableVerticals.filter { $0.state == .downloading }.count + let finishedCount = downloadableVerticals.filter { $0.state == .finished }.count + if finishedCount == totalCount { return false } + return totalCount - finishedCount == downloadingCount + } + @MainActor - private func setDownloadsStates() { + func download(state: DownloadViewState, blocks: [CourseBlock]) async { + do { + switch state { + case .available: + try manager.addToDownloadQueue(blocks: blocks) + case .downloading: + try await manager.cancelDownloading(courseId: courseStructure?.id ?? "", blocks: blocks) + case .finished: + await manager.deleteFile(blocks: blocks) + } + } catch let error { + if error is NoWiFiError { + errorMessage = CoreLocalization.Error.wifi + } + } + } + + @MainActor + func isShowedAllowLargeDownloadAlert(blocks: [CourseBlock]) -> Bool { + waitingDownloads = nil + if storage.allowedDownloadLargeFile == false, manager.isLargeVideosSize(blocks: blocks) { + waitingDownloads = blocks + router.presentAlert( + alertTitle: CourseLocalization.Download.download, + alertMessage: CourseLocalization.Download.downloadLargeFileMessage, + positiveAction: CourseLocalization.Alert.accept, + onCloseTapped: { + self.router.dismiss(animated: true) + }, + okTapped: { + self.continueDownload() + self.router.dismiss(animated: true) + }, + type: .default(positiveAction: CourseLocalization.Alert.accept, image: nil) + ) + return true + } + return false + } + + @MainActor + func downloadableBlocks(from sequential: CourseSequential) -> [CourseBlock] { + let verticals = sequential.childs + let blocks = verticals + .flatMap { $0.childs } + .filter { $0.isDownloadable } + return blocks + } + + @MainActor + func setDownloadsStates() async { guard let course = courseStructure else { return } - let downloads = manager.getDownloadsForCourse(course.id) - var states: [String: DownloadViewState] = [:] + courseDownloadTasks = await manager.getDownloadTasksForCourse(course.id) + downloadableVerticals = [] + var sequentialsStates: [String: DownloadViewState] = [:] for chapter in course.childs { for sequential in chapter.childs where sequential.isDownloadable { - var childs: [DownloadViewState] = [] + var sequentialsChilds: [DownloadViewState] = [] for vertical in sequential.childs where vertical.isDownloadable { + var verticalsChilds: [DownloadViewState] = [] for block in vertical.childs where block.isDownloadable { - if let download = downloads.first(where: { $0.id == block.id }) { + if let download = courseDownloadTasks.first(where: { $0.id == block.id }) { switch download.state { case .waiting, .inProgress: - childs.append(.downloading) - case .paused: - childs.append(.available) + sequentialsChilds.append(.downloading) + verticalsChilds.append(.downloading) case .finished: - childs.append(.finished) + sequentialsChilds.append(.finished) + verticalsChilds.append(.finished) } } else { - childs.append(.available) + sequentialsChilds.append(.available) + verticalsChilds.append(.available) } } + if verticalsChilds.first(where: { $0 == .downloading }) != nil { + downloadableVerticals.insert(.init(vertical: vertical, state: .downloading)) + } else if verticalsChilds.allSatisfy({ $0 == .finished }) { + downloadableVerticals.insert(.init(vertical: vertical, state: .finished)) + } else { + downloadableVerticals.insert(.init(vertical: vertical, state: .available)) + } } - if childs.first(where: { $0 == .downloading }) != nil { - states[sequential.id] = .downloading - } else if childs.allSatisfy({ $0 == .finished }) { - states[sequential.id] = .finished + if sequentialsChilds.first(where: { $0 == .downloading }) != nil { + sequentialsStates[sequential.id] = .downloading + } else if sequentialsChilds.allSatisfy({ $0 == .finished }) { + sequentialsStates[sequential.id] = .finished } else { - states[sequential.id] = .available + sequentialsStates[sequential.id] = .available } } - self.downloadState = states + self.sequentialsDownloadState = sequentialsStates } } @@ -238,7 +394,8 @@ public class CourseContainerViewModel: BaseCourseViewModel { return ContinueWith( chapterIndex: chapterIndex, sequentialIndex: sequentialIndex, - verticalIndex: verticalIndex + verticalIndex: verticalIndex, + lastVisitedBlockId: block.id ) } } @@ -246,4 +403,33 @@ public class CourseContainerViewModel: BaseCourseViewModel { } return nil } + + private func addObservers() { + manager.eventPublisher() + .sink { [weak self] state in + guard let self else { return } + if case .progress = state { return } + Task(priority: .background) { + debugLog(state, "--- state ---") + await self.setDownloadsStates() + } + } + .store(in: &cancellables) + + connectivity.internetReachableSubject + .sink { [weak self] _ in + guard let self else { return } + self.isInternetAvaliable = self.connectivity.isInternetAvaliable + } + .store(in: &cancellables) + } +} + +struct VerticalsDownloadState: Hashable { + let vertical: CourseVertical + let state: DownloadViewState + + var downloadableBlocks: [CourseBlock] { + vertical.childs.filter { $0.isDownloadable } + } } diff --git a/Course/Course/Presentation/CourseAnalytics.swift b/Course/Course/Presentation/CourseAnalytics.swift index 6ad6e0389..ca7e66db7 100644 --- a/Course/Course/Presentation/CourseAnalytics.swift +++ b/Course/Course/Presentation/CourseAnalytics.swift @@ -9,9 +9,6 @@ import Foundation //sourcery: AutoMockable public protocol CourseAnalytics { - func courseEnrollClicked(courseId: String, courseName: String) - func courseEnrollSuccess(courseId: String, courseName: String) - func viewCourseClicked(courseId: String, courseName: String) func resumeCourseTapped(courseId: String, courseName: String, blockId: String) func sequentialClicked(courseId: String, courseName: String, blockId: String, blockName: String) func verticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) @@ -22,15 +19,13 @@ public protocol CourseAnalytics { func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) func courseOutlineCourseTabClicked(courseId: String, courseName: String) func courseOutlineVideosTabClicked(courseId: String, courseName: String) + func courseOutlineDatesTabClicked(courseId: String, courseName: String) func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) } #if DEBUG class CourseAnalyticsMock: CourseAnalytics { - public func courseEnrollClicked(courseId: String, courseName: String) {} - public func courseEnrollSuccess(courseId: String, courseName: String) {} - public func viewCourseClicked(courseId: String, courseName: String) {} public func resumeCourseTapped(courseId: String, courseName: String, blockId: String) {} public func sequentialClicked(courseId: String, courseName: String, blockId: String, blockName: String) {} public func verticalClicked(courseId: String, courseName: String, blockId: String, blockName: String) {} @@ -46,6 +41,7 @@ class CourseAnalyticsMock: CourseAnalytics { public func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) {} public func courseOutlineCourseTabClicked(courseId: String, courseName: String) {} public func courseOutlineVideosTabClicked(courseId: String, courseName: String) {} + public func courseOutlineDatesTabClicked(courseId: String, courseName: String) {} public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) {} public func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) {} } diff --git a/Course/Course/Presentation/CourseRouter.swift b/Course/Course/Presentation/CourseRouter.swift index 2c9a60982..35619bc2d 100644 --- a/Course/Course/Presentation/CourseRouter.swift +++ b/Course/Course/Presentation/CourseRouter.swift @@ -10,15 +10,7 @@ import Core public protocol CourseRouter: BaseRouter { - func showCourseScreens( - courseID: String, - isActive: Bool?, - courseStart: Date?, - courseEnd: Date?, - enrollmentStart: Date?, - enrollmentEnd: Date?, - title: String - ) + func presentAppReview() func showCourseUnit( courseName: String, @@ -39,7 +31,8 @@ public protocol CourseRouter: BaseRouter { verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, - sequentialIndex: Int + sequentialIndex: Int, + animated: Bool ) func showCourseVerticalView( @@ -57,6 +50,11 @@ public protocol CourseRouter: BaseRouter { router: Course.CourseRouter, cssInjector: CSSInjector ) + + func showCourseComponent( + componentID: String, + courseStructure: CourseStructure + ) } // Mark - For testing and SwiftUI preview @@ -65,15 +63,7 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { public override init() {} - public func showCourseScreens( - courseID: String, - isActive: Bool?, - courseStart: Date?, - courseEnd: Date?, - enrollmentStart: Date?, - enrollmentEnd: Date?, - title: String - ) {} + public func presentAppReview() {} public func showCourseUnit( courseName: String, @@ -94,7 +84,8 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, - sequentialIndex: Int + sequentialIndex: Int, + animated: Bool ) {} public func showCourseVerticalView( @@ -113,5 +104,10 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { cssInjector: CSSInjector ) {} + public func showCourseComponent( + componentID: String, + courseStructure: CourseStructure + ) {} + } #endif diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift new file mode 100644 index 000000000..b49356a70 --- /dev/null +++ b/Course/Course/Presentation/Dates/CourseDatesView.swift @@ -0,0 +1,388 @@ +// +// CourseDatesView.swift +// Discussion +// +// Created by Muhammad Umer on 10/17/23. +// + +import Foundation +import SwiftUI +import Core +import Theme + +public struct CourseDatesView: View { + + private let courseID: String + + @StateObject + private var viewModel: CourseDatesViewModel + + public init( + courseID: String, + viewModel: CourseDatesViewModel + ) { + self.courseID = courseID + self._viewModel = StateObject(wrappedValue: viewModel) + } + + public var body: some View { + ZStack { + VStack(alignment: .center) { + if viewModel.isShowProgress { + HStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } + } else if let courseDates = viewModel.courseDates, !courseDates.courseDateBlocks.isEmpty { + CourseDateListView(viewModel: viewModel, courseDates: courseDates) + .padding(.top, 10) + } + } + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + } + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + } + .onFirstAppear { + Task { + await viewModel.getCourseDates(courseID: courseID) + } + } + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct Line: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: rect.midX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY)) + return path + } +} + +struct TimeLineView: View { + let status: CompletionStatus + + var body: some View { + ZStack(alignment: .top) { + VStack { + Line() + .stroke(style: StrokeStyle(lineWidth: 8, lineCap: .round, lineJoin: .round)) + .frame(maxHeight: .infinity, alignment: .top) + .padding(.top, 0) + .foregroundColor(status.foregroundColor) + } + } + .frame(width: 16) + } +} + +struct CourseDateListView: View { + @ObservedObject var viewModel: CourseDatesViewModel + @State private var isExpanded = false + var courseDates: CourseDates + + var body: some View { + VStack { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(viewModel.sortedStatuses), id: \.self) { status in + let courseDateBlockDict = courseDates.statusDatesBlocks[status]! + if status == .completed { + CompletedBlocks( + isExpanded: $isExpanded, + courseDateBlockDict: courseDateBlockDict, + viewModel: viewModel + ) + } else { + Text(status.rawValue) + .font(Theme.Fonts.titleSmall) + .padding(.top, 10) + .padding(.bottom, 10) + HStack { + TimeLineView(status: status) + .padding(.bottom, 15) + VStack(alignment: .leading) { + ForEach(courseDateBlockDict.keys.sorted(), id: \.self) { date in + let blocks = courseDateBlockDict[date]! + let block = blocks[0] + Text(block.formattedDate) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textPrimary) + BlockStatusView( + viewModel: viewModel, + block: block, + blocks: blocks + ) + } + } + } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.vertical, 5) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } +} + +struct CompletedBlocks: View { + @Binding var isExpanded: Bool + let courseDateBlockDict: [Date: [CourseDateBlock]] + let viewModel: CourseDatesViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + // Toggle button to expand/collapse the cell + Button(action: { + withAnimation { + isExpanded.toggle() + } + }) { + HStack { + VStack(alignment: .leading) { + Text(CompletionStatus.completed.rawValue) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textPrimary) + + if !isExpanded { + let totalCount = courseDateBlockDict.values.reduce(0) { $0 + $1.count } + let itemsHidden = totalCount == 1 ? + CoreLocalization.CourseDates.itemHidden : + CoreLocalization.CourseDates.itemsHidden + Text("\(totalCount) \(itemsHidden)") + .font(Theme.Fonts.labelMedium) + .foregroundColor(Theme.Colors.textPrimary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 16) + .padding(.vertical, 8) + + Image(systemName: "chevron.down") + .labelStyle(.iconOnly) + .dropdownArrowRotationAnimation(value: isExpanded) + .foregroundColor(Theme.Colors.textPrimary) + .padding() + } + } + + // Your expandable content goes here + if isExpanded { + VStack(alignment: .leading) { + ForEach(courseDateBlockDict.keys.sorted(), id: \.self) { date in + let blocks = courseDateBlockDict[date]! + let block = blocks[0] + + Spacer() + Text(block.formattedDate) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textPrimary) + + ForEach(blocks) { block in + HStack(alignment: .top) { + block.blockImage?.swiftUIImage + .foregroundColor(Theme.Colors.textPrimary) + StyleBlock(block: block, viewModel: viewModel) + .padding(.bottom, 15) + Spacer() + if block.canShowLink && !block.firstComponentBlockID.isEmpty { + Image(systemName: "chevron.right") + .resizable() + .scaledToFit() + .frame(width: 6.55, height: 11.15) + .labelStyle(.iconOnly) + .foregroundColor(Theme.Colors.textPrimary) + } + } + .padding(.trailing, 15) + } + } + } + .padding(.bottom, 15) + .padding(.leading, 16) + } + + } + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.datesSectionStroke, lineWidth: 2) + ) + .background(Theme.Colors.datesSectionBackground) + } +} + +struct BlockStatusView: View { + let viewModel: CourseDatesViewModel + let block: CourseDateBlock + let blocks: [CourseDateBlock] + + var body: some View { + VStack(alignment: .leading) { + ForEach(blocks) { block in + HStack(alignment: .top) { + block.blockImage?.swiftUIImage + .foregroundColor(Theme.Colors.textPrimary) + StyleBlock(block: block, viewModel: viewModel) + .padding(.bottom, 15) + Spacer() + if block.canShowLink && !block.firstComponentBlockID.isEmpty { + Image(systemName: "chevron.right") + .resizable() + .scaledToFit() + .frame(width: 6.55, height: 11.15) + .labelStyle(.iconOnly) + .foregroundColor(Theme.Colors.textPrimary) + } + } + .padding(.trailing, 15) + } + .padding(.top, 0.2) + } + } + + func applyStyle(string: String, forgroundColor: Color, backgroundColor: Color) -> AttributedString { + var attributedString = AttributedString(string) + attributedString.font = Theme.Fonts.bodySmall + attributedString.foregroundColor = forgroundColor + attributedString.backgroundColor = backgroundColor + return attributedString + } +} + +struct StyleBlock: View { + let block: CourseDateBlock + let viewModel: CourseDatesViewModel + + var body: some View { + VStack(alignment: .leading) { + styleBlock(block: block) + if !block.description.isEmpty { + Text(block.description) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.thisWeekTimelineColor) + .fixedSize(horizontal: false, vertical: true) + } + } + } + + func styleBlock(block: CourseDateBlock) -> some View { + var attributedString = AttributedString("") + + if let prefix = block.assignmentType, !prefix.isEmpty { + attributedString += AttributedString("\(prefix): ") + } + + attributedString += styleTitle(block: block) + + return Text(attributedString) + .font(Theme.Fonts.titleSmall) + .lineLimit(1) + .foregroundStyle({ + if block.isAssignment { + return block.isAvailable ? Theme.Colors.textPrimary : Color.gray.opacity(0.6) + } else { + return Theme.Colors.textPrimary + } + }()) + .onTapGesture { + if block.canShowLink && !block.firstComponentBlockID.isEmpty { + Task { + await viewModel.showCourseDetails(componentID: block.firstComponentBlockID) + } + } + } + } + + func styleTitle(block: CourseDateBlock) -> AttributedString { + var attributedString = AttributedString(block.title) + attributedString.font = Theme.Fonts.titleSmall + return attributedString + } +} + +fileprivate extension BlockStatus { + var title: String { + switch self { + case .completed: return CoreLocalization.CourseDates.completed + case .pastDue: return CoreLocalization.CourseDates.pastDue + case .dueNext: return CoreLocalization.CourseDates.dueNext + case .unreleased: return CoreLocalization.CourseDates.unreleased + case .verifiedOnly: return CoreLocalization.CourseDates.verifiedOnly + default: return "" + } + } + + var foregroundColor: Color { + switch self { + case .completed: return Color.white + case .verifiedOnly: return Color.white + case .pastDue: return Color.black + case .dueNext: return Color.white + default: return Color.white.opacity(0) + } + } + + var backgroundColor: Color { + switch self { + case .completed: return Color.black.opacity(0.5) + case .verifiedOnly: return Color.black.opacity(0.5) + case .pastDue: return Color.gray.opacity(0.4) + case .dueNext: return Color.black.opacity(0.5) + default: return Color.white.opacity(0) + } + } +} + +fileprivate extension CompletionStatus { + var foregroundColor: Color { + switch self { + case .pastDue: return Theme.Colors.pastDueTimelineColor + case .today: return Theme.Colors.todayTimelineColor + case .thisWeek: return Theme.Colors.thisWeekTimelineColor + case .nextWeek: return Theme.Colors.nextWeekTimelineColor + case .upcoming: return Theme.Colors.upcomingTimelineColor + default: return Color.white.opacity(0) + } + } +} + +fileprivate extension AttributedString { + mutating func appendSpaces(_ count: Int = 1) { + self += AttributedString(String(repeating: " ", count: count)) + } +} + +#if DEBUG +struct CourseDatesView_Previews: PreviewProvider { + static var previews: some View { + let viewModel = CourseDatesViewModel( + interactor: CourseInteractor(repository: CourseRepositoryMock()), + router: CourseRouterMock(), + cssInjector: CSSInjectorMock(), + connectivity: Connectivity(), + courseID: "") + + CourseDatesView( + courseID: "", + viewModel: viewModel) + } +} +#endif diff --git a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift new file mode 100644 index 000000000..3ee731bd0 --- /dev/null +++ b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift @@ -0,0 +1,94 @@ +// +// CourseDatesViewModel.swift +// Course +// +// Created by Muhammad Umer on 10/18/23. +// + +import Foundation +import Core +import SwiftUI + +public class CourseDatesViewModel: ObservableObject { + + @Published private(set) var isShowProgress = false + @Published var showError: Bool = false + @Published var courseDates: CourseDates? + + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + private let interactor: CourseInteractorProtocol + let cssInjector: CSSInjector + let router: CourseRouter + let connectivity: ConnectivityProtocol + let courseID: String + + public init( + interactor: CourseInteractorProtocol, + router: CourseRouter, + cssInjector: CSSInjector, + connectivity: ConnectivityProtocol, + courseID: String + ) { + self.interactor = interactor + self.router = router + self.cssInjector = cssInjector + self.connectivity = connectivity + self.courseID = courseID + } + + var sortedStatuses: [CompletionStatus] { + let desiredSequence = [ + CompletionStatus.completed, + CompletionStatus.pastDue, + CompletionStatus.today, + CompletionStatus.thisWeek, + CompletionStatus.nextWeek, + CompletionStatus.upcoming + ] + + // Filter out keys that don't exist in the dictionary + let filteredKeys = desiredSequence.filter { + courseDates?.statusDatesBlocks.keys.contains($0) ?? false } + return filteredKeys + } + + @MainActor + func getCourseDates(courseID: String) async { + isShowProgress = true + do { + courseDates = try await interactor.getCourseDates(courseID: courseID) + if courseDates?.courseDateBlocks == nil { + isShowProgress = false + errorMessage = CoreLocalization.Error.unknownError + return + } + isShowProgress = false + } catch let error { + isShowProgress = false + if error.isInternetError || error is NoCachedDataError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + func showCourseDetails(componentID: String) async { + do { + let courseStructure = try await interactor.getLoadedCourseBlocks(courseID: courseID) + router.showCourseComponent( + componentID: componentID, + courseStructure: courseStructure + ) + } catch _ { + errorMessage = CourseLocalization.Error.componentNotFount + } + } +} diff --git a/Course/Course/Presentation/Downloads/DownloadsView.swift b/Course/Course/Presentation/Downloads/DownloadsView.swift new file mode 100644 index 000000000..c6b0262d1 --- /dev/null +++ b/Course/Course/Presentation/Downloads/DownloadsView.swift @@ -0,0 +1,108 @@ +// +// DownloadsView.swift +// Course +// +// Created by Eugene Yatsenko on 20.12.2023. +// + +import SwiftUI +import Core +import Theme +import Combine + +struct DownloadsView: View { + + // MARK: - Properties + + @Environment(\.dismiss) private var dismiss + @StateObject private var viewModel: DownloadsViewModel + + init( + courseId: String? = nil, + manager: DownloadManagerProtocol + ) { + self._viewModel = .init( + wrappedValue: .init(courseId: courseId, manager: manager) + ) + } + + // MARK: - Body + + var body: some View { + NavigationView { + ScrollView { + LazyVStack { + ForEach( + viewModel.downloads, + content: cell + ) + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(CourseLocalization.Download.downloads) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .foregroundColor(Theme.Colors.accentColor) + } + .accessibilityIdentifier("close_button") + } + } + .padding(.top, 1) + } + } + + // MARK: - Views + + @ViewBuilder + func cell(task: DownloadDataTask) -> some View { + VStack(spacing: 0) { + VStack { + HStack { + VStack(alignment: .leading) { + let title = viewModel.title(task: task) + Text(title) + .font(Theme.Fonts.titleMedium) + .multilineTextAlignment(.leading) + .lineLimit(1) + .accessibilityElement(children: .ignore) + .accessibilityLabel(title) + .accessibilityIdentifier("file_name_text") + let fileSizeInMbText = task.fileSizeInMbText + Text(fileSizeInMbText) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textSecondary) + .multilineTextAlignment(.leading) + .lineLimit(1) + .accessibilityElement(children: .ignore) + .accessibilityLabel(fileSizeInMbText) + .accessibilityIdentifier("file_size_text") + if task.state != .finished { + ProgressView(value: task.progress, total: 1.0) + .tint(Theme.Colors.accentColor) + .accessibilityIdentifier("progress_line_view") + } + } + Spacer() + Button { + Task { + await viewModel.cancelDownloading(task: task) + } + } label: { + DownloadProgressView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.cancelDownload) + .accessibilityIdentifier("cancel_download_button") + } + .padding(.horizontal, 15) + } + .padding(.leading, 20) + .padding(.vertical, 5) + Divider() + } + } + } +} diff --git a/Course/Course/Presentation/Downloads/DownloadsViewModel.swift b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift new file mode 100644 index 000000000..b71566f58 --- /dev/null +++ b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift @@ -0,0 +1,86 @@ +// +// DownloadsViewModel.swift +// Course +// +// Created by Eugene Yatsenko on 20.12.2023. +// + +import Foundation +import Core +import Combine + +final class DownloadsViewModel: ObservableObject { + + // MARK: - Properties + + @Published private(set) var downloads: [DownloadDataTask] = [] + private let courseId: String? + + private let manager: DownloadManagerProtocol + private var cancellables = Set() + + init( + courseId: String? = nil, + manager: DownloadManagerProtocol + ) { + self.courseId = courseId + self.manager = manager + Task { await configure() } + observers() + } + + // MARK: - Intents + + func title(task: DownloadDataTask) -> String { + task.displayName.isEmpty ? + "(\(CourseLocalization.Download.untitled))" : + task.displayName + } + + @MainActor + func cancelDownloading(task: DownloadDataTask) async { + do { + try await manager.cancelDownloading(task: task) + downloads.removeAll(where: { $0.id == task.id }) + } catch { + debugLog(error) + } + } + + @MainActor + private func configure() async { + defer { + filter() + } + if let courseId = courseId { + downloads = await manager.getDownloadTasksForCourse(courseId) + return + } + downloads = await manager.getDownloadTasks() + + } + + private func observers() { + manager.eventPublisher() + .sink { [weak self] event in + guard let self else { return } + switch event { + case .progress(let progress, let downloadData): + if let firstIndex = downloads.firstIndex(where: { $0.id == downloadData.id }) { + self.downloads[firstIndex].progress = progress + } + case .finished(let downloadData): + downloads.removeAll(where: { $0.id == downloadData.id }) + default: + break + } + } + .store(in: &cancellables) + } + + private func filter() { + downloads = downloads + .filter { $0.state == .inProgress || $0.state == .waiting } + .sorted(by: { $0.state.order < $1.state.order }) + } +} diff --git a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift index 57b12b542..e289b1239 100644 --- a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift @@ -7,11 +7,13 @@ import SwiftUI import Core +import Theme public struct HandoutsUpdatesDetailView: View { - @Environment(\.colorScheme) var colorScheme + @Environment(\.colorScheme) var colorSchemeNative private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @State var colorScheme: ColorScheme = UITraitCollection.current.userInterfaceStyle == .light ? .light : .dark private var router: CourseRouter private let cssInjector: CSSInjector @@ -36,6 +38,10 @@ public struct HandoutsUpdatesDetailView: View { self.cssInjector = cssInjector } + private func updateColorScheme() { + colorScheme = UITraitCollection.current.userInterfaceStyle == .light ? .light : .dark + } + private func fixBrokenLinks(in htmlString: String) -> String { do { let regex = try NSRegularExpression( @@ -126,6 +132,10 @@ public struct HandoutsUpdatesDetailView: View { .navigationBarHidden(false) .navigationBarBackButtonHidden(false) .navigationTitle(title) + .onChange(of: colorSchemeNative) { newValue in + guard UIApplication.shared.applicationState == .active else { return } + updateColorScheme() + } } } diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index d6563b46f..cc8dcd325 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme struct HandoutsView: View { diff --git a/Course/Course/Presentation/Outline/ContinueWithView.swift b/Course/Course/Presentation/Outline/ContinueWithView.swift index 610d9fee0..e1b929728 100644 --- a/Course/Course/Presentation/Outline/ContinueWithView.swift +++ b/Course/Course/Presentation/Outline/ContinueWithView.swift @@ -7,34 +7,34 @@ import SwiftUI import Core +import Theme struct ContinueWith { let chapterIndex: Int let sequentialIndex: Int let verticalIndex: Int + let lastVisitedBlockId: String } struct ContinueWithView: View { private let data: ContinueWith - private let courseStructure: CourseStructure private let action: () -> Void + private let courseContinueUnit: CourseVertical private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - init(data: ContinueWith, courseStructure: CourseStructure, action: @escaping () -> Void) { + init(data: ContinueWith, courseContinueUnit: CourseVertical, action: @escaping () -> Void) { self.data = data - self.courseStructure = courseStructure self.action = action + self.courseContinueUnit = courseContinueUnit } var body: some View { VStack(alignment: .leading) { - let chapter = courseStructure.childs[data.chapterIndex] - if let vertical = chapter.childs[data.sequentialIndex].childs.first { if idiom == .pad { HStack(alignment: .top) { VStack(alignment: .leading) { - ContinueTitle(vertical: vertical) + ContinueTitle(vertical: courseContinueUnit) }.foregroundColor(Theme.Colors.textPrimary) Spacer() UnitButtonView(type: .continueLesson, action: action) @@ -43,13 +43,11 @@ struct ContinueWithView: View { .padding(.top, 32) } else { VStack(alignment: .leading) { - ContinueTitle(vertical: vertical) + ContinueTitle(vertical: courseContinueUnit) .foregroundColor(Theme.Colors.textPrimary) } UnitButtonView(type: .continueLesson, action: action) } - - } }.padding(.horizontal, 24) .padding(.top, 32) } @@ -60,7 +58,7 @@ private struct ContinueTitle: View { let vertical: CourseVertical var body: some View { - Text(CoreLocalization.Courseware.continueWith) + Text(CoreLocalization.Courseware.resumeWith) .font(Theme.Fonts.labelMedium) .foregroundColor(Theme.Colors.textSecondary) HStack { @@ -77,57 +75,49 @@ private struct ContinueTitle: View { #if DEBUG struct ContinueWithView_Previews: PreviewProvider { static var previews: some View { - - let childs = [ - CourseChapter( - blockId: "123", - id: "123", + let blocks = [ + CourseBlock( + blockId: "1", + id: "1", + courseId: "123", + graded: true, + completion: 0, + type: .html, + displayName: "Continue lesson", + studentUrl: "", + encodedVideo: nil + ), + CourseBlock( + blockId: "2", + id: "2", + courseId: "123", + graded: true, + completion: 0, + type: .html, displayName: "Continue lesson", - type: .chapter, - childs: [ - CourseSequential( - blockId: "1", - id: "1", - displayName: "Name", - type: .sequential, - completion: 0, - childs: [ - CourseVertical( - blockId: "1", - id: "1", - courseId: "123", - displayName: "Vertical", - type: .vertical, - completion: 0, - childs: [ - CourseBlock( - blockId: "2", - id: "2", - courseId: "123", - graded: true, - completion: 0, - type: .html, - displayName: "Continue lesson", - studentUrl: "") - ])])]) + studentUrl: "", + encodedVideo: nil + + ) ] ContinueWithView( - data: ContinueWith(chapterIndex: 0, sequentialIndex: 0, verticalIndex: 0), - courseStructure: CourseStructure( - id: "123", - graded: true, - completion: 0, - viewYouTubeUrl: "", - encodedVideo: "", - displayName: "Namaste", - childs: childs, - media: DataLayer.CourseMedia( - image: .init(raw: "", small: "", large: "") - ), - certificate: nil) - ) { - } + data: ContinueWith( + chapterIndex: 0, + sequentialIndex: 0, + verticalIndex: 0, + lastVisitedBlockId: "test_block_id" + ), + courseContinueUnit: CourseVertical( + blockId: "2", + id: "2", + courseId: "123", + displayName: "Second Unit", + type: .vertical, + completion: 1, + childs: blocks + ) + ) { } } } #endif diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 19a6fa413..043e7a609 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -8,6 +8,7 @@ import SwiftUI import Core import Kingfisher +import Theme public struct CourseOutlineView: View { @@ -18,7 +19,11 @@ public struct CourseOutlineView: View { @State private var openCertificateView: Bool = false private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - + + @State private var showingDownloads: Bool = false + @State private var showingVideoDownloadQuality: Bool = false + @State private var showingNoWifiMessage: Bool = false + public init( viewModel: CourseContainerViewModel, title: String, @@ -41,80 +46,47 @@ public struct CourseOutlineView: View { await viewModel.getCourseBlocks(courseID: courseID, withProgress: false) }) { VStack(alignment: .leading) { - ZStack { - // MARK: - Course Banner - if let banner = viewModel.courseStructure?.media.image.raw - .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - KFImage(URL(string: viewModel.config.baseURL.absoluteString + banner)) - .onFailureImage(CoreAssets.noCourseImage.image) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxWidth: proxy.size.width - 12, maxHeight: .infinity) - } - - // MARK: - Course Certificate - if let certificate = viewModel.courseStructure?.certificate { - if let url = certificate.url, url.count > 0 { - Theme.Colors.certificateForeground - VStack(alignment: .center, spacing: 8) { - CoreAssets.certificate.swiftUIImage - Text(CourseLocalization.Outline.congratulations) - .multilineTextAlignment(.center) - .font(Theme.Fonts.headlineMedium) - Text(CourseLocalization.Outline.passedTheCourse) - .font(Theme.Fonts.bodyMedium) - .multilineTextAlignment(.center) - StyledButton( - CourseLocalization.Outline.viewCertificate, - action: { openCertificateView = true }, - isTransparent: true - ) - .frame(width: 141) - .padding(.top, 8) - - .fullScreenCover( - isPresented: $openCertificateView, - content: { - WebBrowser( - url: url, - pageTitle: CourseLocalization.Outline.certificate - ) - }) - }.padding(.horizontal, 24) - .padding(.top, 8) - .foregroundColor(.white) - } - } + if viewModel.config.uiComponents.courseBannerEnabled { + courseBanner(proxy: proxy) } - .frame(maxHeight: 250) - .cornerRadius(12) - .padding(.horizontal, 6) - .padding(.top, 7) - .fixedSize(horizontal: false, vertical: true) - + + downloadQualityBars + if let continueWith = viewModel.continueWith, let courseStructure = viewModel.courseStructure, !isVideo { + let chapter = courseStructure.childs[continueWith.chapterIndex] + let sequential = chapter.childs[continueWith.sequentialIndex] + let continueUnit = sequential.childs[continueWith.verticalIndex] // MARK: - ContinueWith button ContinueWithView( data: continueWith, - courseStructure: courseStructure + courseContinueUnit: continueUnit ) { - let chapter = courseStructure.childs[continueWith.chapterIndex] - let sequential = chapter.childs[continueWith.sequentialIndex] + var continueBlock: CourseBlock? + continueUnit.childs.forEach { block in + if block.id == continueWith.lastVisitedBlockId { + continueBlock = block + } + } viewModel.trackResumeCourseTapped( - blockId: sequential.childs[continueWith.verticalIndex].blockId - ) - viewModel.router.showCourseVerticalView( - courseID: courseStructure.id, - courseName: courseStructure.displayName, - title: sequential.displayName, - chapters: courseStructure.childs, - chapterIndex: continueWith.chapterIndex, - sequentialIndex: continueWith.sequentialIndex + blockId: continueBlock?.id ?? "" ) + + if let course = viewModel.courseStructure { + viewModel.router.showCourseUnit( + courseName: course.displayName, + blockId: continueBlock?.id ?? "", + courseID: course.id, + sectionName: continueUnit.displayName, + verticalIndex: continueWith.verticalIndex, + chapters: course.childs, + chapterIndex: continueWith.chapterIndex, + sequentialIndex: continueWith.sequentialIndex + ) + } } } @@ -123,11 +95,20 @@ public struct CourseOutlineView: View { : viewModel.courseStructure { // MARK: - Sections - CourseStructureView( - proxy: proxy, - course: course, - viewModel: viewModel - ) + if viewModel.config.uiComponents.courseNestedListEnabled { + CourseStructureNestedListView( + proxy: proxy, + course: course, + viewModel: viewModel + ) + } else { + CourseStructureView( + proxy: proxy, + course: course, + viewModel: viewModel + ) + } + } else { if let courseStart = viewModel.courseStart { Text(courseStart > Date() ? CourseLocalization.Outline.courseHasntStarted : "") @@ -137,11 +118,14 @@ public struct CourseOutlineView: View { } Spacer(minLength: 84) } - }.frameLimit() + } + .frameLimit() .onRightSwipeGesture { viewModel.router.back() } - }.padding(.top, 8) + } + .padding(.top, viewModel.config.uiComponents.courseTopTabBarEnabled ? 0 : 8) + .accessibilityAction {} // MARK: - Offline mode SnackBar OfflineSnackBarView( @@ -157,7 +141,7 @@ public struct CourseOutlineView: View { Spacer() SnackBarView(message: viewModel.errorMessage) } - .padding(.bottom, viewModel.connectivity.isInternetAvaliable + .padding(.bottom, viewModel.isInternetAvaliable ? 0 : OfflineSnackBarView.height) .transition(.move(edge: .bottom)) .onAppear { @@ -179,120 +163,120 @@ public struct CourseOutlineView: View { Theme.Colors.background .ignoresSafeArea() ) + .sheet(isPresented: $showingDownloads) { + DownloadsView(manager: viewModel.manager) + } + .sheet(isPresented: $showingVideoDownloadQuality) { + viewModel.storage.userSettings.map { + VideoDownloadQualityContainerView( + downloadQuality: $0.downloadQuality, + didSelect: viewModel.update(downloadQuality:) + ) + } + } + .onReceive( + NotificationCenter.default.publisher( + for: .onBlockCompletion + ) + ) { notification in + guard let userInfo = notification.userInfo, + let chapterID = userInfo["chapterID"] as? String, + let sequentialID = userInfo["sequentialID"] as? String, + let verticalID = userInfo["verticalID"] as? String, + let blockID = userInfo["blockID"] as? String else { + return + } + viewModel.completeBlock( + chapterID: chapterID, + sequentialID: sequentialID, + verticalID: verticalID, + blockID: blockID + ) + } } -} -struct CourseStructureView: View { - - private let proxy: GeometryProxy - private let course: CourseStructure - private let viewModel: CourseContainerViewModel - private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - - init(proxy: GeometryProxy, course: CourseStructure, viewModel: CourseContainerViewModel) { - self.proxy = proxy - self.course = course - self.viewModel = viewModel + @ViewBuilder + private var downloadQualityBars: some View { + if isVideo, + let courseVideosStructure = viewModel.courseVideosStructure, + viewModel.hasVideoForDowbloads() { + VStack(spacing: 0) { + CourseVideoDownloadBarView( + courseStructure: courseVideosStructure, + courseViewModel: viewModel, + onNotInternetAvaliable: { + viewModel.errorMessage = CourseLocalization.Download.noWifiMessage + }, + onTap: { + showingDownloads = true + } + ) + viewModel.userSettings.map { + VideoDownloadQualityBarView( + downloadQuality: $0.downloadQuality + ) { + if viewModel.isAllDownloading() { + viewModel.errorMessage = CourseLocalization.Download.changeQualityAlert + return + } + showingVideoDownloadQuality = true + } + } + } + } } - - var body: some View { - let chapters = course.childs - ForEach(chapters, id: \.id) { chapter in - let chapterIndex = chapters.firstIndex(where: { $0.id == chapter.id }) - Text(chapter.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .foregroundColor(Theme.Colors.textSecondary) - .padding(.horizontal, 24) - .padding(.top, 40) - ForEach(chapter.childs, id: \.id) { child in - let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == child.id }) - VStack(alignment: .leading) { - Button( - action: { - if let chapterIndex, let sequentialIndex { - viewModel.trackSequentialClicked(child) - viewModel.router.showCourseVerticalView( - courseID: viewModel.courseStructure?.id ?? "", - courseName: viewModel.courseStructure?.displayName ?? "", - title: child.displayName, - chapters: chapters, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex + + private func courseBanner(proxy: GeometryProxy) -> some View { + ZStack { + // MARK: - Course Banner + if let banner = viewModel.courseStructure?.media.image.raw + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + KFImage(URL(string: viewModel.config.baseURL.absoluteString + banner)) + .onFailureImage(CoreAssets.noCourseImage.image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: proxy.size.width - 12, maxHeight: .infinity) + } + + // MARK: - Course Certificate + if let certificate = viewModel.courseStructure?.certificate { + if let url = certificate.url, url.count > 0 { + Theme.Colors.certificateForeground + VStack(alignment: .center, spacing: 8) { + CoreAssets.certificate.swiftUIImage + Text(CourseLocalization.Outline.congratulations) + .multilineTextAlignment(.center) + .font(Theme.Fonts.headlineMedium) + Text(CourseLocalization.Outline.passedTheCourse) + .font(Theme.Fonts.bodyMedium) + .multilineTextAlignment(.center) + StyledButton( + CourseLocalization.Outline.viewCertificate, + action: { openCertificateView = true }, + isTransparent: true + ) + .frame(width: 141) + .padding(.top, 8) + + .fullScreenCover( + isPresented: $openCertificateView, + content: { + WebBrowser( + url: url, + pageTitle: CourseLocalization.Outline.certificate ) - } - }, - label: { - Group { - if child.completion == 1 { - CoreAssets.finished.swiftUIImage - .renderingMode(.template) - .foregroundColor(.accentColor) - } else { - child.type.image - } - Text(child.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .lineLimit(1) - .frame( - maxWidth: idiom == .pad - ? proxy.size.width * 0.5 - : proxy.size.width * 0.6, - alignment: .leading - ) - }.foregroundColor(Theme.Colors.textPrimary) - Spacer() - if let state = viewModel.downloadState[child.id] { - switch state { - case .available: - DownloadAvailableView() - .onTapGesture { - viewModel.onDownloadViewTap( - chapter: chapter, - blockId: child.id, - state: state - ) - } - .onForeground { - viewModel.onForeground() - } - case .downloading: - DownloadProgressView() - .onTapGesture { - viewModel.onDownloadViewTap( - chapter: chapter, - blockId: child.id, - state: state - ) - } - .onBackground { - viewModel.onBackground() - } - case .finished: - DownloadFinishedView() - .onTapGesture { - viewModel.onDownloadViewTap( - chapter: chapter, - blockId: child.id, - state: state - ) - } - } - } - Image(systemName: "chevron.right") - .foregroundColor(Theme.Colors.accentColor) - }).padding(.horizontal, 36) - .padding(.vertical, 20) - if chapterIndex != chapters.count - 1 { - Divider() - .frame(height: 1) - .overlay(Theme.Colors.cardViewStroke) - .padding(.horizontal, 24) - } + }) + }.padding(.horizontal, 24) + .padding(.top, 8) + .foregroundColor(.white) } } } + .frame(maxHeight: 250) + .cornerRadius(12) + .padding(.horizontal, 6) + .padding(.top, 7) + .fixedSize(horizontal: false, vertical: true) } } @@ -307,6 +291,7 @@ struct CourseOutlineView_Previews: PreviewProvider { config: ConfigMock(), connectivity: Connectivity(), manager: DownloadManagerMock(), + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, diff --git a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift new file mode 100644 index 000000000..8cada0ce0 --- /dev/null +++ b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift @@ -0,0 +1,245 @@ +// +// CourseStructureNestedListView.swift +// Course +// +// Created by Eugene Yatsenko on 09.11.2023. +// + +import SwiftUI +import Core +import Kingfisher +import Theme + +struct CourseStructureNestedListView: View { + + private let proxy: GeometryProxy + private let course: CourseStructure + private let viewModel: CourseContainerViewModel + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @State private var isExpandedIds: [String] = [] + + init(proxy: GeometryProxy, course: CourseStructure, viewModel: CourseContainerViewModel) { + self.proxy = proxy + self.course = course + self.viewModel = viewModel + } + + var body: some View { + ForEach(course.childs, content: disclosureGroup) + } + + private func disclosureGroup(chapter: CourseChapter) -> some View { + CustomDisclosureGroup( + animation: .easeInOut(duration: 0.2), + isExpanded: .constant(isExpandedIds.contains(where: { $0 == chapter.id })), + onClick: { onHeaderClick(chapter: chapter) }, + header: { isExpanded in header(chapter: chapter, isExpanded: isExpanded) }, + content: { section(chapter: chapter) } + ) + } + + private func header( + chapter: CourseChapter, + isExpanded: Bool + ) -> some View { + HStack { + Text(chapter.displayName) + .font(Theme.Fonts.titleMedium) + .multilineTextAlignment(.leading) + .lineLimit(1) + .foregroundColor(Theme.Colors.textPrimary) + Spacer() + Image(systemName: "chevron.down") + .foregroundColor(Theme.Colors.accentColor) + .dropdownArrowRotationAnimation(value: isExpanded) + } + .padding(.horizontal, 30) + .padding(.vertical, 15) + } + + private func section(chapter: CourseChapter) -> some View { + ForEach(chapter.childs) { sequential in + VStack(spacing: 0) { + sequentialLabel( + sequential: sequential, + chapter: chapter, + isExpanded: false + ) + } + } + } + + @ViewBuilder + private func sequentialLabel( + sequential: CourseSequential, + chapter: CourseChapter, + isExpanded: Bool + ) -> some View { + HStack { + Button { + onLabelClick(sequential: sequential, chapter: chapter) + } label: { + Group { + if sequential.completion == 1 { + CoreAssets.finished.swiftUIImage + .renderingMode(.template) + .foregroundColor(.accentColor) + } else { + sequential.type.image + } + Text(sequential.displayName) + .font(Theme.Fonts.titleMedium) + .multilineTextAlignment(.leading) + .lineLimit(1) + } + .foregroundColor(Theme.Colors.textPrimary) + } + Spacer() + downloadButton( + sequential: sequential, + chapter: chapter + ) + + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(sequential.displayName) + .padding(.leading, 40) + .padding(.trailing, 28) + .padding(.vertical, 14) + } + + @ViewBuilder + private func downloadButton( + sequential: CourseSequential, + chapter: CourseChapter + ) -> some View { + if let state = viewModel.sequentialsDownloadState[sequential.id] { + switch state { + case .available: + if viewModel.isInternetAvaliable { + Button { + Task { + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: sequential.id, + state: state + ) + } + } label: { + DownloadAvailableView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.download) + } + downloadCount(sequential: sequential) + } + case .downloading: + if viewModel.isInternetAvaliable { + Button { + Task { + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: sequential.id, + state: state + ) + } + } label: { + DownloadProgressView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.cancelDownload) + } + } + case .finished: + Button { + viewModel.router.presentAlert( + alertTitle: "Warning", + alertMessage: "\(CourseLocalization.Alert.deleteVideos) \"\(sequential.displayName)\"?", + positiveAction: CoreLocalization.Alert.delete, + onCloseTapped: { + viewModel.router.dismiss(animated: true) + }, + okTapped: { + Task { + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: sequential.id, + state: state + ) + } + viewModel.router.dismiss(animated: true) + }, + type: .default( + positiveAction: CoreLocalization.Alert.delete, + image: CoreAssets.bgDelete.swiftUIImage + ) + ) + } label: { + DownloadFinishedView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.deleteDownload) + } + downloadCount(sequential: sequential) + } + } + } + + @ViewBuilder + private func downloadCount(sequential: CourseSequential) -> some View { + let downloadable = viewModel.verticalsBlocksDownloadable(by: sequential) + if !downloadable.isEmpty { + Text(String(downloadable.count)) + .foregroundColor(Color(UIColor.label)) + } + } + + private func onHeaderClick(chapter: CourseChapter) { + if let index = isExpandedIds.firstIndex(where: {$0 == chapter.id}) { + isExpandedIds.remove(at: index) + } else { + isExpandedIds.append(chapter.id) + } + } + + private func onLabelClick( + sequential: CourseSequential, + chapter: CourseChapter + ) { + guard let chapterIndex = course.childs.firstIndex( + where: { $0.id == chapter.id } + ) else { + return + } + + guard let sequentialIndex = chapter.childs.firstIndex( + where: { $0.id == sequential.id } + ) else { + return + } + + guard let courseVertical = sequential.childs.first else { + return + } + + guard let block = courseVertical.childs.first else { + return + } + + viewModel.trackVerticalClicked( + courseId: viewModel.courseStructure?.id ?? "", + courseName: viewModel.courseStructure?.displayName ?? "", + vertical: courseVertical + ) + viewModel.router.showCourseUnit( + courseName: viewModel.courseStructure?.displayName ?? "", + blockId: block.id, + courseID: viewModel.courseStructure?.id ?? "", + sectionName: block.displayName, + verticalIndex: 0, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + + } + +} diff --git a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift new file mode 100644 index 000000000..fad515777 --- /dev/null +++ b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift @@ -0,0 +1,134 @@ +// +// CourseStructureView.swift +// Course +// +// Created by Eugene Yatsenko on 15.12.2023. +// + +import SwiftUI +import Core +import Theme + +struct CourseStructureView: View { + + private let proxy: GeometryProxy + private let course: CourseStructure + private let viewModel: CourseContainerViewModel + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + init(proxy: GeometryProxy, course: CourseStructure, viewModel: CourseContainerViewModel) { + self.proxy = proxy + self.course = course + self.viewModel = viewModel + } + + var body: some View { + let chapters = course.childs + ForEach(chapters, id: \.id) { chapter in + let chapterIndex = chapters.firstIndex(where: { $0.id == chapter.id }) + Text(chapter.displayName) + .font(Theme.Fonts.titleMedium) + .multilineTextAlignment(.leading) + .foregroundColor(Theme.Colors.textSecondary) + .padding(.horizontal, 24) + .padding(.top, 40) + ForEach(chapter.childs, id: \.id) { child in + let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == child.id }) + VStack(alignment: .leading) { + HStack { + Button { + if let chapterIndex, let sequentialIndex { + viewModel.trackSequentialClicked(child) + viewModel.router.showCourseVerticalView( + courseID: viewModel.courseStructure?.id ?? "", + courseName: viewModel.courseStructure?.displayName ?? "", + title: child.displayName, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } + } label: { + Group { + if child.completion == 1 { + CoreAssets.finished.swiftUIImage + .renderingMode(.template) + .foregroundColor(.accentColor) + } else { + child.type.image + } + Text(child.displayName) + .font(Theme.Fonts.titleMedium) + .multilineTextAlignment(.leading) + .lineLimit(1) + .frame( + maxWidth: idiom == .pad + ? proxy.size.width * 0.5 + : proxy.size.width * 0.6, + alignment: .leading + ) + } + .foregroundColor(Theme.Colors.textPrimary) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(child.displayName) + Spacer() + if let state = viewModel.sequentialsDownloadState[child.id] { + switch state { + case .available: + DownloadAvailableView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.download) + .onTapGesture { + Task { + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: child.id, + state: state + ) + } + } + case .downloading: + DownloadProgressView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.cancelDownload) + .onTapGesture { + Task { + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: child.id, + state: state + ) + } + } + case .finished: + DownloadFinishedView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.deleteDownload) + .onTapGesture { + Task { + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: child.id, + state: state + ) + } + } + } + } + Image(systemName: "chevron.right") + .foregroundColor(Theme.Colors.accentColor) + } + .padding(.horizontal, 36) + .padding(.vertical, 20) + if chapterIndex != chapters.count - 1 { + Divider() + .frame(height: 1) + .overlay(Theme.Colors.cardViewStroke) + .padding(.horizontal, 24) + } + } + } + } + } +} diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift new file mode 100644 index 000000000..7d6288fd7 --- /dev/null +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift @@ -0,0 +1,111 @@ +// +// CourseVerticalImageView.swift +// Course +// +// Created by Vadim Kuznetsov on 5.12.23. +// + +import Core +import SwiftUI + +struct CourseVerticalImageView: View { + var blocks: [CourseBlock] + + var body: some View { + if blocks.contains(where: { $0.type == .problem }) { + return CoreAssets.pen.swiftUIImage.renderingMode(.template) + } else if blocks.contains(where: { $0.type == .video }) { + return CoreAssets.video.swiftUIImage.renderingMode(.template) + } else if blocks.contains(where: { $0.type == .discussion }) { + return CoreAssets.discussion.swiftUIImage.renderingMode(.template) + } else if blocks.contains(where: { $0.type == .html }) { + return CoreAssets.extra.swiftUIImage.renderingMode(.template) + } else { + return CoreAssets.extra.swiftUIImage.renderingMode(.template) + } + } +} +#if DEBUG +struct CourseVerticalImageView_Previews: PreviewProvider { + static var previews: some View { + let blocks1 = [ + CourseBlock( + blockId: "1", + id: "1", + courseId: "123", + topicId: "1", + graded: false, + completion: 1, + type: .video, + displayName: "Block 1", + studentUrl: "", + encodedVideo: nil + ) + ] + + let blocks2 = [ + CourseBlock( + blockId: "1", + id: "1", + courseId: "123", + topicId: "1", + graded: false, + completion: 1, + type: .problem, + displayName: "Block 1", + studentUrl: "", + encodedVideo: nil + ) + ] + let blocks3 = [ + CourseBlock( + blockId: "1", + id: "1", + courseId: "123", + topicId: "1", + graded: false, + completion: 1, + type: .discussion, + displayName: "Block 1", + studentUrl: "", + encodedVideo: nil + ) + ] + let blocks4 = [ + CourseBlock( + blockId: "1", + id: "1", + courseId: "123", + topicId: "1", + graded: false, + completion: 1, + type: .html, + displayName: "Block 1", + studentUrl: "", + encodedVideo: nil + ) + ] + let blocks5 = [ + CourseBlock( + blockId: "1", + id: "1", + courseId: "123", + topicId: "1", + graded: false, + completion: 1, + type: .unknown, + displayName: "Block 1", + studentUrl: "", + encodedVideo: nil + ) + ] + HStack { + CourseVerticalImageView(blocks: blocks1) + CourseVerticalImageView(blocks: blocks2) + CourseVerticalImageView(blocks: blocks3) + CourseVerticalImageView(blocks: blocks4) + CourseVerticalImageView(blocks: blocks5) + } + } +} +#endif diff --git a/Course/Course/Presentation/Outline/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift similarity index 78% rename from Course/Course/Presentation/Outline/CourseVerticalView.swift rename to Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift index c5cb290af..889d6e155 100644 --- a/Course/Course/Presentation/Outline/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift @@ -9,6 +9,7 @@ import SwiftUI import Core import Kingfisher +import Theme public struct CourseVerticalView: View { @@ -40,6 +41,7 @@ public struct CourseVerticalView: View { // MARK: - Lessons list ForEach(viewModel.verticals, id: \.id) { vertical in if let index = viewModel.verticals.firstIndex(where: {$0.id == vertical.id}) { + HStack { Button(action: { let vertical = viewModel.verticals[index] if let block = vertical.childs.first { @@ -60,14 +62,13 @@ public struct CourseVerticalView: View { ) } }, label: { - HStack { Group { if vertical.completion == 1 { CoreAssets.finished.swiftUIImage .renderingMode(.template) .foregroundColor(.accentColor) } else { - vertical.type.image + CourseVerticalImageView(blocks: vertical.childs) } Text(vertical.displayName) .font(Theme.Fonts.titleMedium) @@ -79,45 +80,55 @@ public struct CourseVerticalView: View { .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .leading) }.foregroundColor(Theme.Colors.textPrimary) + }).accessibilityElement(children: .ignore) + .accessibilityLabel(vertical.displayName) Spacer() if let state = viewModel.downloadState[vertical.id] { switch state { case .available: DownloadAvailableView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.download) .onTapGesture { - viewModel.onDownloadViewTap( - blockId: vertical.id, - state: state - ) - } - .onForeground { - viewModel.onForeground() + Task { + await viewModel.onDownloadViewTap( + blockId: vertical.id, + state: state + ) + } + } case .downloading: DownloadProgressView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.cancelDownload) .onTapGesture { - viewModel.onDownloadViewTap( - blockId: vertical.id, - state: state - ) - } - .onBackground { - viewModel.onBackground() + Task { + await viewModel.onDownloadViewTap( + blockId: vertical.id, + state: state + ) + } + } case .finished: DownloadFinishedView() + .accessibilityElement(children: .ignore) + .accessibilityLabel(CourseLocalization.Accessibility.deleteDownload) .onTapGesture { - viewModel.onDownloadViewTap( - blockId: vertical.id, - state: state - ) + Task { + await viewModel.onDownloadViewTap( + blockId: vertical.id, + state: state + ) + } } } } Image(systemName: "chevron.right") .padding(.vertical, 8) } - }).padding(.horizontal, 36) + .padding(.horizontal, 36) .padding(.vertical, 14) if index != viewModel.verticals.count - 1 { Divider() @@ -129,7 +140,8 @@ public struct CourseVerticalView: View { } } Spacer(minLength: 84) - }.frameLimit() + }.accessibilityAction {} + .frameLimit() .onRightSwipeGesture { viewModel.router.back() } diff --git a/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift similarity index 87% rename from Course/Course/Presentation/Outline/CourseVerticalViewModel.swift rename to Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift index 9f2ae30b7..4ce5ebd70 100644 --- a/Course/Course/Presentation/Outline/CourseVerticalViewModel.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift @@ -49,16 +49,19 @@ public class CourseVerticalViewModel: BaseCourseViewModel { manager.publisher() .sink(receiveValue: { [weak self] _ in guard let self else { return } - DispatchQueue.main.async { - self.setDownloadsStates() + Task { + await self.setDownloadsStates() } }) .store(in: &cancellables) - setDownloadsStates() + Task { + await setDownloadsStates() + } } - func onDownloadViewTap(blockId: String, state: DownloadViewState) { + @MainActor + func onDownloadViewTap(blockId: String, state: DownloadViewState) async { if let vertical = verticals.first(where: { $0.id == blockId }) { let blocks = vertical.childs.filter { $0.isDownloadable } do { @@ -67,10 +70,10 @@ public class CourseVerticalViewModel: BaseCourseViewModel { try manager.addToDownloadQueue(blocks: blocks) downloadState[vertical.id] = .downloading case .downloading: - try manager.cancelDownloading(courseId: vertical.courseId, blocks: blocks) + try await manager.cancelDownloading(courseId: vertical.courseId, blocks: blocks) downloadState[vertical.id] = .available case .finished: - manager.deleteFile(blocks: blocks) + await manager.deleteFile(blocks: blocks) downloadState[vertical.id] = .available } } catch let error { @@ -80,7 +83,7 @@ public class CourseVerticalViewModel: BaseCourseViewModel { } } } - + func trackVerticalClicked( courseId: String, courseName: String, @@ -94,19 +97,18 @@ public class CourseVerticalViewModel: BaseCourseViewModel { ) } - private func setDownloadsStates() { + @MainActor + private func setDownloadsStates() async { guard let courseId = verticals.first?.courseId else { return } - let downloads = manager.getDownloadsForCourse(courseId) + let downloadTasks = await manager.getDownloadTasksForCourse(courseId) var states: [String: DownloadViewState] = [:] for vertical in verticals where vertical.isDownloadable { var childs: [DownloadViewState] = [] for block in vertical.childs where block.isDownloadable { - if let download = downloads.first(where: { $0.id == block.id }) { + if let download = downloadTasks.first(where: { $0.id == block.id }) { switch download.state { case .waiting, .inProgress: childs.append(.downloading) - case .paused: - childs.append(.available) case .finished: childs.append(.finished) } diff --git a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift new file mode 100644 index 000000000..600b565da --- /dev/null +++ b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarView.swift @@ -0,0 +1,145 @@ +// +// CourseVideoDownloadBarView.swift +// Course +// +// Created by Eugene Yatsenko on 15.12.2023. +// + +import SwiftUI +import Core +import Theme +import Combine + +struct CourseVideoDownloadBarView: View { + + // MARK: - Properties + + @StateObject var viewModel: CourseVideoDownloadBarViewModel + private var onTap: (() -> Void)? + private var onNotInternetAvaliable: (() -> Void)? + + init( + courseStructure: CourseStructure, + courseViewModel: CourseContainerViewModel, + onNotInternetAvaliable: (() -> Void)?, + onTap: (() -> Void)? = nil + ) { + self._viewModel = .init( + wrappedValue: .init( + courseStructure: courseStructure, + courseViewModel: courseViewModel + ) + ) + self.onNotInternetAvaliable = onNotInternetAvaliable + self.onTap = onTap + } + + // MARK: - Body + + var body: some View { + VStack(spacing: 0) { + Divider() + HStack(spacing: 0) { + image + titles + toggle + } + .padding(.vertical, 10) + if viewModel.isOn, !viewModel.allVideosDownloaded { + ProgressView(value: viewModel.progress, total: 1) + .tint(Theme.Colors.accentColor) + .accessibilityIdentifier("progress_line_view") + } + Divider() + } + .contentShape(Rectangle()) + .onTapGesture { + Task { + let downloads = await viewModel.allActiveDownloads() + if !downloads.isEmpty { + onTap?() + } + } + } + .accessibilityIdentifier("videos_download_bar") + } + + // MARK: - Views + + private var image: some View { + VStack { + if viewModel.isOn, !viewModel.allVideosDownloaded { + ProgressView() + .accessibilityIdentifier("progress_view") + } else { + CoreAssets.video.swiftUIImage + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(width: 30, height: 30) + .accessibilityIdentifier("video_image") + } + } + .frame(width: 40, height: 40) + .padding(.leading, 15) + } + + @ViewBuilder + private var titles: some View { + HStack { + VStack(alignment: .leading) { + let title = viewModel.title + Text(title) + .lineLimit(1) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(title) + .accessibilityIdentifier("bar_title_text") + HStack(spacing: 0) { + Group { + if viewModel.remainingVideos == 0 { + let text = "\(CourseLocalization.Download.videos) \(viewModel.totalFinishedVideos)" + Text(text) + .accessibilityElement(children: .ignore) + .accessibilityLabel(text) + .accessibilityIdentifier("videos_total_finished_text") + } else { + let text = "\(CourseLocalization.Download.remaining) \(viewModel.remainingVideos)" + Text(text) + .accessibilityElement(children: .ignore) + .accessibilityLabel(text) + .accessibilityIdentifier("remaining_videos_text") + } + if let totalSize = viewModel.totalSize { + let text = ", \(totalSize)MB \(CourseLocalization.Download.total)" + Text(text) + .accessibilityElement(children: .ignore) + .accessibilityLabel(text) + .accessibilityIdentifier("total_size_text") + } + } + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textSecondary) + } + } + Spacer() + } + .padding(.horizontal, 10) + .layoutPriority(1) + } + + private var toggle: some View { + Toggle("", isOn: .constant(viewModel.isOn)) + .toggleStyle(SwitchToggleStyle(tint: Theme.Colors.accentColor)) + .padding(.trailing, 15) + .onTapGesture { + if !viewModel.isInternetAvaliable { + onNotInternetAvaliable?() + return + } + Task { await viewModel.onToggle() } + } + .accessibilityIdentifier("download_toggle") + } +} diff --git a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift new file mode 100644 index 000000000..4a64dbf4f --- /dev/null +++ b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift @@ -0,0 +1,249 @@ +// +// CourseVideoDownloadBarViewModel.swift +// Course +// +// Created by Eugene Yatsenko on 20.12.2023. +// + +import Foundation +import Core +import Combine + +final class CourseVideoDownloadBarViewModel: ObservableObject { + + // MARK: - Properties + + private let courseStructure: CourseStructure + private let courseViewModel: CourseContainerViewModel + + @Published private(set) var currentDownloadTask: DownloadDataTask? + @Published private(set) var isOn: Bool = false + + private var cancellables = Set() + + var isInternetAvaliable: Bool { + courseViewModel.isInternetAvaliable + } + + var title: String { + if isOn { + if remainingVideos == 0 { + return CourseLocalization.Download.allVideosDownloaded + } else { + return CourseLocalization.Download.downloadingVideos + } + } else { + return CourseLocalization.Download.downloadToDevice + } + } + + var progress: Double { + guard let currentDownloadTask = currentDownloadTask else { + return 0.0 + } + guard let index = courseViewModel.courseDownloadTasks.firstIndex( + where: { $0.id == currentDownloadTask.id } + ) else { + return 0.0 + } + courseViewModel.courseDownloadTasks[index].progress = currentDownloadTask.progress + return courseViewModel + .courseDownloadTasks + .reduce(0) { $0 + $1.progress } / Double(courseViewModel.courseDownloadTasks.count) + } + + var downloadableVerticals: Set { + courseViewModel.downloadableVerticals + } + + var allVideosDownloaded: Bool { + let totalFinishedCount = downloadableVerticals.filter { $0.state == .finished }.count + return totalFinishedCount == downloadableVerticals.count + } + + var remainingVideos: Int { + let inProgress = downloadableVerticals.filter { $0.state != .finished } + return inProgress.flatMap { $0.downloadableBlocks }.count + } + + var downloadingVideos: Int { + let downloading = downloadableVerticals.filter { $0.state == .downloading } + return downloading.flatMap { $0.downloadableBlocks }.count + } + + var totalFinishedVideos: Int { + let finished = downloadableVerticals.filter { $0.state == .finished } + return finished.flatMap { $0.downloadableBlocks }.count + } + + var totalSize: String? { + let downloadQuality = courseViewModel.userSettings?.downloadQuality ?? .auto + let mb = courseStructure.totalVideosSizeInMb( + downloadQuality: downloadQuality + ) + + if mb == 0 { return nil } + + if isOn { + let size = mb - calculateSize(value: mb, percentage: progress * 100) + if size == 0 { + return String(format: "%.2f", mb) + } + return String(format: "%.2f", size) + } + + let size = blockToMB( + data: Set(downloadableVerticals + .filter { $0.state != .finished } + .flatMap { $0.downloadableBlocks } + ), + downloadQuality: downloadQuality + ) + + return String(format: "%.2f", size) + } + + init( + courseStructure: CourseStructure, + courseViewModel: CourseContainerViewModel + ) { + self.courseStructure = courseStructure + self.courseViewModel = courseViewModel + observers() + } + + func allActiveDownloads() async -> [DownloadDataTask] { + await courseViewModel.manager.getDownloadTasks() + .filter { $0.state == .inProgress || $0.state == .waiting } + } + + @MainActor + func onToggle() async { + if allVideosDownloaded { + courseViewModel.router.presentAlert( + alertTitle: "Warning", + alertMessage: "\(CourseLocalization.Alert.deleteAllVideos) \"\(courseStructure.displayName)\"?", + positiveAction: CoreLocalization.Alert.delete, + onCloseTapped: { [weak self] in + self?.courseViewModel.router.dismiss(animated: true) + }, + okTapped: { [weak self] in + guard let self else { return } + Task { + await self.downloadAll(isOn: false) + } + self.courseViewModel.router.dismiss(animated: true) + }, + type: .default(positiveAction: CoreLocalization.Alert.delete, image: CoreAssets.bgDelete.swiftUIImage) + ) + return + } + + if isOn { + courseViewModel.router.presentAlert( + alertTitle: "Warning", + alertMessage: "\(CourseLocalization.Alert.stopDownloading) \"\(courseStructure.displayName)\"?", + positiveAction: CoreLocalization.Alert.accept, + onCloseTapped: { [weak self] in + self?.courseViewModel.router.dismiss(animated: true) + }, + okTapped: { [weak self] in + guard let self else { return } + Task { + await self.downloadAll(isOn: false) + } + self.courseViewModel.router.dismiss(animated: true) + }, + type: .default(positiveAction: CoreLocalization.Alert.accept, image: nil) + ) + return + } + + await downloadAll(isOn: true) + } + + @MainActor + private func downloadAll(isOn: Bool) async { + let blocks = downloadableVerticals.flatMap { $0.vertical.childs } + + if isOn, courseViewModel.isShowedAllowLargeDownloadAlert(blocks: blocks) { + return + } + + if isOn { + let blocks = downloadableVerticals.filter { $0.state != .finished }.flatMap { $0.vertical.childs } + await courseViewModel.download( + state: .available, + blocks: blocks + ) + } else { + do { + try await courseViewModel.manager.cancelDownloading(courseId: courseStructure.id) + } catch { + debugLog(error) + } + + } + } + + // MARK: - Private intents + + private func toggleStateIsOn() { + let totalCount = courseViewModel.downloadableVerticals.count + let availableCount = courseViewModel.downloadableVerticals.filter { $0.state == .available }.count + let finishedCount = courseViewModel.downloadableVerticals.filter { $0.state == .finished }.count + let downloadingCount = courseViewModel.downloadableVerticals.filter { $0.state == .downloading }.count + + if downloadingCount == totalCount { + self.isOn = true + return + } + if totalCount == finishedCount { + self.isOn = true + return + } + if availableCount > 0 { + self.isOn = false + return + } + if downloadingCount == 0 { + self.isOn = false + return + } + + let isOn = totalCount - finishedCount == downloadingCount + self.isOn = isOn + } + + private func observers() { + currentDownloadTask = courseViewModel.manager.currentDownloadTask + toggleStateIsOn() + courseViewModel.$downloadableVerticals + .sink { [weak self] _ in + guard let self else { return } + self.currentDownloadTask = self.courseViewModel.manager.currentDownloadTask + self.toggleStateIsOn() + } + .store(in: &cancellables) + courseViewModel.manager.eventPublisher() + .sink { [weak self] state in + guard let self else { return } + if case .progress = state { + self.currentDownloadTask = self.courseViewModel.manager.currentDownloadTask + } + self.toggleStateIsOn() + } + .store(in: &cancellables) + } + + private func calculateSize(value: Double, percentage: Double) -> Double { + let val = value * percentage + return val / 100.0 + } + + private func blockToMB(data: Set, downloadQuality: DownloadQuality) -> Double { + data.reduce(0) { + $0 + Double($1.encodedVideo?.video(downloadQuality: downloadQuality)?.fileSize ?? 0) + } / 1024.0 / 1024.0 + } +} diff --git a/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityBarView.swift b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityBarView.swift new file mode 100644 index 000000000..56d4fa158 --- /dev/null +++ b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityBarView.swift @@ -0,0 +1,71 @@ +// +// VideoDownloadQualityBarView.swift +// Course +// +// Created by Eugene Yatsenko on 04.01.2024. +// + +import SwiftUI +import Core +import Theme +import Combine +import Profile + +struct VideoDownloadQualityBarView: View { + + private var downloadQuality: DownloadQuality + private var onTap: (() -> Void)? + + init(downloadQuality: DownloadQuality, onTap: (() -> Void)? = nil) { + self.downloadQuality = downloadQuality + self.onTap = onTap + } + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 0) { + VStack { + Image(systemName: "gearshape") + .renderingMode(.template) + .resizable() + .scaledToFit() + .font(.system(size: 25, weight: .medium)) + .frame(width: 25, height: 25) + .accessibilityIdentifier("gearshape_image") + + } + .frame(width: 40, height: 40) + .padding(.leading, 15) + titles + Spacer() + } + .padding(.vertical, 10) + Divider() + } + .contentShape(Rectangle()) + .onTapGesture { onTap?() } + .accessibilityIdentifier("video_download_quality_bar") + } + + @ViewBuilder + private var titles: some View { + VStack(alignment: .leading) { + let videoDownloadQualityTitle = CoreLocalization.Settings.videoDownloadQualityTitle + Text(videoDownloadQualityTitle) + .lineLimit(1) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(videoDownloadQualityTitle) + .accessibilityIdentifier("video_quality_title_text") + let settingsDescription = downloadQuality.settingsDescription + Text(settingsDescription) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textSecondary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(settingsDescription) + .accessibilityIdentifier("video_quality_description_text") + } + .padding(.horizontal, 10) + } +} diff --git a/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift new file mode 100644 index 000000000..f81c4b91d --- /dev/null +++ b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift @@ -0,0 +1,45 @@ +// +// VideoDownloadQualityContainerView.swift +// Course +// +// Created by Eugene Yatsenko on 04.01.2024. +// + +import SwiftUI +import Core +import Theme + +struct VideoDownloadQualityContainerView: View { + + @Environment(\.dismiss) private var dismiss + + private var downloadQuality: DownloadQuality + private var didSelect: ((DownloadQuality) -> Void)? + + init(downloadQuality: DownloadQuality, didSelect: ((DownloadQuality) -> Void)?) { + self.downloadQuality = downloadQuality + self.didSelect = didSelect + } + + var body: some View { + NavigationView { + VideoDownloadQualityView( + downloadQuality: downloadQuality, + didSelect: didSelect + ) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .foregroundColor(Theme.Colors.accentColor) + } + .accessibilityIdentifier("close_button") + } + } + .padding(.top, 1) + } + } +} diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift index ba9d34a5a..0834019e3 100644 --- a/Course/Course/Presentation/Unit/CourseNavigationView.swift +++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift @@ -30,121 +30,123 @@ struct CourseNavigationView: View { HStack(alignment: .top, spacing: 7) { if viewModel.selectedLesson() == viewModel.verticals[viewModel.verticalIndex].childs.first && viewModel.verticals[viewModel.verticalIndex].childs.count != 1 { - UnitButtonView(type: .nextBig, action: { - playerStateSubject.send(VideoPlayerState.pause) - viewModel.select(move: .next) - }).frame(width: 215) + nextBigButton + .frame(width: 215) } else { if viewModel.selectedLesson() == viewModel.verticals[viewModel.verticalIndex].childs.last { if viewModel.selectedLesson() != viewModel.verticals[viewModel.verticalIndex].childs.first { - UnitButtonView(type: .previous, action: { - playerStateSubject.send(VideoPlayerState.pause) - viewModel.select(move: .previous) - }) + prevButton } - UnitButtonView(type: .last, action: { - let sequentials = viewModel.chapters[viewModel.chapterIndex].childs - let verticals = viewModel - .chapters[viewModel.chapterIndex] - .childs[viewModel.sequentialIndex] - .childs - let chapters = viewModel.chapters - let currentVertical = viewModel.verticals[viewModel.verticalIndex] - - viewModel.router.presentAlert( - alertTitle: CourseLocalization.Courseware.goodWork, - alertMessage: (CourseLocalization.Courseware.section - + currentVertical.displayName + CourseLocalization.Courseware.isFinished), - nextSectionName: { - if viewModel.verticals.count > viewModel.verticalIndex + 1 { - return viewModel.verticals[viewModel.verticalIndex + 1].displayName - } else if sequentials.count > viewModel.sequentialIndex + 1 { - return sequentials[viewModel.sequentialIndex + 1].childs.first?.displayName - } else if chapters.count > viewModel.chapterIndex + 1 { - return chapters[viewModel.chapterIndex + 1].childs.first?.childs.first?.displayName - } else { - return nil - } - }(), - action: CourseLocalization.Courseware.backToOutline, - image: CoreAssets.goodWork.swiftUIImage, - onCloseTapped: { viewModel.router.dismiss(animated: false) }, - okTapped: { - playerStateSubject.send(VideoPlayerState.pause) - playerStateSubject.send(VideoPlayerState.kill) - - viewModel.trackFinishVerticalBackToOutlineClicked() - viewModel.router.dismiss(animated: false) - viewModel.router.back(animated: true) - }, - nextSectionTapped: { - playerStateSubject.send(VideoPlayerState.pause) - playerStateSubject.send(VideoPlayerState.kill) - viewModel.router.dismiss(animated: false) - - let chapterIndex: Int - let sequentialIndex: Int - let verticalIndex: Int - - // Switch to the next Vertical - if verticals.count - 1 > viewModel.verticalIndex { - chapterIndex = viewModel.chapterIndex - sequentialIndex = viewModel.sequentialIndex - verticalIndex = viewModel.verticalIndex + 1 - // Switch to the next Sequential - } else if sequentials.count - 1 > viewModel.sequentialIndex { - chapterIndex = viewModel.chapterIndex - sequentialIndex = viewModel.sequentialIndex + 1 - verticalIndex = 0 - } else { - // Switch to the next Chapter - chapterIndex = viewModel.chapterIndex + 1 - sequentialIndex = 0 - verticalIndex = 0 - } - - viewModel.analytics - .finishVerticalNextSectionClicked( - courseId: viewModel.courseID, - courseName: viewModel.courseName, - blockId: viewModel.selectedLesson().blockId, - blockName: viewModel.selectedLesson().displayName - ) - - viewModel.router.replaceCourseUnit( - courseName: viewModel.courseName, - blockId: viewModel.lessonID, - courseID: viewModel.courseID, - sectionName: viewModel.selectedLesson().displayName, - verticalIndex: verticalIndex, - chapters: viewModel.chapters, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex) - } - ) - viewModel.analytics.finishVerticalClicked( - courseId: viewModel.courseID, - courseName: viewModel.courseName, - blockId: viewModel.selectedLesson().blockId, - blockName: viewModel.selectedLesson().displayName - ) - }) + lastButton } else { if viewModel.selectedLesson() != viewModel.verticals[viewModel.verticalIndex].childs.first { - UnitButtonView(type: .previous, action: { - playerStateSubject.send(VideoPlayerState.pause) - viewModel.select(move: .previous) - }) + prevButton } - UnitButtonView(type: .next, action: { - playerStateSubject.send(VideoPlayerState.pause) - viewModel.select(move: .next) - }) + nextButton } } - }.frame(minWidth: 0, maxWidth: .infinity) - .padding(.horizontal, 24) + }.padding(.horizontal, 24) + } + + private var nextBigButton: some View { + UnitButtonView( + type: .nextBig, + isVerticalNavigation: !viewModel.courseUnitProgressEnabled, + action: { + playerStateSubject.send(VideoPlayerState.pause) + viewModel.select(move: .next) + } + ) + } + + private var nextButton: some View { + UnitButtonView( + type: .next, + isVerticalNavigation: !viewModel.courseUnitProgressEnabled, + action: { + playerStateSubject.send(VideoPlayerState.pause) + viewModel.select(move: .next) + } + ) + } + + private var prevButton: some View { + UnitButtonView( + type: .previous, + isVerticalNavigation: !viewModel.courseUnitProgressEnabled, + action: { + playerStateSubject.send(VideoPlayerState.pause) + viewModel.select(move: .previous) + } + ) + } + + private var lastButton: some View { + UnitButtonView( + type: .last, + isVerticalNavigation: !viewModel.courseUnitProgressEnabled, + action: { + let currentVertical = viewModel.verticals[viewModel.verticalIndex] + + viewModel.router.presentAlert( + alertTitle: CourseLocalization.Courseware.goodWork, + alertMessage: (CourseLocalization.Courseware.section + + currentVertical.displayName + CourseLocalization.Courseware.isFinished), + nextSectionName: { + if let data = viewModel.nextData, + let vertical = viewModel.vertical(for: data) { + return vertical.displayName + } + return nil + }(), + action: CourseLocalization.Courseware.backToOutline, + image: CoreAssets.goodWork.swiftUIImage, + onCloseTapped: { viewModel.router.dismiss(animated: false) }, + okTapped: { + playerStateSubject.send(VideoPlayerState.pause) + playerStateSubject.send(VideoPlayerState.kill) + + viewModel.trackFinishVerticalBackToOutlineClicked() + viewModel.router.dismiss(animated: false) + viewModel.router.back(animated: true) + }, + nextSectionTapped: { + playerStateSubject.send(VideoPlayerState.pause) + playerStateSubject.send(VideoPlayerState.kill) + viewModel.router.dismiss(animated: false) + + viewModel.analytics + .finishVerticalNextSectionClicked( + courseId: viewModel.courseID, + courseName: viewModel.courseName, + blockId: viewModel.selectedLesson().blockId, + blockName: viewModel.selectedLesson().displayName + ) + + guard let data = viewModel.nextData else { return } + viewModel.router.replaceCourseUnit( + courseName: viewModel.courseName, + blockId: viewModel.lessonID, + courseID: viewModel.courseID, + sectionName: viewModel.selectedLesson().displayName, + verticalIndex: data.verticalIndex, + chapters: viewModel.chapters, + chapterIndex: data.chapterIndex, + sequentialIndex: data.sequentialIndex, + animated: true + ) + } + ) + playerStateSubject.send(VideoPlayerState.pause) + viewModel.analytics.finishVerticalClicked( + courseId: viewModel.courseID, + courseName: viewModel.courseName, + blockId: viewModel.selectedLesson().blockId, + blockName: viewModel.selectedLesson().displayName + ) + } + ) } } @@ -159,10 +161,12 @@ struct CourseNavigationView_Previews: PreviewProvider { chapterIndex: 1, sequentialIndex: 1, verticalIndex: 1, - interactor: CourseInteractor.mock, + interactor: CourseInteractor.mock, + config: ConfigMock(), router: CourseRouterMock(), analytics: CourseAnalyticsMock(), connectivity: Connectivity(), + storage: CourseStorageMock(), manager: DownloadManagerMock() ) diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index e9afacbc1..d75b96530 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -9,8 +9,8 @@ import Foundation import SwiftUI import Core import Discussion -import Swinject import Combine +import Theme public struct CourseUnitView: View { @@ -23,135 +23,67 @@ public struct CourseUnitView: View { } } } - @State var offsetView: CGFloat = 0 + @State var offsetView: CGPoint = .zero @State var showDiscussion: Bool = false - @Environment(\.presentationMode) private var presentationMode - + @Environment(\.isPresented) private var isPresented + @Environment(\.isHorizontal) private var isHorizontal private let sectionName: String public let playerStateSubject = CurrentValueSubject(nil) - public init(viewModel: CourseUnitViewModel, - sectionName: String) { + //Dropdown parameters + @State var showDropdown: Bool = false + private let portraitTopSpacing: CGFloat = 60 + private let landscapeTopSpacing: CGFloat = 75 + + let isDropdownActive: Bool + + var sequenceTitle: String { + let chapter = viewModel.chapters[viewModel.chapterIndex] + let sequence = chapter.childs[viewModel.sequentialIndex] + return sequence.displayName + } + + var unitTitle: String { + let chapter = viewModel.chapters[viewModel.chapterIndex] + let sequence = chapter.childs[viewModel.sequentialIndex] + let unit = sequence.childs[viewModel.verticalIndex] + return unit.displayName + } + + var isDropdownAvailable: Bool { + viewModel.verticals.count > 1 + } + + var isHorizontalNavigation: Bool { + viewModel.courseUnitProgressEnabled + } + + public init( + viewModel: CourseUnitViewModel, + sectionName: String, + isDropdownActive: Bool = false + ) { self.viewModel = viewModel self.sectionName = sectionName + self.isDropdownActive = isDropdownActive viewModel.loadIndex() viewModel.nextTitles() } - + public var body: some View { ZStack(alignment: .top) { // MARK: - Page Body ZStack(alignment: .bottom) { GeometryReader { reader in VStack(spacing: 0) { - VStack {}.frame(height: 100) - LazyVStack(spacing: 0) { - let data = Array(viewModel.verticals[viewModel.verticalIndex].childs.enumerated()) - ForEach(data, id: \.offset) { index, block in - VStack(spacing: 0) { - if index >= viewModel.index - 1 && index <= viewModel.index + 1 { - switch LessonType.from(block) { - // MARK: YouTube - case let .youtube(url, blockID): - if viewModel.connectivity.isInternetAvaliable { - YouTubeView( - name: block.displayName, - url: url, - courseID: viewModel.courseID, - blockID: blockID, - playerStateSubject: playerStateSubject, - languages: block.subtitles ?? [], - isOnScreen: index == viewModel.index - ).frameLimit() - Spacer(minLength: 100) - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } - // MARK: Encoded Video - case let .video(encodedUrl, blockID): - let url = viewModel.urlForVideoFileOrFallback( - blockId: blockID, - url: encodedUrl - ) - if viewModel.connectivity.isInternetAvaliable || url?.isFileURL == true { - EncodedVideoView( - name: block.displayName, - url: url, - courseID: viewModel.courseID, - blockID: blockID, - playerStateSubject: playerStateSubject, - languages: block.subtitles ?? [], - isOnScreen: index == viewModel.index - ).frameLimit() - Spacer(minLength: 100) - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } - // MARK: Web - case .web(let url): - if viewModel.connectivity.isInternetAvaliable { - WebView(url: url, viewModel: viewModel) - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } - // MARK: Unknown - case .unknown(let url): - if viewModel.connectivity.isInternetAvaliable { - UnknownView(url: url, viewModel: viewModel) - Spacer() - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } - // MARK: Discussion - case let .discussion(blockID, blockKey, title): - if viewModel.connectivity.isInternetAvaliable { - VStack { - if showDiscussion { - DiscussionView( - id: viewModel.courseID, - blockID: blockID, - blockKey: blockKey, - title: title, - viewModel: viewModel - ) - Spacer(minLength: 100) - } else { - VStack { - Color.clear - } - } - }.frameLimit() - } else { - NoInternetView(playerStateSubject: playerStateSubject) - } - } - } else { - EmptyView() - } - } - .frame(height: reader.size.height) - .id(index) - } - } - .offset(y: offsetView) - .clipped() - .onChange(of: viewModel.index, perform: { index in - DispatchQueue.main.async { - withAnimation(Animation.easeInOut(duration: 0.2)) { - offsetView = -(reader.size.height * CGFloat(index)) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - showDiscussion = viewModel.selectedLesson().type == .discussion - } - } - } - - }) - - }.frame(maxWidth: .infinity) - .clipped() - + topInset(reader: reader) + content(reader: reader) + } + .frame(maxWidth: .infinity) + .clipped() + // MARK: Progress Dots - if viewModel.verticals[viewModel.verticalIndex].childs.count > 1 { + if !viewModel.courseUnitProgressEnabled { LessonProgressView(viewModel: viewModel) } } @@ -166,7 +98,7 @@ public struct CourseUnitView: View { } Text(alertMessage ?? "") }.shadowCardStyle(bgColor: Theme.Colors.accentColor, - textColor: .white) + textColor: Theme.Colors.white) .transition(.move(edge: .bottom)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { @@ -176,36 +108,325 @@ public struct CourseUnitView: View { } } } - - // MARK: - Course Navigation - VStack { - CourseNavigationView( - sectionName: sectionName, - viewModel: viewModel, - playerStateSubject: playerStateSubject - ).padding(.bottom, 30) - .frameLimit(sizePortrait: 420) - }.frame(maxWidth: .infinity) - .onRightSwipeGesture { - playerStateSubject.send(VideoPlayerState.kill) - viewModel.router.back() - } + courseNavigation } .onDisappear { - if !presentationMode.wrappedValue.isPresented { + if !isPresented { playerStateSubject.send(VideoPlayerState.kill) } } + if isDropdownActive && showDropdown { + CourseUnitVerticalsDropdownView( + verticals: viewModel.verticals, + currentIndex: viewModel.verticalIndex, + offsetY: isHorizontal ? landscapeTopSpacing : portraitTopSpacing, + showDropdown: $showDropdown + ) { [weak viewModel] vertical in + viewModel?.route(to: vertical) + } + } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) + .ignoresSafeArea(.all, edges: .bottom) + .onRightSwipeGesture { + playerStateSubject.send(VideoPlayerState.kill) + viewModel.router.back() + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + showDiscussion = viewModel.selectedLesson().type == .discussion + } + } + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) .navigationTitle("") - .ignoresSafeArea() - .background( - Theme.Colors.background - .ignoresSafeArea() + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + .dropdownAnimation(isActive: isDropdownActive, value: showDropdown) + } + + // MARK: - Content + + private func topInset(reader: GeometryProxy) -> some View { + VStack { Theme.Colors.background } + .frame( + width: reader.size.width, + height: isHorizontal ? + (viewModel.courseUnitProgressEnabled ? 78 : 75) : + (viewModel.courseUnitProgressEnabled ? 68 : 50) ) } + + // swiftlint:disable function_body_length + @ViewBuilder + private func content(reader: GeometryProxy) -> some View { + let alignment = UnitAlignment(horizontalAlignment: .top, verticalAlignment: .leading) + let offset = viewOffset(for: viewModel.index, with: reader.size, insets: reader.safeAreaInsets) + UnitStack(isVerticalNavigation: !isHorizontalNavigation, alignment: alignment, spacing: 0) { + let data = Array(viewModel.verticals[viewModel.verticalIndex].childs.enumerated()) + ForEach(data, id: \.offset) { index, block in + VStack(spacing: 0) { + if isDropdownActive { + dropdown(block: block) + } + switch LessonType.from(block, streamingQuality: viewModel.streamingQuality) { + // MARK: YouTube + case let .youtube(url, blockID): + if index >= viewModel.index - 1 && index <= viewModel.index + 1 { + if viewModel.connectivity.isInternetAvaliable { + YouTubeView( + name: block.displayName, + url: url, + courseID: viewModel.courseID, + blockID: blockID, + playerStateSubject: playerStateSubject, + languages: block.subtitles ?? [], + isOnScreen: index == viewModel.index + ) + .frameLimit() + if !isHorizontal { + Spacer(minLength: 150) + } + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } + + } else { + EmptyView() + } + // MARK: Encoded Video + case let .video(encodedUrl, blockID): + if index == viewModel.index { + let url = viewModel.urlForVideoFileOrFallback( + blockId: blockID, + url: encodedUrl + ) + if viewModel.connectivity.isInternetAvaliable || url?.isFileURL == true { + EncodedVideoView( + name: block.displayName, + url: url, + courseID: viewModel.courseID, + blockID: blockID, + playerStateSubject: playerStateSubject, + languages: block.subtitles ?? [], + isOnScreen: index == viewModel.index + ) + .padding(.top, 5) + .frameLimit() + if !isHorizontal { + Spacer(minLength: 150) + } + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } + } + // MARK: Web + case let .web(url, injections): + if index >= viewModel.index - 1 && index <= viewModel.index + 1 { + if viewModel.connectivity.isInternetAvaliable { + WebView( + url: url, + injections: injections, + roundedBackgroundEnabled: !viewModel.courseUnitProgressEnabled + ) + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } + } else { + EmptyView() + } + // MARK: Unknown + case .unknown(let url): + if index >= viewModel.index - 1 && index <= viewModel.index + 1 { + if viewModel.connectivity.isInternetAvaliable { + ScrollView(showsIndicators: false) { + UnknownView(url: url, viewModel: viewModel) + Spacer() + .frame(minHeight: 100) + } + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } + } else { + EmptyView() + } + // MARK: Discussion + case let .discussion(blockID, blockKey, title): + if index >= viewModel.index - 1 && index <= viewModel.index + 1 { + if viewModel.connectivity.isInternetAvaliable { + VStack { + if showDiscussion { + DiscussionView( + id: viewModel.courseID, + blockID: blockID, + blockKey: blockKey, + title: title, + viewModel: viewModel + ) + Spacer(minLength: 100) + } else { + VStack { + Color.clear + } + } + }.frameLimit() + } else { + NoInternetView(playerStateSubject: playerStateSubject) + } + } else { + EmptyView() + } + } + + } + .frame( + width: isHorizontal ? reader.size.width - (isHorizontalNavigation ? 0 : 16) : reader.size.width, + height: reader.size.height + ) + .padding(.trailing, isHorizontal && isHorizontalNavigation ? reader.safeAreaInsets.trailing : 0) + .id(index) + } + } + .offset(x: offset.x, y: offset.y) + .animation(.easeInOut(duration: 0.2), value: viewModel.index) + .clipped() + .onChange( + of: viewModel.index, + perform: { _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + showDiscussion = viewModel.selectedLesson().type == .discussion + } + } + ) + + .onReceive( + NotificationCenter.default.publisher( + for: NSNotification.blockCompletion + ) + ) { _ in + let blockID = viewModel.selectedLesson().id + Task { + await viewModel.blockCompletionRequest(blockID: blockID) + } + } + } + // swiftlint:enable function_body_length + + private func viewOffset(for index: Int, with size: CGSize, insets: EdgeInsets) -> CGPoint { + let rightInset = (isHorizontal ? insets.trailing * CGFloat(index) : 0) + let x: CGFloat = isHorizontalNavigation ? -(size.width * CGFloat(index) + rightInset) : 0 + let y: CGFloat = isHorizontalNavigation ? 0 : -(size.height * CGFloat(index)) + return CGPoint(x: x, y: y) + } + + private func dropdown(block: CourseBlock) -> some View { + HStack { + if block.type == .video { + let title = block.displayName + Text(title) + .lineLimit(1) + .font(Theme.Fonts.titleLarge) + .foregroundStyle(Theme.Colors.textPrimary) + .padding(.vertical, 10) + .padding(.horizontal, 20) + Spacer() + } + } + } + + // MARK: - Course Navigation + + private var courseNavigation: some View { + VStack(spacing: 0) { + ZStack { + if !isDropdownActive { + GeometryReader { reader in + VStack { + HStack { + let currentBlock = viewModel.verticals[viewModel.verticalIndex] + .childs[viewModel.index] + if currentBlock.type == .video { + let title = currentBlock.displayName + Text(title) + .lineLimit(1) + .font(Theme.Fonts.titleLarge) + .foregroundStyle(Theme.Colors.textPrimary) + .padding(.leading, isHorizontal ? 30 : 42) + .padding(.top, isHorizontal ? 14 : 2) + Spacer() + } + } + .frame(maxWidth: isHorizontal ? reader.size.width * 0.5 : nil) + Spacer() + } + } + } + navigationBar + courseNavigationView + } + } + .frame(maxWidth: .infinity) + } + + private var navigationBar: some View { + VStack(spacing: 0) { + ZStack(alignment: .bottom) { + NavigationBar( + title: isDropdownActive ? sequenceTitle : "", + leftButtonAction: { + viewModel.router.back() + playerStateSubject.send(VideoPlayerState.kill) + } + ) + .padding(.top, isHorizontal ? 10 : 0) + .padding(.leading, isHorizontal ? -16 : 0) + + if isDropdownActive { + CourseUnitDropDownTitle( + title: unitTitle, + isAvailable: isDropdownAvailable, + showDropdown: $showDropdown + ) + .padding(.bottom, 0) + .padding(.horizontal, 48) + } + } + .background(Theme.Colors.background) + .padding(.trailing, isHorizontal ? 215 : 0) + + if viewModel.courseUnitProgressEnabled { + LessonLineProgressView(viewModel: viewModel) + .padding(.top, 4) + } + Spacer() + } + } + + private var courseNavigationView: some View { + HStack(alignment: .center) { + if isHorizontal { + Spacer() + } + VStack { + if !isHorizontal { + Spacer() + } + CourseNavigationView( + sectionName: sectionName, + viewModel: viewModel, + playerStateSubject: playerStateSubject + ) + if isHorizontal { + Spacer() + } + }//.frame(height: isHorizontal ? nil : 44) + + .padding(.bottom, isHorizontal ? 0 : 50) + .padding(.top, isHorizontal ? 12 : 0) + } + .frameLimit(sizePortrait: 420) + } } #if DEBUG @@ -223,8 +444,7 @@ struct CourseUnitView_Previews: PreviewProvider { type: .video, displayName: "Lesson 1", studentUrl: "", - videoUrl: nil, - youTubeUrl: nil + encodedVideo: nil ), CourseBlock( blockId: "2", @@ -236,8 +456,7 @@ struct CourseUnitView_Previews: PreviewProvider { type: .video, displayName: "Lesson 2", studentUrl: "2", - videoUrl: nil, - youTubeUrl: nil + encodedVideo: nil ), CourseBlock( blockId: "3", @@ -249,8 +468,7 @@ struct CourseUnitView_Previews: PreviewProvider { type: .unknown, displayName: "Lesson 3", studentUrl: "3", - videoUrl: nil, - youTubeUrl: nil + encodedVideo: nil ), CourseBlock( blockId: "4", @@ -262,8 +480,7 @@ struct CourseUnitView_Previews: PreviewProvider { type: .unknown, displayName: "4", studentUrl: "4", - videoUrl: nil, - youTubeUrl: nil + encodedVideo: nil ), ] @@ -331,9 +548,11 @@ struct CourseUnitView_Previews: PreviewProvider { sequentialIndex: 0, verticalIndex: 0, interactor: CourseInteractor.mock, + config: ConfigMock(), router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity(), + connectivity: Connectivity(), + storage: CourseStorageMock(), manager: DownloadManagerMock() ), sectionName: "") } diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index e48e3ab32..51e234e18 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -9,33 +9,37 @@ import SwiftUI import Core public enum LessonType: Equatable { - case web(String) - case youtube(viewYouTubeUrl: String, blockID: String) + case web(url: String, injections: [WebviewInjection]) + case youtube(youtubeVideoUrl: String, blockID: String) case video(videoUrl: String, blockID: String) case unknown(String) case discussion(String, String, String) - static func from(_ block: CourseBlock) -> Self { + static func from(_ block: CourseBlock, streamingQuality: StreamingQuality) -> Self { switch block.type { case .course, .chapter, .vertical, .sequential, .unknown: return .unknown(block.studentUrl) case .html: - return .web(block.studentUrl) + return .web(url: block.studentUrl, injections: [.ajaxCallback]) case .discussion: return .discussion(block.topicId ?? "", block.id, block.displayName) case .video: - if block.youTubeUrl != nil, let encodedVideo = block.videoUrl { + if block.encodedVideo?.youtubeVideoUrl != nil, let encodedVideo = block.encodedVideo?.video(streamingQuality: streamingQuality)?.url { return .video(videoUrl: encodedVideo, blockID: block.id) - } else if let viewYouTubeUrl = block.youTubeUrl { - return .youtube(viewYouTubeUrl: viewYouTubeUrl, blockID: block.id) - } else if let encodedVideo = block.videoUrl { + } else if let youtubeVideoUrl = block.encodedVideo?.youtubeVideoUrl { + return .youtube(youtubeVideoUrl: youtubeVideoUrl, blockID: block.id) + } else if let encodedVideo = block.encodedVideo?.video(streamingQuality: streamingQuality)?.url { return .video(videoUrl: encodedVideo, blockID: block.id) } else { return .unknown(block.studentUrl) } case .problem: - return .web(block.studentUrl) + return .web(url: block.studentUrl, injections: [.ajaxCallback]) + case .dragAndDropV2: + return .web(url: block.studentUrl, injections: [.ajaxCallback, .dragAndDropCss]) + case .survey: + return .web(url: block.studentUrl, injections: [.ajaxCallback, .surveyCSS]) } } } @@ -46,7 +50,13 @@ public class CourseUnitViewModel: ObservableObject { case next case previous } - + + struct VerticalData { + var chapterIndex: Int + var sequentialIndex: Int + var verticalIndex: Int + } + var verticals: [CourseVertical] var verticalIndex: Int var courseName: String @@ -66,18 +76,28 @@ public class CourseUnitViewModel: ObservableObject { private let interactor: CourseInteractorProtocol let router: CourseRouter + let config: ConfigProtocol let analytics: CourseAnalytics let connectivity: ConnectivityProtocol + let storage: CourseStorage private let manager: DownloadManagerProtocol private var subtitlesDownloaded: Bool = false let chapters: [CourseChapter] let chapterIndex: Int let sequentialIndex: Int - + + var streamingQuality: StreamingQuality { + storage.userSettings?.streamingQuality ?? .auto + } + func loadIndex() { index = selectLesson() } - + + var courseUnitProgressEnabled: Bool { + config.uiComponents.courseUnitProgressEnabled + } + public init( lessonID: String, courseID: String, @@ -87,9 +107,11 @@ public class CourseUnitViewModel: ObservableObject { sequentialIndex: Int, verticalIndex: Int, interactor: CourseInteractorProtocol, + config: ConfigProtocol, router: CourseRouter, analytics: CourseAnalytics, connectivity: ConnectivityProtocol, + storage: CourseStorage, manager: DownloadManagerProtocol ) { self.lessonID = lessonID @@ -101,10 +123,12 @@ public class CourseUnitViewModel: ObservableObject { self.verticalIndex = verticalIndex self.verticals = chapters[chapterIndex].childs[sequentialIndex].childs self.interactor = interactor + self.config = config self.router = router self.analytics = analytics self.connectivity = connectivity self.manager = manager + self.storage = storage } private func selectLesson() -> Int { @@ -147,6 +171,7 @@ public class CourseUnitViewModel: ObservableObject { func blockCompletionRequest(blockID: String) async { do { try await interactor.blockCompletionRequest(courseID: courseID, blockID: blockID) + setBlockCompletionForSelectedLesson() } catch let error { if error.isInternetError || error is NoCachedDataError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection @@ -155,7 +180,7 @@ public class CourseUnitViewModel: ObservableObject { } } } - + func nextTitles() { if index != 0 { previousLesson = verticals[verticalIndex].childs[index - 1].displayName @@ -180,4 +205,97 @@ public class CourseUnitViewModel: ObservableObject { func trackFinishVerticalBackToOutlineClicked() { analytics.finishVerticalBackToOutlineClicked(courseId: courseID, courseName: courseName) } + + // MARK: Navigation to next vertical + var nextData: VerticalData? { + nextData( + from: VerticalData( + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + verticalIndex: verticalIndex + ) + ) + } + + private func chapter(for data: VerticalData) -> CourseChapter? { + guard data.chapterIndex >= 0 && data.chapterIndex < chapters.count else { return nil } + return chapters[data.chapterIndex] + } + + private func sequential(for data: VerticalData) -> CourseSequential? { + guard let chapter = chapter(for: data), + data.sequentialIndex >= 0 && data.sequentialIndex < chapter.childs.count + else { return nil } + return chapter.childs[data.sequentialIndex] + } + + func vertical(for data: VerticalData) -> CourseVertical? { + guard let sequential = sequential(for: data), + data.verticalIndex >= 0 && data.verticalIndex < sequential.childs.count + else { return nil } + return sequential.childs[data.verticalIndex] + } + + private func sequentials(for data: VerticalData) -> [CourseSequential]? { + guard let chapter = chapter(for: data) else { return nil } + return chapter.childs + } + + private func verticals(for data: VerticalData) -> [CourseVertical]? { + guard let sequential = sequential(for: data) else { return nil } + return sequential.childs + } + + private func nextData(from data: VerticalData) -> VerticalData? { + var resultData: VerticalData = data + if let verticals = verticals(for: data), verticals.count > data.verticalIndex + 1 { + resultData.verticalIndex = data.verticalIndex + 1 + } else if let sequentials = sequentials(for: data), sequentials.count > data.sequentialIndex + 1 { + resultData.sequentialIndex = data.sequentialIndex + 1 + resultData.verticalIndex = 0 + } else if chapters.count > data.chapterIndex + 1 { + resultData.chapterIndex = data.chapterIndex + 1 + resultData.sequentialIndex = 0 + resultData.verticalIndex = 0 + } else { + return nil + } + + if let vertical = vertical(for: resultData), vertical.childs.count > 0 { + return resultData + } else { + return nextData(from: resultData) + } + } + + private func setBlockCompletionForSelectedLesson() { + verticals[verticalIndex].childs[index].completion = 1.0 + NotificationCenter.default.post( + name: .onBlockCompletion, + object: nil, + userInfo: [ + "chapterID": chapters[chapterIndex].id, + "sequentialID": chapters[chapterIndex].childs[sequentialIndex].id, + "verticalID": chapters[chapterIndex].childs[sequentialIndex].childs[verticalIndex].id, + "blockID": verticals[verticalIndex].childs[index].id + ] + ) + } + + func route(to vertical: CourseVertical) { + if let index = verticals.firstIndex(where: { $0.id == vertical.id }), + let block = vertical.childs.first { + router.replaceCourseUnit( + courseName: courseName, + blockId: block.id, + courseID: courseID, + sectionName: block.displayName, + verticalIndex: index, + chapters: chapters, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex, + animated: false + ) + } + } } diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift new file mode 100644 index 000000000..612c164be --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift @@ -0,0 +1,97 @@ +// +// CourseUnitDropDownCell.swift +// Course +// +// Created by Vadim Kuznetsov on 4.12.23. +// + +import Core +import SwiftUI +import Theme + +struct CourseUnitDropDownCell: View { + var vertical: CourseVertical + var isLast: Bool = false + var isSelected: Bool = false + var action: () -> Void + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + var body: some View { + VStack(spacing: 0) { + Button(action: { + action() + }, label: { + HStack { + Group { + VStack { + if vertical.completion == 1 { + CoreAssets.finished.swiftUIImage + .renderingMode(.template) + .foregroundColor(.accentColor) + } + } + .frame(width: 25) + Text(vertical.displayName) + .font(Theme.Fonts.titleSmall) + .lineLimit(1) + .frame(alignment: .leading) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + CourseVerticalImageView(blocks: vertical.childs) + } + .foregroundColor(Theme.Colors.textPrimary) + } + }) + .padding(.horizontal, 20) + .padding(.vertical, 5) + if !isLast { + Divider() + .frame(height: 1) + .overlay(Theme.Colors.cardViewStroke) + .padding(.horizontal, 20) + .padding(.vertical, 0) + } + } + .padding(0) + .background( + isSelected ? Color.secondary.opacity(0.2) : Color.clear + ) + + } +} + +#if DEBUG +struct CourseUnitDropDownCell_Previews: PreviewProvider { + static var previews: some View { + let vertical = CourseVertical( + blockId: "1", + id: "1", + courseId: "123", + displayName: "Lesson 1", + type: .vertical, + completion: 1, + childs: [ + CourseBlock( + blockId: "1", + id: "1", + courseId: "123", + topicId: "1", + graded: false, + completion: 1, + type: .video, + displayName: "Lesson 1", + studentUrl: "", + encodedVideo: nil + ) + ] + ) + + CourseUnitDropDownCell( + vertical: vertical, + isLast: false, + isSelected: false, + action: {} + ) + } +} +#endif diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift new file mode 100644 index 000000000..60b0a501e --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift @@ -0,0 +1,149 @@ +// +// CourseUnitDropDownList.swift +// Course +// +// Created by Vadim Kuznetsov on 4.12.23. +// + +import Core +import SwiftUI +import Theme + +struct CourseUnitDropDownList: View where Content: View { + @ViewBuilder var content: () -> Content + + @State var height: CGFloat = 0 + + var scrollViewHeight: CGFloat { + height > 400 ? 400 : height + } + var body: some View { + VStack { + ScrollView { + VStack(spacing: 0) { + content() + } + .background( + GeometryReader { proxy in + Color.clear + .onAppear { + height = proxy.size.height + } + } + ) + } + } + .background(Theme.Colors.background) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .frame(height: scrollViewHeight) + .shadow(radius: 4) + } +} + +#if DEBUG +struct CourseUnitDropDownList_Previews: PreviewProvider { + static var previews: some View { + let blocks = [ + CourseBlock( + blockId: "1", + id: "1", + courseId: "123", + topicId: "1", + graded: false, + completion: 0, + type: .video, + displayName: "Lesson 1", + studentUrl: "", + encodedVideo: nil + ), + CourseBlock( + blockId: "2", + id: "2", + courseId: "123", + topicId: "2", + graded: false, + completion: 0, + type: .video, + displayName: "Lesson 2", + studentUrl: "2", + encodedVideo: nil + ), + CourseBlock( + blockId: "3", + id: "3", + courseId: "123", + topicId: "3", + graded: false, + completion: 0, + type: .unknown, + displayName: "Lesson 3", + studentUrl: "3", + encodedVideo: nil + ), + CourseBlock( + blockId: "4", + id: "4", + courseId: "123", + topicId: "4", + graded: false, + completion: 0, + type: .unknown, + displayName: "4", + studentUrl: "4", + encodedVideo: nil + ) + ] + + let verticals = [ + CourseVertical( + blockId: "1", + id: "1", + courseId: "123", + displayName: "First Unit", + type: .vertical, + completion: 0, + childs: blocks + ), + CourseVertical( + blockId: "2", + id: "2", + courseId: "123", + displayName: "Second Unit", + type: .vertical, + completion: 1, + childs: blocks + ), + CourseVertical( + blockId: "3", + id: "3", + courseId: "123", + displayName: "Third Unit", + type: .vertical, + completion: 0, + childs: blocks + ), + CourseVertical( + blockId: "4", + id: "4", + courseId: "123", + displayName: "Fourth Unit", + type: .vertical, + completion: 1, + childs: blocks + ) + ] + + CourseUnitDropDownList(content: { + ForEach(verticals, id: \.id) { vertical in + let isLast = verticals.last?.id == vertical.id + CourseUnitDropDownCell( + vertical: vertical, + isLast: isLast, + isSelected: false + ) {} + } + }) + .padding(10) + } +} +#endif diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownTitle.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownTitle.swift new file mode 100644 index 000000000..79b1e193f --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownTitle.swift @@ -0,0 +1,50 @@ +// +// CourseUnitDropDownTitle.swift +// Course +// +// Created by Vadim Kuznetsov on 4.12.23. +// + +import SwiftUI + +struct CourseUnitDropDownTitle: View { + var title: String + var isAvailable: Bool + @Binding var showDropdown: Bool + + var body: some View { + if isAvailable { + Button { + if isAvailable { + showDropdown.toggle() + } + } label: { + HStack { + Text(title) + .opacity(showDropdown ? 0.7 : 1.0) + .lineLimit(1) + if isAvailable { + Image(systemName: "chevron.down") + .dropdownArrowRotationAnimation(value: showDropdown) + } + } + } + .buttonStyle(.plain) + } else { + Text(title) + .lineLimit(1) + } + } +} + +#if DEBUG +struct CourseUnitDropDownTitle_Previews: PreviewProvider { + static var previews: some View { + CourseUnitDropDownTitle( + title: "Dropdown title", + isAvailable: true, + showDropdown: .constant(false) + ) + } +} +#endif diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift new file mode 100644 index 000000000..1bee39cc4 --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift @@ -0,0 +1,161 @@ +// +// CourseUnitVerticalsDropdownView.swift +// Course +// +// Created by Vadim Kuznetsov on 4.12.23. +// + +import Core +import SwiftUI + +struct CourseUnitVerticalsDropdownView: View { + var verticals: [CourseVertical] + var currentIndex: Int + var offsetY: CGFloat + @Binding var showDropdown: Bool + var action: (CourseVertical) -> Void + + var body: some View { + ZStack(alignment: .top) { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + showDropdown.toggle() + } + .simultaneousGesture( + DragGesture() + .onChanged { _ in + if showDropdown { showDropdown.toggle() } + } + ) + CourseUnitDropDownList(content: { + ForEach(verticals, id: \.id) { vertical in + let isLast = verticals.last?.id == vertical.id + let isSelected = vertical.id == verticals[currentIndex].id + CourseUnitDropDownCell( + vertical: vertical, + isLast: isLast, + isSelected: isSelected + ) { + if isSelected { + showDropdown.toggle() + return + } + action(vertical) + } + } + }) + .offset(y: offsetY) + .padding(.horizontal, 20) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .transition(.opacity) + } +} + +#if DEBUG +struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { + static var previews: some View { + let blocks = [ + CourseBlock( + blockId: "1", + id: "1", + courseId: "123", + topicId: "1", + graded: false, + completion: 0, + type: .video, + displayName: "Lesson 1", + studentUrl: "", + encodedVideo: nil + + ), + CourseBlock( + blockId: "2", + id: "2", + courseId: "123", + topicId: "2", + graded: false, + completion: 0, + type: .video, + displayName: "Lesson 2", + studentUrl: "2", + encodedVideo: nil + + ), + CourseBlock( + blockId: "3", + id: "3", + courseId: "123", + topicId: "3", + graded: false, + completion: 0, + type: .unknown, + displayName: "Lesson 3", + studentUrl: "3", + encodedVideo: nil + + ), + CourseBlock( + blockId: "4", + id: "4", + courseId: "123", + topicId: "4", + graded: false, + completion: 0, + type: .unknown, + displayName: "4", + studentUrl: "4", + encodedVideo: nil + ) + ] + + let verticals = [ + CourseVertical( + blockId: "1", + id: "1", + courseId: "123", + displayName: "First Unit", + type: .vertical, + completion: 0, + childs: blocks + ), + CourseVertical( + blockId: "2", + id: "2", + courseId: "123", + displayName: "Second Unit", + type: .vertical, + completion: 1, + childs: blocks + ), + CourseVertical( + blockId: "3", + id: "3", + courseId: "123", + displayName: "Third Unit", + type: .vertical, + completion: 0, + childs: blocks + ), + CourseVertical( + blockId: "4", + id: "4", + courseId: "123", + displayName: "Fourth Unit", + type: .vertical, + completion: 1, + childs: blocks + ) + ] + + CourseUnitVerticalsDropdownView( + verticals: verticals, + currentIndex: 0, + offsetY: 0, + showDropdown: .constant(true) + ) {_ in } + .padding(10) + } +} +#endif diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/DropdownAnimationModifier.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/DropdownAnimationModifier.swift new file mode 100644 index 000000000..d833d060e --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/DropdownAnimationModifier.swift @@ -0,0 +1,40 @@ +// +// DropdownAnimationModifier.swift +// Course +// +// Created by Vadim Kuznetsov on 5.12.23. +// + +import SwiftUI + +struct DropdownAnimationModifier: ViewModifier where V: Equatable { + var isActive: Bool + var value: V + func body(content: Content) -> some View { + if isActive { + content + .animation(.easeOut(duration: 0.2), value: value) + } else { + content + } + } +} + +struct DropdownArrowRotationModifier: ViewModifier { + var value: Bool + func body(content: Content) -> some View { + content + .rotationEffect(value ? .degrees(180) : .degrees(0)) + .animation(.easeOut(duration: 0.2), value: value) + } +} + +extension View { + func dropdownAnimation(isActive: Bool, value: V) -> some View where V: Equatable { + modifier(DropdownAnimationModifier(isActive: isActive, value: value)) + } + + func dropdownArrowRotationAnimation(value: Bool) -> some View { + modifier(DropdownArrowRotationModifier(value: value)) + } +} diff --git a/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift b/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift index 1bdc629fa..d4c9efbb6 100644 --- a/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift +++ b/Course/Course/Presentation/Unit/Subviews/EncodedVideoView.swift @@ -19,23 +19,16 @@ struct EncodedVideoView: View { let playerStateSubject: CurrentValueSubject let languages: [SubtitleUrl] let isOnScreen: Bool - + var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text(name) - .font(Theme.Fonts.titleLarge) - .padding(.horizontal, 24) - - let vm = Container.shared.resolve( - EncodedVideoPlayerViewModel.self, - arguments: url, - blockID, - courseID, - languages, - playerStateSubject - )! - EncodedVideoPlayer(viewModel: vm, isOnScreen: isOnScreen) - Spacer(minLength: 100) - } + let vm = Container.shared.resolve( + EncodedVideoPlayerViewModel.self, + arguments: url, + blockID, + courseID, + languages, + playerStateSubject + )! + EncodedVideoPlayer(viewModel: vm, isOnScreen: isOnScreen) } } diff --git a/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift b/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift new file mode 100644 index 000000000..17ae05de2 --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift @@ -0,0 +1,46 @@ +// +// LessonLineProgressView.swift +// Course +// +// Created by Eugene Yatsenko on 11.12.2023. +// + +import SwiftUI +import Theme + +struct LessonLineProgressView: View { + @ObservedObject var viewModel: CourseUnitViewModel + + @Environment (\.isHorizontal) private var isHorizontal + + init(viewModel: CourseUnitViewModel) { + self.viewModel = viewModel + } + + var body: some View { + ZStack(alignment: .bottom) { + Theme.Colors.background + HStack(spacing: 3) { + let vertical = viewModel.verticals[viewModel.verticalIndex] + let data = Array(vertical.childs.enumerated()) + ForEach(data, id: \.offset) { index, item in + let selected = viewModel.verticals[viewModel.verticalIndex].childs[index] + let isSelected = selected == viewModel.selectedLesson() + let isDone = item.completion == 1.0 || vertical.completion == 1.0 + if isSelected && isDone { + Theme.Colors.progressSelectedAndDone + .frame(height: 7) + } else if isSelected { + Theme.Colors.onProgress + .frame(height: 7) + } else if isDone { + Theme.Colors.progressDone + } else { + Theme.Colors.progressSkip + } + } + }.frame(height: 5) + } + .frame(height: 10) + } +} diff --git a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift index 57a881589..d1e848258 100644 --- a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift +++ b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift @@ -7,10 +7,13 @@ import SwiftUI import Core +import Theme struct LessonProgressView: View { @ObservedObject var viewModel: CourseUnitViewModel + @Environment (\.isHorizontal) private var isHorizontal + init(viewModel: CourseUnitViewModel) { self.viewModel = viewModel } @@ -36,7 +39,7 @@ struct LessonProgressView: View { } Spacer() } - .padding(.trailing, 6) + .padding(.trailing, isHorizontal ? 0 : 6) } } } diff --git a/Course/Course/Presentation/Unit/Subviews/UnitStack.swift b/Course/Course/Presentation/Unit/Subviews/UnitStack.swift new file mode 100644 index 000000000..a1cfb5959 --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/UnitStack.swift @@ -0,0 +1,69 @@ +// +// UnitStack.swift +// Course +// +// Created by Vadim Kuznetsov on 23.01.24. +// + +import SwiftUI + +public struct UnitAlignment: Equatable { + var horizontalAlignment: VerticalAlignment + var verticalAlignment: HorizontalAlignment +} + +public struct UnitStack: View where Content: View { + let alignment: UnitAlignment + let spacing: CGFloat? + let pinnedViews: PinnedScrollableViews + let content: () -> Content + var isVerticalNavigation: Bool + + public init( + isVerticalNavigation: Bool, + alignment: UnitAlignment, + spacing: CGFloat? = nil, + pinnedViews: PinnedScrollableViews = .init(), + @ViewBuilder content: @escaping () -> Content + ) { + self.isVerticalNavigation = isVerticalNavigation + self.alignment = alignment + self.spacing = spacing + self.pinnedViews = pinnedViews + self.content = content + } + + public var body: some View { + if isVerticalNavigation { + LazyVStack( + alignment: alignment.verticalAlignment, + spacing: spacing, + pinnedViews: pinnedViews, + content: content + ) + } else { + LazyHStack( + alignment: alignment.horizontalAlignment, + spacing: spacing, + pinnedViews: pinnedViews, + content: content + ) + } + } +} +#if DEBUG +#Preview { + VStack { + let alignment = UnitAlignment(horizontalAlignment: .top, verticalAlignment: .leading) + UnitStack(isVerticalNavigation: true, alignment: alignment) { + Text("First element") + Text("Second element") + } + Divider() + UnitStack(isVerticalNavigation: false, alignment: alignment) { + Text("First element") + Text("Second element") + } + } +} +#endif diff --git a/Course/Course/Presentation/Unit/Subviews/UnknownView.swift b/Course/Course/Presentation/Unit/Subviews/UnknownView.swift index 4f25de9da..6a41ae3e7 100644 --- a/Course/Course/Presentation/Unit/Subviews/UnknownView.swift +++ b/Course/Course/Presentation/Unit/Subviews/UnknownView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme struct UnknownView: View { let url: String diff --git a/Course/Course/Presentation/Unit/Subviews/WebView.swift b/Course/Course/Presentation/Unit/Subviews/WebView.swift index 9cdc59269..8d1b0c7ad 100644 --- a/Course/Course/Presentation/Unit/Subviews/WebView.swift +++ b/Course/Course/Presentation/Unit/Subviews/WebView.swift @@ -8,16 +8,29 @@ import SwiftUI import Swinject import Core +import Theme struct WebView: View { let url: String - let viewModel: CourseUnitViewModel - + let injections: [WebviewInjection] + var roundedBackgroundEnabled: Bool = true + var body: some View { VStack(spacing: 0) { - WebUnitView(url: url, viewModel: Container.shared.resolve(WebUnitViewModel.self)!) - Spacer(minLength: 5) + WebUnitView( + url: url, + viewModel: Container.shared.resolve(WebUnitViewModel.self)!, + injections: injections + ) + if roundedBackgroundEnabled { + Spacer(minLength: 5) + } + } + .if(roundedBackgroundEnabled) { view in + view.roundedBackgroundWeb( + strokeColor: Theme.Colors.textInputUnfocusedStroke, + maxIpadWidth: .infinity + ) } - .roundedBackground(strokeColor: .clear, maxIpadWidth: .infinity) } } diff --git a/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift index 94080fc32..934d75803 100644 --- a/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift +++ b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift @@ -9,6 +9,7 @@ import SwiftUI import Core import Combine import Swinject +import Theme struct YouTubeView: View { @@ -19,25 +20,17 @@ struct YouTubeView: View { let playerStateSubject: CurrentValueSubject let languages: [SubtitleUrl] let isOnScreen: Bool - + var body: some View { - VStack(alignment: .leading, spacing: 8) { - VStack(alignment: .leading) { - Text(name) - .font(Theme.Fonts.titleLarge) - .padding(.horizontal, 24) - - let vm = Container.shared.resolve( - YouTubeVideoPlayerViewModel.self, - arguments: url, - blockID, - courseID, - languages, - playerStateSubject - )! - YouTubeVideoPlayer(viewModel: vm, isOnScreen: isOnScreen) - Spacer(minLength: 100) - }.background(Theme.Colors.background) - } + let vm = Container.shared.resolve( + YouTubeVideoPlayerViewModel.self, + arguments: url, + blockID, + courseID, + languages, + playerStateSubject + )! + YouTubeVideoPlayer(viewModel: vm, isOnScreen: isOnScreen) + .background(Theme.Colors.background) } } diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index 251b59417..c40fcabfa 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -30,6 +30,7 @@ public struct EncodedVideoPlayer: View { @State private var isViewedOnce: Bool = false @State private var currentTime: Double = 0 @State private var isOrientationChanged: Bool = false + @State private var pause: Bool = false @State var showAlert = false @State var alertMessage: String? { @@ -40,6 +41,8 @@ public struct EncodedVideoPlayer: View { } } + @Environment(\.isHorizontal) private var isHorizontal + public init( viewModel: EncodedVideoPlayerViewModel, isOnScreen: Bool @@ -50,74 +53,84 @@ public struct EncodedVideoPlayer: View { public var body: some View { ZStack { - VStack(alignment: .leading) { - PlayerViewController( - videoURL: viewModel.url, - controller: viewModel.controller, - progress: { progress in - if progress >= 0.8 { - if !isViewedOnce { - Task { - await viewModel.blockCompletionRequest() - } - isViewedOnce = true + GeometryReader { reader in + VStack { + HStack { + VStack { + PlayerViewController( + videoURL: viewModel.url, + controller: viewModel.controller, + bitrate: viewModel.getVideoResolution(), + progress: { progress in + if progress >= 0.8 { + if !isViewedOnce { + Task { + await viewModel.blockCompletionRequest() + } + isViewedOnce = true + } + } + if progress == 1 { + viewModel.router.presentAppReview() + } + }, seconds: { seconds in + currentTime = seconds + }) + .statusBarHidden(false) + .aspectRatio(16 / 9, contentMode: .fit) + .frame(minWidth: isHorizontal ? reader.size.width * 0.6 : 380) + .cornerRadius(12) + if isHorizontal { + Spacer() } } - }, seconds: { seconds in - currentTime = seconds - }) - .aspectRatio(16 / 9, contentMode: .fit) - .cornerRadius(12) - .padding(.horizontal, 6) - .onReceive(NotificationCenter.Publisher( - center: .default, - name: UIDevice.orientationDidChangeNotification) - ) { _ in - if isOnScreen { - self.orientation = UIDevice.current.orientation - if self.orientation.isLandscape { - viewModel.controller.enterFullScreen(animated: true) - viewModel.controller.player?.play() - isOrientationChanged = true - } else { - if isOrientationChanged { - viewModel.controller.exitFullScreen(animated: true) - viewModel.controller.player?.pause() - isOrientationChanged = false - } + if isHorizontal { + SubtittlesView( + languages: viewModel.languages, + currentTime: $currentTime, + viewModel: viewModel, + scrollTo: { date in + viewModel.controller.player?.seek( + to: CMTime( + seconds: date.secondsSinceMidnight(), + preferredTimescale: 10000 + ) + ) + viewModel.controller.player?.play() + pauseScrolling() + currentTime = (date.secondsSinceMidnight() + 1) + }) } } - } - SubtittlesView(languages: viewModel.languages, - currentTime: $currentTime, - viewModel: viewModel) - Spacer() - if !orientation.isLandscape || idiom != .pad { - VStack {}.onAppear { - isLoading = false - alertMessage = CourseLocalization.Alert.rotateDevice + if !isHorizontal { + SubtittlesView( + languages: viewModel.languages, + currentTime: $currentTime, + viewModel: viewModel, + scrollTo: { date in + viewModel.controller.player?.seek( + to: CMTime( + seconds: date.secondsSinceMidnight(), + preferredTimescale: 10000 + ) + ) + viewModel.controller.player?.play() + pauseScrolling() + currentTime = (date.secondsSinceMidnight() + 1) + }) } } } - - // MARK: - Alert - if showAlert, let alertMessage { - VStack(alignment: .center) { - Spacer() - HStack(spacing: 6) { - CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) - Text(alertMessage) - }.shadowCardStyle(bgColor: Theme.Colors.snackbarInfoAlert, - textColor: .white) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - self.alertMessage = nil - showAlert = false - } - } - } + }.padding(.horizontal, isHorizontal ? 0 : 8) + .onDisappear { + viewModel.controller.player?.allowsExternalPlayback = false } + } + + private func pauseScrolling() { + pause = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.pause = false } } } @@ -133,7 +146,8 @@ struct EncodedVideoPlayer_Previews: PreviewProvider { languages: [], playerStateSubject: CurrentValueSubject(nil), interactor: CourseInteractor(repository: CourseRepositoryMock()), - router: CourseRouterMock(), + router: CourseRouterMock(), + appStorage: CoreStorageMock(), connectivity: Connectivity() ), isOnScreen: true diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift index b75a57384..6163c8f93 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -24,6 +24,7 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { playerStateSubject: CurrentValueSubject, interactor: CourseInteractorProtocol, router: CourseRouter, + appStorage: CoreStorage, connectivity: ConnectivityProtocol ) { self.url = url @@ -32,7 +33,8 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { courseID: courseID, languages: languages, interactor: interactor, - router: router, + router: router, + appStorage: appStorage, connectivity: connectivity) playerStateSubject.sink(receiveValue: { [weak self] state in @@ -46,4 +48,19 @@ public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { } }).store(in: &subscription) } + + func getVideoResolution() -> CGSize { + switch appStorage.userSettings?.streamingQuality { + case .auto: + return CGSize(width: 1280, height: 720) + case .low: + return CGSize(width: 640, height: 360) + case .medium: + return CGSize(width: 854, height: 480) + case .high: + return CGSize(width: 1280, height: 720) + case .none: + return CGSize(width: 1280, height: 720) + } + } } diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index 01a27e640..40938029f 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -11,17 +11,21 @@ import _AVKit_SwiftUI struct PlayerViewController: UIViewControllerRepresentable { var videoURL: URL? + var videoResolution: CGSize var controller: AVPlayerViewController var progress: ((Float) -> Void) var seconds: ((Double) -> Void) init( - videoURL: URL?, controller: AVPlayerViewController, + videoURL: URL?, + controller: AVPlayerViewController, + bitrate: CGSize, progress: @escaping ((Float) -> Void), seconds: @escaping ((Double) -> Void) ) { self.videoURL = videoURL self.controller = controller + self.videoResolution = bitrate self.progress = progress self.seconds = seconds } @@ -29,15 +33,12 @@ struct PlayerViewController: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> AVPlayerViewController { controller.modalPresentationStyle = .fullScreen controller.allowsPictureInPicturePlayback = true - controller.player = AVPlayer() - - addPeriodicTimeObserver( - controller, - currentProgress: { progress, seconds in - self.progress(progress) - self.seconds(seconds) - } - ) + let player = AVPlayer() + controller.player = player + context.coordinator.setPlayer(player) { progress, seconds in + self.progress(progress) + self.seconds(seconds) + } do { try AVAudioSession.sharedInstance().setCategory(.playback) @@ -48,38 +49,64 @@ struct PlayerViewController: UIViewControllerRepresentable { return controller } - private func addPeriodicTimeObserver( - _ controller: AVPlayerViewController, - currentProgress: @escaping ((Float, Double) -> Void) - ) { - let interval = CMTime( - seconds: 0.1, - preferredTimescale: CMTimeScale(NSEC_PER_SEC) - ) - - self.controller.player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in - var progress: Float = .zero - let currentSeconds = CMTimeGetSeconds(time) - guard let duration = controller.player?.currentItem?.duration else { return } - let totalSeconds = CMTimeGetSeconds(duration) - progress = Float(currentSeconds / totalSeconds) - currentProgress(progress, currentSeconds) + func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { + let asset = playerController.player?.currentItem?.asset as? AVURLAsset + if asset?.url.absoluteString != videoURL?.absoluteString { + let player = context.coordinator.player(from: playerController) + player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!)) + player?.currentItem?.preferredMaximumResolution = videoResolution + + context.coordinator.setPlayer(player) { progress, seconds in + self.progress(progress) + self.seconds(seconds) + } } } - func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { - DispatchQueue.main.async { - let asset = playerController.player?.currentItem?.asset as? AVURLAsset - if asset?.url.absoluteString != videoURL?.absoluteString { - if playerController.player == nil { - playerController.player = AVPlayer() - } - playerController.player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!)) - addPeriodicTimeObserver(playerController, currentProgress: { progress, seconds in - self.progress(progress) - self.seconds(seconds) - }) + func makeCoordinator() -> Coordinator { + Coordinator() + } + + static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: Coordinator) { + coordinator.setPlayer(nil) { _, _ in } + } + + class Coordinator { + var currentPlayer: AVPlayer? + var observer: Any? + + func player(from playerController: AVPlayerViewController) -> AVPlayer? { + var player = playerController.player + if player == nil { + player = AVPlayer() + player?.allowsExternalPlayback = true + playerController.player = player + } + return player + } + + func setPlayer(_ player: AVPlayer?, currentProgress: @escaping ((Float, Double) -> Void)) { + if let observer = observer { + currentPlayer?.removeTimeObserver(observer) + currentPlayer?.pause() + } + + let interval = CMTime( + seconds: 0.1, + preferredTimescale: CMTimeScale(NSEC_PER_SEC) + ) + + observer = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) {[weak player] time in + var progress: Float = .zero + let currentSeconds = CMTimeGetSeconds(time) + guard let duration = player?.currentItem?.duration else { return } + let totalSeconds = CMTimeGetSeconds(duration) + progress = Float(currentSeconds / totalSeconds) + currentProgress(progress, currentSeconds) } + + currentPlayer = player + } } } diff --git a/Course/Course/Presentation/Video/SubtittlesView.swift b/Course/Course/Presentation/Video/SubtittlesView.swift index 6501cb409..fb38221cc 100644 --- a/Course/Course/Presentation/Video/SubtittlesView.swift +++ b/Course/Course/Presentation/Video/SubtittlesView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme public struct Subtitle { var id: Int @@ -16,24 +17,30 @@ public struct Subtitle { public struct SubtittlesView: View { + @Environment (\.isHorizontal) private var isHorizontal + @ObservedObject private var viewModel: VideoPlayerViewModel + private var scrollTo: ((Date) -> Void) = { _ in } @Binding var currentTime: Double @State var id = 0 + @State var pause: Bool = false @State var languages: [SubtitleUrl] public init(languages: [SubtitleUrl], currentTime: Binding, - viewModel: VideoPlayerViewModel) { + viewModel: VideoPlayerViewModel, + scrollTo: @escaping (Date) -> Void) { self.languages = languages self.viewModel = viewModel self._currentTime = currentTime + self.scrollTo = scrollTo } public var body: some View { ScrollViewReader { scroll in - VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 16) { HStack { Text(viewModel.subtitles.isEmpty ? "" : CourseLocalization.Subtitles.title) .font(Theme.Fonts.titleMedium) @@ -51,40 +58,56 @@ public struct SubtittlesView: View { } } ZStack { + ScrollView { if viewModel.subtitles.count > 0 { VStack(alignment: .leading, spacing: 0) { ForEach(viewModel.subtitles, id: \.id) { subtitle in HStack { + Button(action: { + scrollTo(subtitle.fromTo.start) + pause = false + }, label: { Text(subtitle.text) .padding(.vertical, 16) .font(Theme.Fonts.bodyMedium) + .multilineTextAlignment(.leading) .foregroundColor(subtitle.fromTo.contains(Date(milliseconds: currentTime)) ? Theme.Colors.textPrimary : Theme.Colors.textSecondary) + .onChange(of: currentTime, perform: { _ in if subtitle.fromTo.contains(Date(milliseconds: currentTime)) { if id != subtitle.id { withAnimation { - scroll.scrollTo(subtitle.id, anchor: .top) + if !pause { + scroll.scrollTo(subtitle.id, anchor: .top) + } } } self.id = subtitle.id } }) + }) }.id(subtitle.id) } } - .introspect(.scrollView, on: .iOS(.v14, .v15, .v16, .v17), customize: { scrollView in - scrollView.isScrollEnabled = false - }) } - } - // Forced disable scrolling for iOS 14, 15 - Color.white.opacity(0) + }.simultaneousGesture( + DragGesture().onChanged({ _ in + pauseScrolling() + })) } }.padding(.horizontal, 24) - .padding(.top, 34) + .padding(.top, 16) + .padding(.bottom, isHorizontal ? 100 : 16) + } + } + + private func pauseScrolling() { + pause = true + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + self.pause = false } } } @@ -101,9 +124,10 @@ struct SubtittlesView_Previews: PreviewProvider { blockID: "", courseID: "", languages: [], interactor: CourseInteractor(repository: CourseRepositoryMock()), - router: CourseRouterMock(), + router: CourseRouterMock(), + appStorage: CoreStorageMock(), connectivity: Connectivity() - ) + ), scrollTo: {_ in } ) } } diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index cddcdba6c..445a7372c 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -13,11 +13,12 @@ public class VideoPlayerViewModel: ObservableObject { private var blockID: String private var courseID: String - + private let interactor: CourseInteractorProtocol public let connectivity: ConnectivityProtocol public let router: CourseRouter - + public let appStorage: CoreStorage + private var subtitlesDownloaded: Bool = false @Published var subtitles: [Subtitle] = [] var languages: [SubtitleUrl] @@ -30,13 +31,14 @@ public class VideoPlayerViewModel: ObservableObject { showError = errorMessage != nil } } - + public init( blockID: String, courseID: String, languages: [SubtitleUrl], interactor: CourseInteractorProtocol, router: CourseRouter, + appStorage: CoreStorage, connectivity: ConnectivityProtocol ) { self.blockID = blockID @@ -44,15 +46,19 @@ public class VideoPlayerViewModel: ObservableObject { self.languages = languages self.interactor = interactor self.router = router + self.appStorage = appStorage self.connectivity = connectivity self.prepareLanguages() } @MainActor func blockCompletionRequest() async { - let fullBlockID = "block-v1:\(courseID.dropFirst(10))+type@video+block@\(blockID)" do { - try await interactor.blockCompletionRequest(courseID: courseID, blockID: fullBlockID) + try await interactor.blockCompletionRequest(courseID: courseID, blockID: blockID) + NotificationCenter.default.post( + name: NSNotification.blockCompletion, + object: nil + ) } catch let error { if error.isInternetError || error is NoCachedDataError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index f3a886c72..08a868665 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -16,7 +16,6 @@ public struct YouTubeVideoPlayer: View { @StateObject private var viewModel: YouTubeVideoPlayerViewModel private var isOnScreen: Bool - @State private var showAlert = false @State @@ -28,75 +27,54 @@ public struct YouTubeVideoPlayer: View { } } + @Environment(\.isHorizontal) private var isHorizontal + public init(viewModel: YouTubeVideoPlayerViewModel, isOnScreen: Bool) { self._viewModel = StateObject(wrappedValue: { viewModel }()) self.isOnScreen = isOnScreen } public var body: some View { - ZStack { - VStack { - YouTubePlayerView( - viewModel.youtubePlayer, - transaction: .init(animation: .easeIn), - overlay: { _ in }) - .onAppear { - alertMessage = CourseLocalization.Alert.rotateDevice - } - .cornerRadius(12) - .padding(.horizontal, 6) - .aspectRatio(16 / 8.8, contentMode: .fit) - .onReceive(NotificationCenter.Publisher( - center: .default, name: UIDevice.orientationDidChangeNotification - )) { _ in - if isOnScreen { - let orientation = UIDevice.current.orientation - if orientation.isPortrait { - viewModel.youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: { - $0.playInline = true - $0.autoPlay = viewModel.play - $0.startTime = Int(viewModel.currentTime) - })) - } else { - viewModel.youtubePlayer.update(configuration: YouTubePlayer.Configuration(configure: { - $0.playInline = false - $0.autoPlay = true - $0.startTime = Int(viewModel.currentTime) - })) + ZStack { + GeometryReader { reader in + adaptiveStack(isHorizontal: isHorizontal) { + VStack { + YouTubePlayerView( + viewModel.youtubePlayer, + transaction: .init(animation: .easeIn), + overlay: { _ in }) + .onAppear { + alertMessage = CourseLocalization.Alert.rotateDevice + } + .cornerRadius(12) + .padding(.horizontal, isHorizontal ? 0 : 8) + .aspectRatio(16 / 8.8, contentMode: .fit) + .frame(minWidth: isHorizontal ? reader.size.width * 0.6 : 380) + // Adjust the width based on the horizontal state + if isHorizontal { + Spacer() + } } - } - } - SubtittlesView( - languages: viewModel.languages, - currentTime: $viewModel.currentTime, - viewModel: viewModel - ) - } - - if viewModel.isLoading { - ProgressBar(size: 40, lineWidth: 8) - } - - // MARK: - Alert - if showAlert, let alertMessage { - VStack(alignment: .center) { - Spacer() - HStack(spacing: 6) { - CoreAssets.rotateDevice.swiftUIImage.renderingMode(.template) - Text(alertMessage) - }.shadowCardStyle(bgColor: Theme.Colors.snackbarInfoAlert, - textColor: .white) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - self.alertMessage = nil - showAlert = false + ZStack { + SubtittlesView( + languages: viewModel.languages, + currentTime: $viewModel.currentTime, + viewModel: viewModel, scrollTo: { date in + viewModel.youtubePlayer.seek(to: date.secondsSinceMidnight(), allowSeekAhead: true) + viewModel.youtubePlayer.play() + viewModel.pauseScrolling() + viewModel.currentTime = date.secondsSinceMidnight() + 1 + } + ) + if viewModel.isLoading { + ProgressBar(size: 40, lineWidth: 8) + } } } } + } } - } } #if DEBUG @@ -110,7 +88,8 @@ struct YouTubeVideoPlayer_Previews: PreviewProvider { languages: [], playerStateSubject: CurrentValueSubject(nil), interactor: CourseInteractor(repository: CourseRepositoryMock()), - router: CourseRouterMock(), + router: CourseRouterMock(), + appStorage: CoreStorageMock(), connectivity: Connectivity()), isOnScreen: true) } diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift index 5aaacf7ff..f30c65f98 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift @@ -17,6 +17,7 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { private (set) var play = false @Published var isLoading: Bool = true @Published var currentTime: Double = 0 + @Published var pause: Bool = false private var subscription = Set() private var duration: Double? @@ -31,6 +32,7 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { playerStateSubject: CurrentValueSubject, interactor: CourseInteractorProtocol, router: CourseRouter, + appStorage: CoreStorage, connectivity: ConnectivityProtocol ) { self.url = url @@ -60,6 +62,7 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { languages: languages, interactor: interactor, router: router, + appStorage: appStorage, connectivity: connectivity ) @@ -68,11 +71,18 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { subscrube(playerStateSubject: playerStateSubject) } + func pauseScrolling() { + pause = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.pause = false + } + } + private func subscrube(playerStateSubject: CurrentValueSubject) { playerStateSubject.sink(receiveValue: { [weak self] state in switch state { case .pause: - self?.youtubePlayer.pause() + self?.youtubePlayer.stop() case .kill, .none: break } @@ -84,17 +94,23 @@ public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { youtubePlayer.currentTimePublisher(updateInterval: 0.1).sink(receiveValue: { [weak self] time in guard let self else { return } - self.currentTime = time + if !self.pause { + self.currentTime = time + } if let duration = self.duration { if (time / duration) >= 0.8 { if !isViewedOnce { Task { await self.blockCompletionRequest() + } isViewedOnce = true } } + if (time / duration) >= 0.999 { + self.router.presentAppReview() + } } }).store(in: &subscription) diff --git a/Course/Course/SwiftGen/Strings.swift b/Course/Course/SwiftGen/Strings.swift index f5719bf9d..a64b20db6 100644 --- a/Course/Course/SwiftGen/Strings.swift +++ b/Course/Course/SwiftGen/Strings.swift @@ -10,17 +10,31 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum CourseLocalization { + public enum Accessibility { + /// Cancel download + public static let cancelDownload = CourseLocalization.tr("Localizable", "ACCESSIBILITY.CANCEL_DOWNLOAD", fallback: "Cancel download") + /// Delete download + public static let deleteDownload = CourseLocalization.tr("Localizable", "ACCESSIBILITY.DELETE_DOWNLOAD", fallback: "Delete download") + /// Download + public static let download = CourseLocalization.tr("Localizable", "ACCESSIBILITY.DOWNLOAD", fallback: "Download") + } public enum Alert { + /// Accept + public static let accept = CourseLocalization.tr("Localizable", "ALERT.ACCEPT", fallback: "Accept") + /// Are you sure you want to delete all video(s) for + public static let deleteAllVideos = CourseLocalization.tr("Localizable", "ALERT.DELETE_ALL_VIDEOS", fallback: "Are you sure you want to delete all video(s) for") + /// Are you sure you want to delete video(s) for + public static let deleteVideos = CourseLocalization.tr("Localizable", "ALERT.DELETE_VIDEOS", fallback: "Are you sure you want to delete video(s) for") /// Rotate your device to view this video in full screen. public static let rotateDevice = CourseLocalization.tr("Localizable", "ALERT.ROTATE_DEVICE", fallback: "Rotate your device to view this video in full screen.") + /// Turning off the switch will stop downloading and delete all downloaded videos for + public static let stopDownloading = CourseLocalization.tr("Localizable", "ALERT.STOP_DOWNLOADING", fallback: "Turning off the switch will stop downloading and delete all downloaded videos for") } public enum Courseware { /// Back to outline public static let backToOutline = CourseLocalization.tr("Localizable", "COURSEWARE.BACK_TO_OUTLINE", fallback: "Back to outline") /// Continue public static let `continue` = CourseLocalization.tr("Localizable", "COURSEWARE.CONTINUE", fallback: "Continue") - /// Continue with: - public static let continueWith = CourseLocalization.tr("Localizable", "COURSEWARE.CONTINUE_WITH", fallback: "Continue with:") /// Course content public static let courseContent = CourseLocalization.tr("Localizable", "COURSEWARE.COURSE_CONTENT", fallback: "Course content") /// Course units @@ -35,14 +49,18 @@ public enum CourseLocalization { public static let next = CourseLocalization.tr("Localizable", "COURSEWARE.NEXT", fallback: "Next") /// Prev public static let previous = CourseLocalization.tr("Localizable", "COURSEWARE.PREVIOUS", fallback: "Prev") + /// Resume with: + public static let resumeWith = CourseLocalization.tr("Localizable", "COURSEWARE.RESUME_WITH", fallback: "Resume with:") /// Section “ public static let section = CourseLocalization.tr("Localizable", "COURSEWARE.SECTION", fallback: "Section “") } public enum CourseContainer { /// Course public static let course = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.COURSE", fallback: "Course") - /// Discussion - public static let discussion = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.DISCUSSION", fallback: "Discussion") + /// Dates + public static let dates = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.DATES", fallback: "Dates") + /// Discussions + public static let discussions = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.DISCUSSIONS", fallback: "Discussions") /// Handouts public static let handouts = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.HANDOUTS", fallback: "Handouts") /// Handouts In developing @@ -50,20 +68,36 @@ public enum CourseLocalization { /// Videos public static let videos = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.VIDEOS", fallback: "Videos") } - public enum Details { - /// Enroll now - public static let enrollNow = CourseLocalization.tr("Localizable", "DETAILS.ENROLL_NOW", fallback: "Enroll now") - /// You cannot enroll in this course because the enrollment date is over. - public static let enrollmentDateIsOver = CourseLocalization.tr("Localizable", "DETAILS.ENROLLMENT_DATE_IS_OVER", fallback: "You cannot enroll in this course because the enrollment date is over.") - /// Localizable.strings - /// Course - /// - /// Created by  Stepanok Ivan on 26.09.2022. - public static let title = CourseLocalization.tr("Localizable", "DETAILS.TITLE", fallback: "Course details") - /// View course - public static let viewCourse = CourseLocalization.tr("Localizable", "DETAILS.VIEW_COURSE", fallback: "View course") + public enum Download { + /// All videos downloaded + public static let allVideosDownloaded = CourseLocalization.tr("Localizable", "DOWNLOAD.ALL_VIDEOS_DOWNLOADED", fallback: "All videos downloaded") + /// You cannot change the download video quality when all videos are downloading + public static let changeQualityAlert = CourseLocalization.tr("Localizable", "DOWNLOAD.CHANGE_QUALITY_ALERT", fallback: "You cannot change the download video quality when all videos are downloading") + /// Download + public static let download = CourseLocalization.tr("Localizable", "DOWNLOAD.DOWNLOAD", fallback: "Download") + /// The videos you've selected are larger than 1 GB. Do you want to download these videos? + public static let downloadLargeFileMessage = CourseLocalization.tr("Localizable", "DOWNLOAD.DOWNLOAD_LARGE_FILE_MESSAGE", fallback: "The videos you've selected are larger than 1 GB. Do you want to download these videos?") + /// Download to device + public static let downloadToDevice = CourseLocalization.tr("Localizable", "DOWNLOAD.DOWNLOAD_TO_DEVICE", fallback: "Download to device") + /// Downloading videos... + public static let downloadingVideos = CourseLocalization.tr("Localizable", "DOWNLOAD.DOWNLOADING_VIDEOS", fallback: "Downloading videos...") + /// Downloads + public static let downloads = CourseLocalization.tr("Localizable", "DOWNLOAD.DOWNLOADS", fallback: "Downloads") + /// Your current download settings only allow downloads over Wi-Fi. + /// Please connect to a Wi-Fi network or change your download settings. + public static let noWifiMessage = CourseLocalization.tr("Localizable", "DOWNLOAD.NO_WIFI_MESSAGE", fallback: "Your current download settings only allow downloads over Wi-Fi.\nPlease connect to a Wi-Fi network or change your download settings.") + /// Remaining + public static let remaining = CourseLocalization.tr("Localizable", "DOWNLOAD.REMAINING", fallback: "Remaining") + /// Total + public static let total = CourseLocalization.tr("Localizable", "DOWNLOAD.TOTAL", fallback: "Total") + /// Untitled + public static let untitled = CourseLocalization.tr("Localizable", "DOWNLOAD.UNTITLED", fallback: "Untitled") + /// Videos + public static let videos = CourseLocalization.tr("Localizable", "DOWNLOAD.VIDEOS", fallback: "Videos") } public enum Error { + /// Course component not found, please reload + public static let componentNotFount = CourseLocalization.tr("Localizable", "ERROR.COMPONENT_NOT_FOUNT", fallback: "Course component not found, please reload") /// You are not connected to the Internet. Please check your Internet connection. public static let noInternet = CourseLocalization.tr("Localizable", "ERROR.NO_INTERNET", fallback: "You are not connected to the Internet. Please check your Internet connection.") /// Reload @@ -92,7 +126,10 @@ public enum CourseLocalization { public enum Outline { /// Certificate public static let certificate = CourseLocalization.tr("Localizable", "OUTLINE.CERTIFICATE", fallback: "Certificate") - /// Congratulations! + /// Localizable.strings + /// Course + /// + /// Created by  Stepanok Ivan on 26.09.2022. public static let congratulations = CourseLocalization.tr("Localizable", "OUTLINE.CONGRATULATIONS", fallback: "Congratulations!") /// This course hasn't started yet. public static let courseHasntStarted = CourseLocalization.tr("Localizable", "OUTLINE.COURSE_HASNT_STARTED", fallback: "This course hasn't started yet.") diff --git a/Course/Course/en.lproj/Localizable.strings b/Course/Course/en.lproj/Localizable.strings index 0f5edc88f..ee0be8b1c 100644 --- a/Course/Course/en.lproj/Localizable.strings +++ b/Course/Course/en.lproj/Localizable.strings @@ -6,11 +6,6 @@ */ -"DETAILS.TITLE" = "Course details"; -"DETAILS.VIEW_COURSE" = "View course"; -"DETAILS.ENROLL_NOW" = "Enroll now"; -"DETAILS.ENROLLMENT_DATE_IS_OVER" = "You cannot enroll in this course because the enrollment date is over."; - "OUTLINE.CONGRATULATIONS" = "Congratulations!"; "OUTLINE.PASSED_THE_COURSE" = "You've passed the course"; "OUTLINE.VIEW_CERTIFICATE" = "View certificate"; @@ -28,16 +23,22 @@ "COURSEWARE.SECTION" = "Section “"; "COURSEWARE.IS_FINISHED" = "“ is finished."; "COURSEWARE.CONTINUE" = "Continue"; -"COURSEWARE.CONTINUE_WITH" = "Continue with:"; +"COURSEWARE.RESUME_WITH" = "Resume with:"; "ERROR.NO_INTERNET" = "You are not connected to the Internet. Please check your Internet connection."; "ERROR.RELOAD" = "Reload"; +"ERROR.COMPONENT_NOT_FOUNT" = "Course component not found, please reload"; "ALERT.ROTATE_DEVICE" = "Rotate your device to view this video in full screen."; +"ALERT.ACCEPT" = "Accept"; +"ALERT.DELETE_ALL_VIDEOS" = "Are you sure you want to delete all video(s) for"; +"ALERT.DELETE_VIDEOS" = "Are you sure you want to delete video(s) for"; +"ALERT.STOP_DOWNLOADING" = "Turning off the switch will stop downloading and delete all downloaded videos for"; "COURSE_CONTAINER.COURSE" = "Course"; "COURSE_CONTAINER.VIDEOS" = "Videos"; -"COURSE_CONTAINER.DISCUSSION" = "Discussion"; +"COURSE_CONTAINER.DATES" = "Dates"; +"COURSE_CONTAINER.DISCUSSIONS" = "Discussions"; "COURSE_CONTAINER.HANDOUTS" = "Handouts"; "COURSE_CONTAINER.HANDOUTS_IN_DEVELOPING" = "Handouts In developing"; @@ -51,3 +52,21 @@ "NOT_AVALIABLE.BUTTON" = "Open in browser"; "SUBTITLES.TITLE" = "Subtitles"; + +"ACCESSIBILITY.DOWNLOAD" = "Download"; +"ACCESSIBILITY.CANCEL_DOWNLOAD" = "Cancel download"; +"ACCESSIBILITY.DELETE_DOWNLOAD" = "Delete download"; + +"DOWNLOAD.DOWNLOADS" = "Downloads"; +"DOWNLOAD.DOWNLOAD" = "Download"; +"DOWNLOAD.ALL_VIDEOS_DOWNLOADED" = "All videos downloaded"; +"DOWNLOAD.DOWNLOADING_VIDEOS" = "Downloading videos..."; +"DOWNLOAD.DOWNLOAD_TO_DEVICE" = "Download to device"; +"DOWNLOAD.VIDEOS" = "Videos"; +"DOWNLOAD.REMAINING" = "Remaining"; +"DOWNLOAD.UNTITLED"= "Untitled"; +"DOWNLOAD.TOTAL"= "Total"; + +"DOWNLOAD.CHANGE_QUALITY_ALERT" = "You cannot change the download video quality when all videos are downloading"; +"DOWNLOAD.DOWNLOAD_LARGE_FILE_MESSAGE" = "The videos you've selected are larger than 1 GB. Do you want to download these videos?"; +"DOWNLOAD.NO_WIFI_MESSAGE" = "Your current download settings only allow downloads over Wi-Fi.\nPlease connect to a Wi-Fi network or change your download settings."; diff --git a/Course/Course/uk.lproj/Localizable.strings b/Course/Course/uk.lproj/Localizable.strings index cedc987f1..e56d46a37 100644 --- a/Course/Course/uk.lproj/Localizable.strings +++ b/Course/Course/uk.lproj/Localizable.strings @@ -6,11 +6,6 @@ */ -"DETAILS.TITLE" = "Деталі курсу"; -"DETAILS.VIEW_COURSE" = "Переглянути курс"; -"DETAILS.ENROLL_NOW" = "Зареєструватися"; -"DETAILS.ENROLLMENT_DATE_IS_OVER" = "Ви не можете зареєструватися на цей курс, оскільки дата реєстрації закінчилася."; - "OUTLINE.CONGRATULATIONS" = "Вітаємо!"; "OUTLINE.PASSED_THE_COURSE" = "Ви пройшли курс"; "OUTLINE.VIEW_CERTIFICATE" = "Переглянути сертифікат"; @@ -27,16 +22,22 @@ "COURSEWARE.SECTION" = "Секція “"; "COURSEWARE.IS_FINISHED" = "“ завершена."; "COURSEWARE.CONTINUE" = "Продовжити"; -"COURSEWARE.CONTINUE_WITH" = "Продовжити далі:"; +"COURSEWARE.RESUME_WITH" = "Продовжити далі:"; "ERROR.NO_INTERNET" = "Ви не підключені до Інтернету. Перевірте підключення до Інтернету і спробуйте ще."; "ERROR.RELOAD" = "Перезавантажити"; +"ERROR.COMPONENT_NOT_FOUNT" = "Course component not found, please reload"; "ALERT.ROTATE_DEVICE" = "Поверніть пристрій, щоб переглянути це відео на весь екран."; +"ALERT.ACCEPT" = "Accept"; +"ALERT.DELETE_ALL_VIDEOS" = "Are you sure you want to delete all video(s) for"; +"ALERT.DELETE_VIDEOS" = "Are you sure you want to delete video(s) for"; +"ALERT.STOP_DOWNLOADING" = "Turning off the switch will stop downloading and delete all downloaded videos for"; "COURSE_CONTAINER.COURSE" = "Курс"; "COURSE_CONTAINER.VIDEOS" = "Всі відео"; -"COURSE_CONTAINER.DISCUSSION" = "Дискусії"; +"COURSE_CONTAINER.DATES" = "Dates"; +"COURSE_CONTAINER.DISCUSSIONS" = "Дискусії"; "COURSE_CONTAINER.HANDOUTS" = "Матеріали"; "COURSE_CONTAINER.HANDOUTS_IN_DEVELOPING" = "Матеріали в процесі розробки"; @@ -50,3 +51,21 @@ "NOT_AVALIABLE.BUTTON" = "Відкрити в браузері"; "SUBTITLES.TITLE" = "Субтитри"; + +"ACCESSIBILITY.DOWNLOAD" = "Скачати"; +"ACCESSIBILITY.CANCEL_DOWNLOAD" = "Скасувати завантаження"; +"ACCESSIBILITY.DELETE_DOWNLOAD" = "Видалити файл"; + +"DOWNLOAD.DOWNLOADS" = "Downloads"; +"DOWNLOAD.DOWNLOAD" = "Download"; +"DOWNLOAD.ALL_VIDEOS_DOWNLOADED" = "All videos downloaded"; +"DOWNLOAD.DOWNLOADING_VIDEOS" = "Downloading videos..."; +"DOWNLOAD.DOWNLOAD_TO_DEVICE" = "Download to device"; +"DOWNLOAD.VIDEOS" = "Videos"; +"DOWNLOAD.REMAINING" = "Remaining"; +"DOWNLOAD.UNTITLED"= "Untitled"; +"DOWNLOAD.TOTAL"= "Total"; + +"DOWNLOAD.CHANGE_QUALITY_ALERT" = "You cannot change the download video quality when all videos are downloading"; +"DOWNLOAD.DOWNLOAD_LARGE_FILE_MESSAGE" = "The videos you've selected are larger than 1 GB. Do you want to download these videos?"; +"DOWNLOAD.NO_WIFI_MESSAGE" = "Your current download settings only allow downloads over Wi-Fi.\nPlease connect to a Wi-Fi network or change your download settings."; diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index a4cf6b418..3acd549eb 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.8.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func login(externalToken: String, backend: String) throws -> User { + addInvocation(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) + let perform = methodPerformValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) as? (String, String) -> Void + perform?(`externalToken`, `backend`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) return Matcher.ComparisonResult(results) + case (.m_login__externalToken_externalTokenbackend_backend(let lhsExternaltoken, let lhsBackend), .m_login__externalToken_externalTokenbackend_backend(let rhsExternaltoken, let rhsBackend)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsExternaltoken, rhs: rhsExternaltoken, with: matcher), lhsExternaltoken, rhsExternaltoken, "externalToken")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) @@ -490,22 +539,28 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`controllers`) } - open func showMainScreen() { - addInvocation(.m_showMainScreen) - let perform = methodPerformValue(.m_showMainScreen) as? () -> Void - perform?() + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } - open func showLoginScreen() { - addInvocation(.m_showLoginScreen) - let perform = methodPerformValue(.m_showLoginScreen) as? () -> Void + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void perform?() } - open func showRegisterScreen() { - addInvocation(.m_showRegisterScreen) - let perform = methodPerformValue(.m_showRegisterScreen) as? () -> Void - perform?() + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } open func showForgotPasswordScreen() { @@ -514,6 +569,18 @@ open class BaseRouterMock: BaseRouter, Mock { perform?() } + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -545,10 +612,13 @@ open class BaseRouterMock: BaseRouter, Mock { case m_backWithFade case m_dismiss__animated_animated(Parameter) case m_removeLastView__controllers_controllers(Parameter) - case m_showMainScreen - case m_showLoginScreen - case m_showRegisterScreen + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) @@ -578,14 +648,37 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) return Matcher.ComparisonResult(results) - case (.m_showMainScreen, .m_showMainScreen): return .match + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) - case (.m_showLoginScreen, .m_showLoginScreen): return .match + case (.m_showStartupScreen, .m_showStartupScreen): return .match - case (.m_showRegisterScreen, .m_showRegisterScreen): return .match + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -630,10 +723,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return 0 case let .m_dismiss__animated_animated(p0): return p0.intValue case let .m_removeLastView__controllers_controllers(p0): return p0.intValue - case .m_showMainScreen: return 0 - case .m_showLoginScreen: return 0 - case .m_showRegisterScreen: return 0 + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue @@ -647,10 +743,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return ".backWithFade()" case .m_dismiss__animated_animated: return ".dismiss(animated:)" case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" - case .m_showMainScreen: return ".showMainScreen()" - case .m_showLoginScreen: return ".showLoginScreen()" - case .m_showRegisterScreen: return ".showRegisterScreen()" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" @@ -678,10 +777,13 @@ open class BaseRouterMock: BaseRouter, Mock { public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} - public static func showMainScreen() -> Verify { return Verify(method: .m_showMainScreen)} - public static func showLoginScreen() -> Verify { return Verify(method: .m_showLoginScreen)} - public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} @@ -707,18 +809,27 @@ open class BaseRouterMock: BaseRouter, Mock { public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) } - public static func showMainScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showMainScreen, performs: perform) + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } - public static func showLoginScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showLoginScreen, performs: perform) + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) } - public static func showRegisterScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showRegisterScreen, performs: perform) + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showForgotPasswordScreen, performs: perform) } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1046,24 +1157,6 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { - open func courseEnrollClicked(courseId: String, courseName: String) { - addInvocation(.m_courseEnrollClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) - let perform = methodPerformValue(.m_courseEnrollClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void - perform?(`courseId`, `courseName`) - } - - open func courseEnrollSuccess(courseId: String, courseName: String) { - addInvocation(.m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) - let perform = methodPerformValue(.m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void - perform?(`courseId`, `courseName`) - } - - open func viewCourseClicked(courseId: String, courseName: String) { - addInvocation(.m_viewCourseClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) - let perform = methodPerformValue(.m_viewCourseClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void - perform?(`courseId`, `courseName`) - } - open func resumeCourseTapped(courseId: String, courseName: String, blockId: String) { addInvocation(.m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`))) let perform = methodPerformValue(.m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(Parameter.value(`courseId`), Parameter.value(`courseName`), Parameter.value(`blockId`))) as? (String, String, String) -> Void @@ -1124,6 +1217,12 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { perform?(`courseId`, `courseName`) } + open func courseOutlineDatesTabClicked(courseId: String, courseName: String) { + addInvocation(.m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + open func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) { addInvocation(.m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) let perform = methodPerformValue(.m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void @@ -1138,9 +1237,6 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { fileprivate enum MethodType { - case m_courseEnrollClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) - case m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter, Parameter) - case m_viewCourseClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(Parameter, Parameter, Parameter) case m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter, Parameter, Parameter, Parameter) case m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(Parameter, Parameter, Parameter, Parameter) @@ -1151,29 +1247,12 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { - case (.m_courseEnrollClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) - return Matcher.ComparisonResult(results) - - case (.m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) - return Matcher.ComparisonResult(results) - - case (.m_viewCourseClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_viewCourseClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) - return Matcher.ComparisonResult(results) - case (.m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(let lhsCourseid, let lhsCoursename, let lhsBlockid), .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(let rhsCourseid, let rhsCoursename, let rhsBlockid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) @@ -1247,6 +1326,12 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) return Matcher.ComparisonResult(results) + case (.m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + case (.m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) @@ -1264,9 +1349,6 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { func intValue() -> Int { switch self { - case let .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue - case let .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue - case let .m_viewCourseClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue @@ -1277,15 +1359,13 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case let .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { - case .m_courseEnrollClicked__courseId_courseIdcourseName_courseName: return ".courseEnrollClicked(courseId:courseName:)" - case .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName: return ".courseEnrollSuccess(courseId:courseName:)" - case .m_viewCourseClicked__courseId_courseIdcourseName_courseName: return ".viewCourseClicked(courseId:courseName:)" case .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId: return ".resumeCourseTapped(courseId:courseName:blockId:)" case .m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName: return ".sequentialClicked(courseId:courseName:blockId:blockName:)" case .m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName: return ".verticalClicked(courseId:courseName:blockId:blockName:)" @@ -1296,6 +1376,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName: return ".finishVerticalBackToOutlineClicked(courseId:courseName:)" case .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineCourseTabClicked(courseId:courseName:)" case .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineVideosTabClicked(courseId:courseName:)" + case .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineDatesTabClicked(courseId:courseName:)" case .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineDiscussionTabClicked(courseId:courseName:)" case .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineHandoutsTabClicked(courseId:courseName:)" } @@ -1316,9 +1397,6 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public struct Verify { fileprivate var method: MethodType - public static func courseEnrollClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} - public static func courseEnrollSuccess(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} - public static func viewCourseClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_viewCourseClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func resumeCourseTapped(courseId: Parameter, courseName: Parameter, blockId: Parameter) -> Verify { return Verify(method: .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(`courseId`, `courseName`, `blockId`))} public static func sequentialClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter) -> Verify { return Verify(method: .m_sequentialClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`))} public static func verticalClicked(courseId: Parameter, courseName: Parameter, blockId: Parameter, blockName: Parameter) -> Verify { return Verify(method: .m_verticalClicked__courseId_courseIdcourseName_courseNameblockId_blockIdblockName_blockName(`courseId`, `courseName`, `blockId`, `blockName`))} @@ -1329,6 +1407,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func finishVerticalBackToOutlineClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func courseOutlineCourseTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func courseOutlineVideosTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func courseOutlineDatesTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func courseOutlineDiscussionTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func courseOutlineHandoutsTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} } @@ -1337,15 +1416,6 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { fileprivate var method: MethodType var performs: Any - public static func courseEnrollClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { - return Perform(method: .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) - } - public static func courseEnrollSuccess(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { - return Perform(method: .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) - } - public static func viewCourseClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { - return Perform(method: .m_viewCourseClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) - } public static func resumeCourseTapped(courseId: Parameter, courseName: Parameter, blockId: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { return Perform(method: .m_resumeCourseTapped__courseId_courseIdcourseName_courseNameblockId_blockId(`courseId`, `courseName`, `blockId`), performs: perform) } @@ -1376,6 +1446,9 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func courseOutlineVideosTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) } + public static func courseOutlineDatesTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } public static func courseOutlineDiscussionTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) } @@ -1501,22 +1574,6 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { - open func getCourseDetails(courseID: String) throws -> CourseDetails { - addInvocation(.m_getCourseDetails__courseID_courseID(Parameter.value(`courseID`))) - let perform = methodPerformValue(.m_getCourseDetails__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void - perform?(`courseID`) - var __value: CourseDetails - do { - __value = try methodReturnValue(.m_getCourseDetails__courseID_courseID(Parameter.value(`courseID`))).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getCourseDetails(courseID: String). Use given") - Failure("Stub return value not specified for getCourseDetails(courseID: String). Use given") - } catch { - throw error - } - return __value - } - open func getCourseBlocks(courseID: String) throws -> CourseStructure { addInvocation(.m_getCourseBlocks__courseID_courseID(Parameter.value(`courseID`))) let perform = methodPerformValue(.m_getCourseBlocks__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void @@ -1547,48 +1604,16 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { return __value } - open func getCourseDetailsOffline(courseID: String) throws -> CourseDetails { - addInvocation(.m_getCourseDetailsOffline__courseID_courseID(Parameter.value(`courseID`))) - let perform = methodPerformValue(.m_getCourseDetailsOffline__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void - perform?(`courseID`) - var __value: CourseDetails - do { - __value = try methodReturnValue(.m_getCourseDetailsOffline__courseID_courseID(Parameter.value(`courseID`))).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getCourseDetailsOffline(courseID: String). Use given") - Failure("Stub return value not specified for getCourseDetailsOffline(courseID: String). Use given") - } catch { - throw error - } - return __value - } - - open func getCourseBlocksOffline(courseID: String) throws -> CourseStructure { - addInvocation(.m_getCourseBlocksOffline__courseID_courseID(Parameter.value(`courseID`))) - let perform = methodPerformValue(.m_getCourseBlocksOffline__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + open func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure { + addInvocation(.m_getLoadedCourseBlocks__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_getLoadedCourseBlocks__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void perform?(`courseID`) var __value: CourseStructure do { - __value = try methodReturnValue(.m_getCourseBlocksOffline__courseID_courseID(Parameter.value(`courseID`))).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getCourseBlocksOffline(courseID: String). Use given") - Failure("Stub return value not specified for getCourseBlocksOffline(courseID: String). Use given") - } catch { - throw error - } - return __value - } - - open func enrollToCourse(courseID: String) throws -> Bool { - addInvocation(.m_enrollToCourse__courseID_courseID(Parameter.value(`courseID`))) - let perform = methodPerformValue(.m_enrollToCourse__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void - perform?(`courseID`) - var __value: Bool - do { - __value = try methodReturnValue(.m_enrollToCourse__courseID_courseID(Parameter.value(`courseID`))).casted() + __value = try methodReturnValue(.m_getLoadedCourseBlocks__courseID_courseID(Parameter.value(`courseID`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for enrollToCourse(courseID: String). Use given") - Failure("Stub return value not specified for enrollToCourse(courseID: String). Use given") + onFatalFailure("Stub return value not specified for getLoadedCourseBlocks(courseID: String). Use given") + Failure("Stub return value not specified for getLoadedCourseBlocks(courseID: String). Use given") } catch { throw error } @@ -1671,27 +1696,36 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { return __value } + open func getCourseDates(courseID: String) throws -> CourseDates { + addInvocation(.m_getCourseDates__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_getCourseDates__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: CourseDates + do { + __value = try methodReturnValue(.m_getCourseDates__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getCourseDates(courseID: String). Use given") + Failure("Stub return value not specified for getCourseDates(courseID: String). Use given") + } catch { + throw error + } + return __value + } + fileprivate enum MethodType { - case m_getCourseDetails__courseID_courseID(Parameter) case m_getCourseBlocks__courseID_courseID(Parameter) case m_getCourseVideoBlocks__fullStructure_fullStructure(Parameter) - case m_getCourseDetailsOffline__courseID_courseID(Parameter) - case m_getCourseBlocksOffline__courseID_courseID(Parameter) - case m_enrollToCourse__courseID_courseID(Parameter) + case m_getLoadedCourseBlocks__courseID_courseID(Parameter) case m_blockCompletionRequest__courseID_courseIDblockID_blockID(Parameter, Parameter) case m_getHandouts__courseID_courseID(Parameter) case m_getUpdates__courseID_courseID(Parameter) case m_resumeBlock__courseID_courseID(Parameter) case m_getSubtitles__url_urlselectedLanguage_selectedLanguage(Parameter, Parameter) + case m_getCourseDates__courseID_courseID(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { - case (.m_getCourseDetails__courseID_courseID(let lhsCourseid), .m_getCourseDetails__courseID_courseID(let rhsCourseid)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) - return Matcher.ComparisonResult(results) - case (.m_getCourseBlocks__courseID_courseID(let lhsCourseid), .m_getCourseBlocks__courseID_courseID(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) @@ -1702,17 +1736,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFullstructure, rhs: rhsFullstructure, with: matcher), lhsFullstructure, rhsFullstructure, "fullStructure")) return Matcher.ComparisonResult(results) - case (.m_getCourseDetailsOffline__courseID_courseID(let lhsCourseid), .m_getCourseDetailsOffline__courseID_courseID(let rhsCourseid)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) - return Matcher.ComparisonResult(results) - - case (.m_getCourseBlocksOffline__courseID_courseID(let lhsCourseid), .m_getCourseBlocksOffline__courseID_courseID(let rhsCourseid)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) - return Matcher.ComparisonResult(results) - - case (.m_enrollToCourse__courseID_courseID(let lhsCourseid), .m_enrollToCourse__courseID_courseID(let rhsCourseid)): + case (.m_getLoadedCourseBlocks__courseID_courseID(let lhsCourseid), .m_getLoadedCourseBlocks__courseID_courseID(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) return Matcher.ComparisonResult(results) @@ -1743,38 +1767,39 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSelectedlanguage, rhs: rhsSelectedlanguage, with: matcher), lhsSelectedlanguage, rhsSelectedlanguage, "selectedLanguage")) return Matcher.ComparisonResult(results) + + case (.m_getCourseDates__courseID_courseID(let lhsCourseid), .m_getCourseDates__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) default: return .none } } func intValue() -> Int { switch self { - case let .m_getCourseDetails__courseID_courseID(p0): return p0.intValue case let .m_getCourseBlocks__courseID_courseID(p0): return p0.intValue case let .m_getCourseVideoBlocks__fullStructure_fullStructure(p0): return p0.intValue - case let .m_getCourseDetailsOffline__courseID_courseID(p0): return p0.intValue - case let .m_getCourseBlocksOffline__courseID_courseID(p0): return p0.intValue - case let .m_enrollToCourse__courseID_courseID(p0): return p0.intValue + case let .m_getLoadedCourseBlocks__courseID_courseID(p0): return p0.intValue case let .m_blockCompletionRequest__courseID_courseIDblockID_blockID(p0, p1): return p0.intValue + p1.intValue case let .m_getHandouts__courseID_courseID(p0): return p0.intValue case let .m_getUpdates__courseID_courseID(p0): return p0.intValue case let .m_resumeBlock__courseID_courseID(p0): return p0.intValue case let .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(p0, p1): return p0.intValue + p1.intValue + case let .m_getCourseDates__courseID_courseID(p0): return p0.intValue } } func assertionName() -> String { switch self { - case .m_getCourseDetails__courseID_courseID: return ".getCourseDetails(courseID:)" case .m_getCourseBlocks__courseID_courseID: return ".getCourseBlocks(courseID:)" case .m_getCourseVideoBlocks__fullStructure_fullStructure: return ".getCourseVideoBlocks(fullStructure:)" - case .m_getCourseDetailsOffline__courseID_courseID: return ".getCourseDetailsOffline(courseID:)" - case .m_getCourseBlocksOffline__courseID_courseID: return ".getCourseBlocksOffline(courseID:)" - case .m_enrollToCourse__courseID_courseID: return ".enrollToCourse(courseID:)" + case .m_getLoadedCourseBlocks__courseID_courseID: return ".getLoadedCourseBlocks(courseID:)" case .m_blockCompletionRequest__courseID_courseIDblockID_blockID: return ".blockCompletionRequest(courseID:blockID:)" case .m_getHandouts__courseID_courseID: return ".getHandouts(courseID:)" case .m_getUpdates__courseID_courseID: return ".getUpdates(courseID:)" case .m_resumeBlock__courseID_courseID: return ".resumeBlock(courseID:)" case .m_getSubtitles__url_urlselectedLanguage_selectedLanguage: return ".getSubtitles(url:selectedLanguage:)" + case .m_getCourseDates__courseID_courseID: return ".getCourseDates(courseID:)" } } } @@ -1788,23 +1813,14 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { } - public static func getCourseDetails(courseID: Parameter, willReturn: CourseDetails...) -> MethodStub { - return Given(method: .m_getCourseDetails__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) - } public static func getCourseBlocks(courseID: Parameter, willReturn: CourseStructure...) -> MethodStub { return Given(method: .m_getCourseBlocks__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getCourseVideoBlocks(fullStructure: Parameter, willReturn: CourseStructure...) -> MethodStub { return Given(method: .m_getCourseVideoBlocks__fullStructure_fullStructure(`fullStructure`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getCourseDetailsOffline(courseID: Parameter, willReturn: CourseDetails...) -> MethodStub { - return Given(method: .m_getCourseDetailsOffline__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getCourseBlocksOffline(courseID: Parameter, willReturn: CourseStructure...) -> MethodStub { - return Given(method: .m_getCourseBlocksOffline__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func enrollToCourse(courseID: Parameter, willReturn: Bool...) -> MethodStub { - return Given(method: .m_enrollToCourse__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func getLoadedCourseBlocks(courseID: Parameter, willReturn: CourseStructure...) -> MethodStub { + return Given(method: .m_getLoadedCourseBlocks__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getHandouts(courseID: Parameter, willReturn: String?...) -> MethodStub { return Given(method: .m_getHandouts__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -1818,6 +1834,9 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { public static func getSubtitles(url: Parameter, selectedLanguage: Parameter, willReturn: [Subtitle]...) -> MethodStub { return Given(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func getCourseDates(courseID: Parameter, willReturn: CourseDates...) -> MethodStub { + return Given(method: .m_getCourseDates__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func getCourseVideoBlocks(fullStructure: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { let willReturn: [CourseStructure] = [] let given: Given = { return Given(method: .m_getCourseVideoBlocks__fullStructure_fullStructure(`fullStructure`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -1825,16 +1844,6 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { willProduce(stubber) return given } - public static func getCourseDetails(courseID: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_getCourseDetails__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func getCourseDetails(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getCourseDetails__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (CourseDetails).self) - willProduce(stubber) - return given - } public static func getCourseBlocks(courseID: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_getCourseBlocks__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -1845,36 +1854,16 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { willProduce(stubber) return given } - public static func getCourseDetailsOffline(courseID: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_getCourseDetailsOffline__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func getCourseDetailsOffline(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getCourseDetailsOffline__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (CourseDetails).self) - willProduce(stubber) - return given - } - public static func getCourseBlocksOffline(courseID: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_getCourseBlocksOffline__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) + public static func getLoadedCourseBlocks(courseID: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getLoadedCourseBlocks__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getCourseBlocksOffline(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getLoadedCourseBlocks(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getCourseBlocksOffline__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getLoadedCourseBlocks__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (CourseStructure).self) willProduce(stubber) return given } - public static func enrollToCourse(courseID: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_enrollToCourse__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func enrollToCourse(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_enrollToCourse__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Bool).self) - willProduce(stubber) - return given - } public static func blockCompletionRequest(courseID: Parameter, blockID: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_blockCompletionRequest__courseID_courseIDblockID_blockID(`courseID`, `blockID`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -1925,45 +1914,44 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { willProduce(stubber) return given } + public static func getCourseDates(courseID: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getCourseDates__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getCourseDates(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getCourseDates__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (CourseDates).self) + willProduce(stubber) + return given + } } public struct Verify { fileprivate var method: MethodType - public static func getCourseDetails(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseDetails__courseID_courseID(`courseID`))} public static func getCourseBlocks(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseBlocks__courseID_courseID(`courseID`))} public static func getCourseVideoBlocks(fullStructure: Parameter) -> Verify { return Verify(method: .m_getCourseVideoBlocks__fullStructure_fullStructure(`fullStructure`))} - public static func getCourseDetailsOffline(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseDetailsOffline__courseID_courseID(`courseID`))} - public static func getCourseBlocksOffline(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseBlocksOffline__courseID_courseID(`courseID`))} - public static func enrollToCourse(courseID: Parameter) -> Verify { return Verify(method: .m_enrollToCourse__courseID_courseID(`courseID`))} + public static func getLoadedCourseBlocks(courseID: Parameter) -> Verify { return Verify(method: .m_getLoadedCourseBlocks__courseID_courseID(`courseID`))} public static func blockCompletionRequest(courseID: Parameter, blockID: Parameter) -> Verify { return Verify(method: .m_blockCompletionRequest__courseID_courseIDblockID_blockID(`courseID`, `blockID`))} public static func getHandouts(courseID: Parameter) -> Verify { return Verify(method: .m_getHandouts__courseID_courseID(`courseID`))} public static func getUpdates(courseID: Parameter) -> Verify { return Verify(method: .m_getUpdates__courseID_courseID(`courseID`))} public static func resumeBlock(courseID: Parameter) -> Verify { return Verify(method: .m_resumeBlock__courseID_courseID(`courseID`))} public static func getSubtitles(url: Parameter, selectedLanguage: Parameter) -> Verify { return Verify(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`))} + public static func getCourseDates(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseDates__courseID_courseID(`courseID`))} } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func getCourseDetails(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getCourseDetails__courseID_courseID(`courseID`), performs: perform) - } public static func getCourseBlocks(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_getCourseBlocks__courseID_courseID(`courseID`), performs: perform) } public static func getCourseVideoBlocks(fullStructure: Parameter, perform: @escaping (CourseStructure) -> Void) -> Perform { return Perform(method: .m_getCourseVideoBlocks__fullStructure_fullStructure(`fullStructure`), performs: perform) } - public static func getCourseDetailsOffline(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getCourseDetailsOffline__courseID_courseID(`courseID`), performs: perform) - } - public static func getCourseBlocksOffline(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getCourseBlocksOffline__courseID_courseID(`courseID`), performs: perform) - } - public static func enrollToCourse(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_enrollToCourse__courseID_courseID(`courseID`), performs: perform) + public static func getLoadedCourseBlocks(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getLoadedCourseBlocks__courseID_courseID(`courseID`), performs: perform) } public static func blockCompletionRequest(courseID: Parameter, blockID: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_blockCompletionRequest__courseID_courseIDblockID_blockID(`courseID`, `blockID`), performs: perform) @@ -1980,6 +1968,9 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { public static func getSubtitles(url: Parameter, selectedLanguage: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_getSubtitles__url_urlselectedLanguage_selectedLanguage(`url`, `selectedLanguage`), performs: perform) } + public static func getCourseDates(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getCourseDates__courseID_courseID(`courseID`), performs: perform) + } } public func given(_ method: Given) { @@ -2095,6 +2086,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var currentDownloadTask: DownloadDataTask? { + get { invocations.append(.p_currentDownloadTask_get); return __p_currentDownloadTask ?? optionalGivenGetterValue(.p_currentDownloadTask_get, "DownloadManagerProtocolMock - stub value for currentDownloadTask was not defined") } + } + private var __p_currentDownloadTask: (DownloadDataTask)? + @@ -2113,29 +2109,44 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func eventPublisher() -> AnyPublisher { + addInvocation(.m_eventPublisher) + let perform = methodPerformValue(.m_eventPublisher) as? () -> Void + perform?() + var __value: AnyPublisher do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void - } catch MockError.notStubed { - // do nothing + __value = try methodReturnValue(.m_eventPublisher).casted() } catch { - throw error + onFatalFailure("Stub return value not specified for eventPublisher(). Use given") + Failure("Stub return value not specified for eventPublisher(). Use given") } + return __value } - open func getDownloadsForCourse(_ courseId: String) -> [DownloadData] { - addInvocation(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) - let perform = methodPerformValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + open func getDownloadTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasks) + let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasks(). Use given") + Failure("Stub return value not specified for getDownloadTasks(). Use given") + } + return __value + } + + open func getDownloadTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void perform?(`courseId`) - var __value: [DownloadData] + var __value: [DownloadDataTask] do { - __value = try methodReturnValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))).casted() + __value = try methodReturnValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))).casted() } catch { - onFatalFailure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") - Failure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") + onFatalFailure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") } return __value } @@ -2153,12 +2164,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() + open func cancelDownloading(task: DownloadDataTask) throws { + addInvocation(.m_cancelDownloading__task_task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_cancelDownloading__task_task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + _ = try methodReturnValue(.m_cancelDownloading__task_task(Parameter.value(`task`))).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -2166,10 +2177,17 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func pauseDownloading() { - addInvocation(.m_pauseDownloading) - let perform = methodPerformValue(.m_pauseDownloading) as? () -> Void - perform?() + open func cancelDownloading(courseId: String) throws { + addInvocation(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } } open func deleteFile(blocks: [CourseBlock]) { @@ -2197,28 +2215,72 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + addInvocation(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + Failure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + } + return __value + } + + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + fileprivate enum MethodType { case m_publisher - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_getDownloadsForCourse__courseId(Parameter) + case m_eventPublisher + case m_getDownloadTasks + case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) - case m_resumeDownloading - case m_pauseDownloading + case m_cancelDownloading__task_task(Parameter) + case m_cancelDownloading__courseId_courseId(Parameter) case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_resumeDownloading + case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { case (.m_publisher, .m_publisher): return .match - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_eventPublisher, .m_eventPublisher): return .match - case (.m_getDownloadsForCourse__courseId(let lhsCourseid), .m_getDownloadsForCourse__courseId(let rhsCourseid)): + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match + + case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) return Matcher.ComparisonResult(results) @@ -2229,9 +2291,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.m_cancelDownloading__task_task(let lhsTask), .m_cancelDownloading__task_task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "task")) + return Matcher.ComparisonResult(results) - case (.m_pauseDownloading, .m_pauseDownloading): return .match + case (.m_cancelDownloading__courseId_courseId(let lhsCourseid), .m_cancelDownloading__courseId_courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + return Matcher.ComparisonResult(results) case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] @@ -2244,6 +2312,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } } @@ -2251,27 +2332,37 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { func intValue() -> Int { switch self { case .m_publisher: return 0 - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_getDownloadsForCourse__courseId(p0): return p0.intValue + case .m_eventPublisher: return 0 + case .m_getDownloadTasks: return 0 + case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue - case .m_resumeDownloading: return 0 - case .m_pauseDownloading: return 0 + case let .m_cancelDownloading__task_task(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_resumeDownloading: return 0 + case .p_currentDownloadTask_get: return 0 } } func assertionName() -> String { switch self { case .m_publisher: return ".publisher()" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_getDownloadsForCourse__courseId: return ".getDownloadsForCourse(_:)" + case .m_eventPublisher: return ".eventPublisher()" + case .m_getDownloadTasks: return ".getDownloadTasks()" + case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" - case .m_resumeDownloading: return ".resumeDownloading()" - case .m_pauseDownloading: return ".pauseDownloading()" + case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" + case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_resumeDownloading: return ".resumeDownloading()" + case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } } @@ -2284,16 +2375,28 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { super.init(products) } + public static func currentDownloadTask(getter defaultValue: DownloadDataTask?...) -> PropertyStub { + return Given(method: .p_currentDownloadTask_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willReturn: AnyPublisher...) -> MethodStub { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getDownloadsForCourse(_ courseId: Parameter, willReturn: [DownloadData]...) -> MethodStub { - return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func eventPublisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { let willReturn: [AnyPublisher] = [] let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -2301,10 +2404,24 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func getDownloadsForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadData]>) -> Void) -> MethodStub { - let willReturn: [[DownloadData]] = [] - let given: Given = { return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([DownloadData]).self) + public static func eventPublisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func getDownloadTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) willProduce(stubber) return given } @@ -2315,13 +2432,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) willProduce(stubber) return given } @@ -2335,6 +2449,36 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func cancelDownloading(task: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(task: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func resumeDownloading(willThrow: Error...) -> MethodStub { return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) } @@ -2351,14 +2495,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { fileprivate var method: MethodType public static func publisher() -> Verify { return Verify(method: .m_publisher)} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func getDownloadsForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadsForCourse__courseId(`courseId`))} + public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} + public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} - public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} - public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} + public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} + public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } public struct Perform { @@ -2368,20 +2517,23 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_publisher, performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func eventPublisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_eventPublisher, performs: perform) } - public static func getDownloadsForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getDownloadsForCourse__courseId(`courseId`), performs: perform) + public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadTasks, performs: perform) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadTasksForCourse__courseId(`courseId`), performs: perform) } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, perform: @escaping (String, [CourseBlock]) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + public static func cancelDownloading(task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__task_task(`task`), performs: perform) } - public static func pauseDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_pauseDownloading, performs: perform) + public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) @@ -2392,6 +2544,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) + } + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) + } } public func given(_ method: Given) { diff --git a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift index 86c12aa7a..a62719c81 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -24,7 +24,8 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() Given(connectivity, .isInternetAvaliable(getter: true)) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -32,7 +33,8 @@ final class CourseContainerViewModelTests: XCTestCase { analytics: analytics, config: config, connectivity: connectivity, - manager: DownloadManagerMock(), + manager: DownloadManagerMock(), + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -50,8 +52,7 @@ final class CourseContainerViewModelTests: XCTestCase { type: .problem, displayName: "", studentUrl: "", - videoUrl: nil, - youTubeUrl: nil + encodedVideo: nil ) let vertical = CourseVertical( blockId: "", @@ -126,7 +127,8 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() Given(connectivity, .isInternetAvaliable(getter: false)) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -134,7 +136,8 @@ final class CourseContainerViewModelTests: XCTestCase { analytics: analytics, config: config, connectivity: connectivity, - manager: DownloadManagerMock(), + manager: DownloadManagerMock(), + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -157,13 +160,13 @@ final class CourseContainerViewModelTests: XCTestCase { certificate: nil ) - Given(interactor, .getCourseBlocksOffline(courseID: .any, willReturn: courseStructure)) + Given(interactor, .getLoadedCourseBlocks(courseID: .any, willReturn: courseStructure)) Given(interactor, .getCourseVideoBlocks(fullStructure: .any, willReturn: courseStructure)) await viewModel.getCourseBlocks(courseID: "123") - Verify(interactor, .getCourseBlocksOffline(courseID: .any)) + Verify(interactor, .getLoadedCourseBlocks(courseID: .any)) Verify(interactor, .getCourseVideoBlocks(fullStructure: .any)) XCTAssertFalse(viewModel.isShowProgress) XCTAssertFalse(viewModel.showError) @@ -180,7 +183,8 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() Given(connectivity, .isInternetAvaliable(getter: true)) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -188,7 +192,8 @@ final class CourseContainerViewModelTests: XCTestCase { analytics: analytics, config: config, connectivity: connectivity, - manager: DownloadManagerMock(), + manager: DownloadManagerMock(), + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -220,7 +225,8 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() Given(connectivity, .isInternetAvaliable(getter: true)) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -229,6 +235,7 @@ final class CourseContainerViewModelTests: XCTestCase { config: config, connectivity: connectivity, manager: DownloadManagerMock(), + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -257,7 +264,8 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() Given(connectivity, .isInternetAvaliable(getter: true)) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -265,7 +273,8 @@ final class CourseContainerViewModelTests: XCTestCase { analytics: analytics, config: config, connectivity: connectivity, - manager: DownloadManagerMock(), + manager: DownloadManagerMock(), + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -294,7 +303,8 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() Given(connectivity, .isInternetAvaliable(getter: true)) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -302,7 +312,8 @@ final class CourseContainerViewModelTests: XCTestCase { analytics: analytics, config: config, connectivity: connectivity, - manager: DownloadManagerMock(), + manager: DownloadManagerMock(), + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -323,7 +334,7 @@ final class CourseContainerViewModelTests: XCTestCase { Verify(analytics, .courseOutlineHandoutsTabClicked(courseId: .value("1"), courseName: .value("name"))) } - func testOnDownloadViewAvailableTap() { + func testOnDownloadViewAvailableTap() async throws { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() @@ -331,11 +342,94 @@ final class CourseContainerViewModelTests: XCTestCase { let config = ConfigMock() let connectivity = ConnectivityProtocolMock() let downloadManager = DownloadManagerProtocolMock() - + + let blockId = "chapter:block:1" + + let block = CourseBlock( + blockId: blockId, + id: "1", + courseId: "123", + topicId: "", + graded: false, + completion: 0, + type: .video, + displayName: "", + studentUrl: "", + encodedVideo: .init( + fallback: nil, + youtube: nil, + desktopMP4: .init(url: "test.mp4", fileSize: 1000, streamPriority: 1), + mobileHigh: nil, + mobileLow: nil, + hls: nil + ) + + ) + + let vertical = CourseVertical( + blockId: blockId, + id: "vertical1", + courseId: "123", + displayName: "", + type: .vertical, + completion: 0, + childs: [block] + ) + + let sequential = CourseSequential( + blockId: blockId, + id: blockId, + displayName: "", + type: .chapter, + completion: 0, + childs: [vertical] + ) + + let chapter = CourseChapter( + blockId: blockId, + id: "1", + displayName: "Chapter 1", + type: .chapter, + childs: [sequential] + ) + + let courseStructure = CourseStructure( + id: "123", + graded: true, + completion: 0, + viewYouTubeUrl: "", + encodedVideo: "", + displayName: "", + topicID: nil, + childs: [chapter], + media: DataLayer.CourseMedia(image: DataLayer.Image( + raw: "", + small: "", + large: "" + )), + certificate: nil + ) + + let downloadData = DownloadDataTask( + id: "1", + courseId: "course123", + url: "https://example.com/file.mp4", + fileName: "file.mp4", + displayName: "file.mp4", + progress: 0, + resumeData: nil, + state: .inProgress, + type: .video, + fileSize: 1000 + ) + Given(connectivity, .isInternetAvaliable(getter: true)) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) - + Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) + Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [downloadData])) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -344,33 +438,33 @@ final class CourseContainerViewModelTests: XCTestCase { config: config, connectivity: connectivity, manager: downloadManager, + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil ) - - let blockId = "chapter:block:1" - - let chapter = CourseChapter( - blockId: blockId, - id: "1", - displayName: "Chapter 1", - type: .chapter, - childs: [] - ) - - viewModel.onDownloadViewTap( - chapter: chapter, - blockId: blockId, - state: .available - ) - - XCTAssertEqual(viewModel.downloadState[blockId], .downloading) + viewModel.courseStructure = courseStructure + await viewModel.setDownloadsStates() + + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: blockId, + state: .available + ) + + let exp = expectation(description: "Task Starting") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + exp.fulfill() + } + + wait(for: [exp], timeout: 1) + + XCTAssertEqual(viewModel.sequentialsDownloadState[blockId], .downloading) } - func testOnDownloadViewDownloadingTap() { + func testOnDownloadViewDownloadingTap() async { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() @@ -379,10 +473,79 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let downloadManager = DownloadManagerProtocolMock() + let blockId = "chapter:block:1" + + let block = CourseBlock( + blockId: blockId, + id: "1", + courseId: "123", + topicId: "", + graded: false, + completion: 0, + type: .video, + displayName: "", + studentUrl: "", + encodedVideo: .init( + fallback: nil, + youtube: nil, + desktopMP4: .init(url: "test.mp4", fileSize: 1000, streamPriority: 1), + mobileHigh: nil, + mobileLow: nil, + hls: nil + ) + ) + + let vertical = CourseVertical( + blockId: blockId, + id: "vertical1", + courseId: "123", + displayName: "", + type: .vertical, + completion: 0, + childs: [block] + ) + + let sequential = CourseSequential( + blockId: blockId, + id: blockId, + displayName: "", + type: .chapter, + completion: 0, + childs: [vertical] + ) + + let chapter = CourseChapter( + blockId: blockId, + id: "1", + displayName: "Chapter 1", + type: .chapter, + childs: [sequential] + ) + + let courseStructure = CourseStructure( + id: "123", + graded: true, + completion: 0, + viewYouTubeUrl: "", + encodedVideo: "", + displayName: "", + topicID: nil, + childs: [chapter], + media: DataLayer.CourseMedia(image: DataLayer.Image( + raw: "", + small: "", + large: "" + )), + certificate: nil + ) + Given(connectivity, .isInternetAvaliable(getter: true)) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) - + Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) + Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [])) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -391,33 +554,33 @@ final class CourseContainerViewModelTests: XCTestCase { config: config, connectivity: connectivity, manager: downloadManager, + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil ) - - let blockId = "chapter:block:1" - - let chapter = CourseChapter( - blockId: blockId, - id: "1", - displayName: "Chapter 1", - type: .chapter, - childs: [] - ) - - viewModel.onDownloadViewTap( - chapter: chapter, - blockId: blockId, - state: .downloading - ) - - XCTAssertEqual(viewModel.downloadState[blockId], .available) + viewModel.courseStructure = courseStructure + await viewModel.setDownloadsStates() + + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: blockId, + state: .downloading + ) + + let exp = expectation(description: "Task Starting") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + exp.fulfill() + } + + wait(for: [exp], timeout: 1) + + XCTAssertEqual(viewModel.sequentialsDownloadState[blockId], .available) } - func testOnDownloadViewFinishedTap() { + func testOnDownloadViewFinishedTap() async throws { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() @@ -426,10 +589,79 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let downloadManager = DownloadManagerProtocolMock() + let blockId = "chapter:block:1" + + let block = CourseBlock( + blockId: blockId, + id: "1", + courseId: "123", + topicId: "", + graded: false, + completion: 0, + type: .video, + displayName: "", + studentUrl: "", + encodedVideo: .init( + fallback: nil, + youtube: nil, + desktopMP4: .init(url: "test.mp4", fileSize: 1000, streamPriority: 1), + mobileHigh: nil, + mobileLow: nil, + hls: nil + ) + ) + + let vertical = CourseVertical( + blockId: blockId, + id: "vertical1", + courseId: "123", + displayName: "", + type: .vertical, + completion: 0, + childs: [block] + ) + + let sequential = CourseSequential( + blockId: blockId, + id: blockId, + displayName: "", + type: .chapter, + completion: 0, + childs: [vertical] + ) + + let chapter = CourseChapter( + blockId: blockId, + id: "1", + displayName: "Chapter 1", + type: .chapter, + childs: [sequential] + ) + + let courseStructure = CourseStructure( + id: "123", + graded: true, + completion: 0, + viewYouTubeUrl: "", + encodedVideo: "", + displayName: "", + topicID: nil, + childs: [chapter], + media: DataLayer.CourseMedia(image: DataLayer.Image( + raw: "", + small: "", + large: "" + )), + certificate: nil + ) + Given(connectivity, .isInternetAvaliable(getter: true)) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) - + Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) + Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [])) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -438,33 +670,34 @@ final class CourseContainerViewModelTests: XCTestCase { config: config, connectivity: connectivity, manager: downloadManager, + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil ) - - let blockId = "chapter:block:1" - - let chapter = CourseChapter( - blockId: blockId, - id: "1", - displayName: "Chapter 1", - type: .chapter, - childs: [] - ) - - viewModel.onDownloadViewTap( - chapter: chapter, - blockId: blockId, - state: .finished - ) - - XCTAssertEqual(viewModel.downloadState[blockId], .available) + viewModel.courseStructure = courseStructure + await viewModel.setDownloadsStates() + + await viewModel.onDownloadViewTap( + chapter: chapter, + blockId: blockId, + state: .finished + ) + + let exp = expectation(description: "Task Starting") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + exp.fulfill() + } + + wait(for: [exp], timeout: 1) + + XCTAssertEqual(viewModel.sequentialsDownloadState[blockId], .available) + } - func testSetDownloadsStatesAvailable() { + func testSetDownloadsStatesAvailable() async throws { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() @@ -473,8 +706,10 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let downloadManager = DownloadManagerProtocolMock() + let blockId = "chapter:block:1" + let block = CourseBlock( - blockId: "block:1", + blockId: blockId, id: "1", courseId: "123", topicId: "", @@ -483,11 +718,18 @@ final class CourseContainerViewModelTests: XCTestCase { type: .video, displayName: "", studentUrl: "", - videoUrl: "https://example.com/file.mp4", - youTubeUrl: nil + encodedVideo: .init( + fallback: nil, + youtube: nil, + desktopMP4: .init(url: "test.mp4", fileSize: 1000, streamPriority: 1), + mobileHigh: nil, + mobileLow: nil, + hls: nil + ) ) + let vertical = CourseVertical( - blockId: "block:vertical1", + blockId: blockId, id: "vertical1", courseId: "123", displayName: "", @@ -495,24 +737,24 @@ final class CourseContainerViewModelTests: XCTestCase { completion: 0, childs: [block] ) + let sequential = CourseSequential( - blockId: "block:sequential1", - id: "sequential1", + blockId: blockId, + id: blockId, displayName: "", type: .chapter, completion: 0, childs: [vertical] ) + let chapter = CourseChapter( - blockId: "", - id: "", - displayName: "", + blockId: blockId, + id: "1", + displayName: "Chapter 1", type: .chapter, childs: [sequential] ) - - let childs = [chapter] - + let courseStructure = CourseStructure( id: "123", graded: true, @@ -521,7 +763,7 @@ final class CourseContainerViewModelTests: XCTestCase { encodedVideo: "", displayName: "", topicID: nil, - childs: childs, + childs: [chapter], media: DataLayer.CourseMedia(image: DataLayer.Image( raw: "", small: "", @@ -529,12 +771,14 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil ) - + Given(connectivity, .isInternetAvaliable(getter: true)) - - Given(downloadManager, .publisher(willReturn: Just(1).eraseToAnyPublisher())) - Given(downloadManager, .getDownloadsForCourse(.any, willReturn: [])) - + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + + Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) + Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) + Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [])) + let viewModel = CourseContainerViewModel( interactor: interactor, authInteractor: authInteractor, @@ -543,6 +787,7 @@ final class CourseContainerViewModelTests: XCTestCase { config: config, connectivity: connectivity, manager: downloadManager, + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -550,18 +795,19 @@ final class CourseContainerViewModelTests: XCTestCase { enrollmentEnd: nil ) viewModel.courseStructure = courseStructure - - let exp = expectation(description: "DispatchQueue.main.async Starting") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + await viewModel.setDownloadsStates() + + let exp = expectation(description: "Task Starting") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { exp.fulfill() } - + wait(for: [exp], timeout: 1) - - XCTAssertEqual(viewModel.downloadState[sequential.id], .available) + + XCTAssertEqual(viewModel.sequentialsDownloadState[sequential.id], .available) } - func testSetDownloadsStatesDownloading() { + func testSetDownloadsStatesDownloading() async throws { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() @@ -570,8 +816,10 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let downloadManager = DownloadManagerProtocolMock() + let blockId = "chapter:block:1" + let block = CourseBlock( - blockId: "block:1", + blockId: blockId, id: "1", courseId: "123", topicId: "", @@ -580,11 +828,18 @@ final class CourseContainerViewModelTests: XCTestCase { type: .video, displayName: "", studentUrl: "", - videoUrl: "https://example.com/file.mp4", - youTubeUrl: nil + encodedVideo: .init( + fallback: nil, + youtube: nil, + desktopMP4: .init(url: "test.mp4", fileSize: 1000, streamPriority: 1), + mobileHigh: nil, + mobileLow: nil, + hls: nil + ) ) + let vertical = CourseVertical( - blockId: "block:vertical1", + blockId: blockId, id: "vertical1", courseId: "123", displayName: "", @@ -592,24 +847,24 @@ final class CourseContainerViewModelTests: XCTestCase { completion: 0, childs: [block] ) + let sequential = CourseSequential( - blockId: "block:sequential1", - id: "sequential1", + blockId: blockId, + id: blockId, displayName: "", type: .chapter, completion: 0, childs: [vertical] ) + let chapter = CourseChapter( - blockId: "", - id: "", - displayName: "", + blockId: blockId, + id: "1", + displayName: "Chapter 1", type: .chapter, childs: [sequential] ) - let childs = [chapter] - let courseStructure = CourseStructure( id: "123", graded: true, @@ -618,7 +873,7 @@ final class CourseContainerViewModelTests: XCTestCase { encodedVideo: "", displayName: "", topicID: nil, - childs: childs, + childs: [chapter], media: DataLayer.CourseMedia(image: DataLayer.Image( raw: "", small: "", @@ -627,21 +882,25 @@ final class CourseContainerViewModelTests: XCTestCase { certificate: nil ) - let downloadData = DownloadData( + let downloadData = DownloadDataTask( id: "1", courseId: "course123", url: "https://example.com/file.mp4", fileName: "file.mp4", + displayName: "file.mp4", progress: 0, resumeData: nil, - state: .waiting, - type: .video + state: .inProgress, + type: .video, + fileSize: 1000 ) Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) - Given(downloadManager, .publisher(willReturn: Just(1).eraseToAnyPublisher())) - Given(downloadManager, .getDownloadsForCourse(.any, willReturn: [downloadData])) + Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) + Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) + Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [downloadData])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -651,6 +910,7 @@ final class CourseContainerViewModelTests: XCTestCase { config: config, connectivity: connectivity, manager: downloadManager, + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -658,18 +918,19 @@ final class CourseContainerViewModelTests: XCTestCase { enrollmentEnd: nil ) viewModel.courseStructure = courseStructure + await viewModel.setDownloadsStates() - let exp = expectation(description: "DispatchQueue.main.async Starting") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let exp = expectation(description: "Task Starting") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { exp.fulfill() } wait(for: [exp], timeout: 1) - XCTAssertEqual(viewModel.downloadState[sequential.id], .downloading) + XCTAssertEqual(viewModel.sequentialsDownloadState[sequential.id], .downloading) } - func testSetDownloadsStatesFinished() { + func testSetDownloadsStatesFinished() async throws { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() @@ -678,8 +939,10 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let downloadManager = DownloadManagerProtocolMock() + let blockId = "chapter:block:1" + let block = CourseBlock( - blockId: "block:1", + blockId: blockId, id: "1", courseId: "123", topicId: "", @@ -688,11 +951,18 @@ final class CourseContainerViewModelTests: XCTestCase { type: .video, displayName: "", studentUrl: "", - videoUrl: "https://example.com/file.mp4", - youTubeUrl: nil + encodedVideo: .init( + fallback: nil, + youtube: nil, + desktopMP4: .init(url: "test.mp4", fileSize: 1000, streamPriority: 1), + mobileHigh: nil, + mobileLow: nil, + hls: nil + ) ) + let vertical = CourseVertical( - blockId: "block:vertical1", + blockId: blockId, id: "vertical1", courseId: "123", displayName: "", @@ -700,24 +970,24 @@ final class CourseContainerViewModelTests: XCTestCase { completion: 0, childs: [block] ) + let sequential = CourseSequential( - blockId: "block:sequential1", - id: "sequential1", + blockId: blockId, + id: blockId, displayName: "", type: .chapter, completion: 0, childs: [vertical] ) + let chapter = CourseChapter( - blockId: "", - id: "", - displayName: "", + blockId: blockId, + id: "1", + displayName: "Chapter 1", type: .chapter, childs: [sequential] ) - let childs = [chapter] - let courseStructure = CourseStructure( id: "123", graded: true, @@ -726,7 +996,7 @@ final class CourseContainerViewModelTests: XCTestCase { encodedVideo: "", displayName: "", topicID: nil, - childs: childs, + childs: [chapter], media: DataLayer.CourseMedia(image: DataLayer.Image( raw: "", small: "", @@ -735,21 +1005,25 @@ final class CourseContainerViewModelTests: XCTestCase { certificate: nil ) - let downloadData = DownloadData( + let downloadData = DownloadDataTask( id: "1", courseId: "course123", url: "https://example.com/file.mp4", fileName: "file.mp4", + displayName: "file.mp4", progress: 0, resumeData: nil, state: .finished, - type: .video + type: .video, + fileSize: 1000 ) Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) - Given(downloadManager, .publisher(willReturn: Just(1).eraseToAnyPublisher())) - Given(downloadManager, .getDownloadsForCourse(.any, willReturn: [downloadData])) + Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) + Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) + Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [downloadData])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -759,6 +1033,7 @@ final class CourseContainerViewModelTests: XCTestCase { config: config, connectivity: connectivity, manager: downloadManager, + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -766,18 +1041,18 @@ final class CourseContainerViewModelTests: XCTestCase { enrollmentEnd: nil ) viewModel.courseStructure = courseStructure + await viewModel.setDownloadsStates() - let exp = expectation(description: "DispatchQueue.main.async Starting") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let exp = expectation(description: "Task Starting") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { exp.fulfill() } wait(for: [exp], timeout: 1) - - XCTAssertEqual(viewModel.downloadState[sequential.id], .finished) + XCTAssertEqual(viewModel.sequentialsDownloadState[sequential.id], .finished) } - func testSetDownloadsStatesPartiallyFinished() { + func testSetDownloadsStatesPartiallyFinished() async throws { let interactor = CourseInteractorProtocolMock() let authInteractor = AuthInteractorProtocolMock() let router = CourseRouterMock() @@ -786,8 +1061,10 @@ final class CourseContainerViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let downloadManager = DownloadManagerProtocolMock() - let block1 = CourseBlock( - blockId: "block:1", + let blockId = "chapter:block:1" + + let block = CourseBlock( + blockId: blockId, id: "1", courseId: "123", topicId: "", @@ -796,12 +1073,18 @@ final class CourseContainerViewModelTests: XCTestCase { type: .video, displayName: "", studentUrl: "", - videoUrl: "https://example.com/file.mp4", - youTubeUrl: nil + encodedVideo: .init( + fallback: nil, + youtube: nil, + desktopMP4: .init(url: "test.mp4", fileSize: 1000, streamPriority: 1), + mobileHigh: nil, + mobileLow: nil, + hls: nil + ) ) let block2 = CourseBlock( - blockId: "block:2", - id: "2", + blockId: "123", + id: "1213", courseId: "123", topicId: "", graded: false, @@ -809,36 +1092,43 @@ final class CourseContainerViewModelTests: XCTestCase { type: .video, displayName: "", studentUrl: "", - videoUrl: "https://example.com/file2.mp4", - youTubeUrl: nil + encodedVideo: .init( + fallback: nil, + youtube: nil, + desktopMP4: .init(url: "test.mp4", fileSize: 1000, streamPriority: 1), + mobileHigh: nil, + mobileLow: nil, + hls: nil + ) ) + let vertical = CourseVertical( - blockId: "block:vertical1", + blockId: blockId, id: "vertical1", courseId: "123", displayName: "", type: .vertical, completion: 0, - childs: [block1, block2] + childs: [block, block2] ) + let sequential = CourseSequential( - blockId: "block:sequential1", - id: "sequential1", + blockId: blockId, + id: blockId, displayName: "", type: .chapter, completion: 0, childs: [vertical] ) + let chapter = CourseChapter( - blockId: "", - id: "", - displayName: "", + blockId: blockId, + id: "1", + displayName: "Chapter 1", type: .chapter, childs: [sequential] ) - let childs = [chapter] - let courseStructure = CourseStructure( id: "123", graded: true, @@ -847,7 +1137,7 @@ final class CourseContainerViewModelTests: XCTestCase { encodedVideo: "", displayName: "", topicID: nil, - childs: childs, + childs: [chapter], media: DataLayer.CourseMedia(image: DataLayer.Image( raw: "", small: "", @@ -856,21 +1146,25 @@ final class CourseContainerViewModelTests: XCTestCase { certificate: nil ) - let downloadData = DownloadData( + let downloadData = DownloadDataTask( id: "1", courseId: "course123", url: "https://example.com/file.mp4", fileName: "file.mp4", + displayName: "file.mp4", progress: 0, resumeData: nil, state: .finished, - type: .video + type: .video, + fileSize: 1000 ) Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) - Given(downloadManager, .publisher(willReturn: Just(1).eraseToAnyPublisher())) - Given(downloadManager, .getDownloadsForCourse(.any, willReturn: [downloadData])) + Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) + Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) + Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [downloadData])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -880,6 +1174,7 @@ final class CourseContainerViewModelTests: XCTestCase { config: config, connectivity: connectivity, manager: downloadManager, + storage: CourseStorageMock(), isActive: true, courseStart: Date(), courseEnd: nil, @@ -887,14 +1182,15 @@ final class CourseContainerViewModelTests: XCTestCase { enrollmentEnd: nil ) viewModel.courseStructure = courseStructure + await viewModel.setDownloadsStates() - let exp = expectation(description: "DispatchQueue.main.async Starting") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let exp = expectation(description: "Task Starting") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { exp.fulfill() } wait(for: [exp], timeout: 1) - XCTAssertEqual(viewModel.downloadState[sequential.id], .available) + XCTAssertEqual(viewModel.sequentialsDownloadState[sequential.id], .available) } } diff --git a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift new file mode 100644 index 000000000..797c27440 --- /dev/null +++ b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift @@ -0,0 +1,473 @@ +// +// CourseDateViewModelTests.swift +// CourseTests +// +// Created by Muhammad Umer on 10/24/23. +// + +import XCTest +import Alamofire +import SwiftyMocky +@testable import Core +@testable import Course + +final class CourseDateViewModelTests: XCTestCase { + func testGetCourseDatesSuccess() async throws { + let interactor = CourseInteractorProtocolMock() + let router = CourseRouterMock() + let cssInjector = CSSInjectorMock() + let connectivity = ConnectivityProtocolMock() + + let courseDates = CourseDates( + datesBannerInfo: + DatesBannerInfo( + missedDeadlines: false, + contentTypeGatingEnabled: false, + missedGatedContent: false, + verifiedUpgradeLink: ""), + courseDateBlocks: [], + hasEnded: false, + learnerIsFullAccess: false, + userTimezone: nil) + + Given(interactor, .getCourseDates(courseID: .any, willReturn: courseDates)) + + let viewModel = CourseDatesViewModel( + interactor: interactor, + router: router, + cssInjector: cssInjector, + connectivity: connectivity, + courseID: "1") + + await viewModel.getCourseDates(courseID: "1") + + Verify(interactor, .getCourseDates(courseID: .any)) + + XCTAssert((viewModel.courseDates != nil)) + XCTAssertFalse(viewModel.isShowProgress) + XCTAssertNil(viewModel.errorMessage) + XCTAssertFalse(viewModel.showError) + } + + func testGetCourseDatesUnknownError() async throws { + let interactor = CourseInteractorProtocolMock() + let router = CourseRouterMock() + let cssInjector = CSSInjectorMock() + let connectivity = ConnectivityProtocolMock() + + Given(interactor, .getCourseDates(courseID: .any, willThrow: NSError())) + + let viewModel = CourseDatesViewModel( + interactor: interactor, + router: router, + cssInjector: cssInjector, + connectivity: connectivity, + courseID: "1") + + await viewModel.getCourseDates(courseID: "1") + + Verify(interactor, .getCourseDates(courseID: .any)) + + XCTAssertTrue(viewModel.showError) + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError, "Error view should be shown on unknown error.") + } + + func testNoInternetConnectionError() async throws { + let interactor = CourseInteractorProtocolMock() + let router = CourseRouterMock() + let cssInjector = CSSInjectorMock() + let connectivity = ConnectivityProtocolMock() + + let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) + + Given(interactor, .getCourseDates(courseID: .any, willThrow: noInternetError)) + + let viewModel = CourseDatesViewModel( + interactor: interactor, + router: router, + cssInjector: cssInjector, + connectivity: connectivity, + courseID: "1") + + await viewModel.getCourseDates(courseID: "1") + + Verify(interactor, .getCourseDates(courseID: .any)) + + XCTAssertTrue(viewModel.showError) + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection, "Error message should be set to 'slow or no internet connection'.") + } + + func testSortedDateTodayToCourseDateBlockDict() { + let block1 = CourseDateBlock( + assignmentType: nil, + complete: nil, + date: Date.today.addingTimeInterval(86400), + dateType: "event", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockID1" + ) + + let block2 = CourseDateBlock( + assignmentType: nil, + complete: true, + date: Date.today, + dateType: "event", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockID1" + ) + + let courseDates = CourseDates( + datesBannerInfo: DatesBannerInfo( + missedDeadlines: false, + contentTypeGatingEnabled: false, + missedGatedContent: false, + verifiedUpgradeLink: nil + ), + courseDateBlocks: [block1, block2], + hasEnded: false, + learnerIsFullAccess: true, + userTimezone: nil + ) + + let sortedDict = courseDates.statusDatesBlocks[.completed] + + XCTAssertEqual(sortedDict?.keys.sorted().first, Date.today) + } + + func testMultipleBlocksForSameDate() { + let block1 = CourseDateBlock( + assignmentType: nil, + complete: true, + date: Date.today, + dateType: "event", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockID1" + ) + + let block2 = CourseDateBlock( + assignmentType: nil, + complete: true, + date: Date.today, + dateType: "event", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockID1" + ) + + let courseDates = CourseDates( + datesBannerInfo: DatesBannerInfo( + missedDeadlines: false, + contentTypeGatingEnabled: false, + missedGatedContent: false, + verifiedUpgradeLink: nil + ), + courseDateBlocks: [block1, block2], + hasEnded: false, + learnerIsFullAccess: true, + userTimezone: nil + ) + + let sortedDict = courseDates.statusDatesBlocks[.completed] + XCTAssertEqual(sortedDict?[block1.date]?.count, 2, "There should be two blocks for the given date.") + } + + func testBlockStatusForAssignmentType() { + let block = CourseDateBlock( + assignmentType: nil, + complete: nil, + date: Date.today, + dateType: "assignment-due-date", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestAssignment", + extraInfo: nil, + firstComponentBlockID: "blockID3" + ) + + XCTAssertEqual(block.blockStatus, .dueNext) + } + + func testBadgeLogicForToday() { + let block = CourseDateBlock( + assignmentType: nil, + complete: false, + date: Date.today, + dateType: "", + description: "", + learnerHasAccess: false, + link: "www.example.com", + linkText: nil, + title: CoreLocalization.CourseDates.today, + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertEqual(block.title, "Today", "Block title for 'today' should be 'Today'") + } + + func testBadgeLogicForCompleted() { + let block = CourseDateBlock( + assignmentType: nil, + complete: true, + date: Date.today, + dateType: "assignment-due-date", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + XCTAssertEqual(block.blockStatus, .completed, "Block status for a completed assignment should be 'completed'") + } + + func testBadgeLogicForVerifiedOnly() { + let block = CourseDateBlock( + assignmentType: nil, + complete: false, + date: Date.today, + dateType: "assignment-due-date", + description: "", + learnerHasAccess: false, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertEqual(block.blockStatus, .verifiedOnly, "Block status for a block without learner access should be 'verifiedOnly'") + } + + func testBadgeLogicForPastDue() { + let block = CourseDateBlock( + assignmentType: nil, + complete: false, + date: Date.today.addingTimeInterval(-86400), + dateType: "assignment-due-date", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertEqual(block.blockStatus, .pastDue, "Block status for a past due assignment should be 'pastDue'") + } + + func testLinkForAvailableAssignment() { + let availableAssignment = CourseDateBlock( + assignmentType: nil, + complete: true, + date: Date.today, + dateType: "assignment-due-date", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + XCTAssertTrue(availableAssignment.canShowLink, "Available assignments should be hyperlinked.") + } + + func testIsAssignment() { + let block = CourseDateBlock( + assignmentType: nil, + complete: false, + date: Date.today.addingTimeInterval(86400), + dateType: "assignment-due-date", + description: "", + learnerHasAccess: false, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertTrue(block.isAssignment) + } + + func testIsCourseStartDate() { + let block = CourseDateBlock( + assignmentType: nil, + complete: nil, + date: Date.today.addingTimeInterval(-86400), + dateType: "course-start-date", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertEqual(block.blockStatus, BlockStatus.courseStartDate) + } + + func testIsCourseEndDate() { + let block = CourseDateBlock( + assignmentType: nil, + complete: nil, + date: Date.today.addingTimeInterval(86400), + dateType: "course-end-date", + description: "", + learnerHasAccess: true, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertEqual(block.blockStatus, BlockStatus.courseEndDate) + } + + func testVerifiedOnly() { + let block = CourseDateBlock( + assignmentType: nil, + complete: false, + date: Date.today.addingTimeInterval(86400), + dateType: "assignment-due-date", + description: "", + learnerHasAccess: false, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertTrue(block.isVerifiedOnly, "Block should be identified as 'verified only' when the learner has no access.") + } + + func testIsCompleted() { + let block = CourseDateBlock( + assignmentType: nil, + complete: true, + date: Date.today, + dateType: "assignment-due-date", + description: "", + learnerHasAccess: false, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertTrue(block.isComplete, "Block should be marked as completed.") + } + + func testBadgeLogicForUnreleasedAssignment() { + let block = CourseDateBlock( + assignmentType: nil, + complete: false, + date: Date.today.addingTimeInterval(86400), + dateType: "assignment-due-date", + description: "", + learnerHasAccess: true, + link: "", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertEqual(block.blockStatus, .unreleased, "Block status should be set to 'unreleased' for unreleased assignments.") + } + + func testNoLinkForUnavailableAssignment() { + let block = CourseDateBlock( + assignmentType: nil, + complete: false, + date: Date.today.addingTimeInterval(86400), + dateType: "assignment-due-date", + description: "", + learnerHasAccess: false, + link: "www.example.com", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertEqual(block.blockStatus, .verifiedOnly) + XCTAssertFalse(block.canShowLink, "Block should not show a link if the assignment is unavailable.") + } + + func testNoLinkAvailableForUnreleasedAssignment() { + let block = CourseDateBlock( + assignmentType: nil, + complete: false, + date: Date.today, + dateType: "assignment-due-date", + description: "", + learnerHasAccess: true, + link: "", + linkText: nil, + title: "TestBlock", + extraInfo: nil, + firstComponentBlockID: "blockIDTest" + ) + + XCTAssertEqual(block.blockStatus, .unreleased) + XCTAssertFalse(block.canShowLink, "Block should not show a link if the assignment is unreleased.") + } + + func testTodayProperty() { + let today = Date.today + let currentDay = Calendar.current.startOfDay(for: Date()) + XCTAssertTrue(today.isToday, "The today property should return true for isToday.") + XCTAssertEqual(today, currentDay, "The today property should equal the start of the current day.") + } + + func testDateIsInPastProperty() { + let pastDate = Date().addingTimeInterval(-100000) + XCTAssertTrue(pastDate.isInPast, "The past date should return true for isInPast.") + XCTAssertFalse(pastDate.isToday, "The past date should return false for isInPast.") + } + + func testDateIsInFutureProperty() { + let futureDate = Date().addingTimeInterval(100000) + XCTAssertTrue(futureDate.isInFuture, "The future date should return false for isInFuture.") + XCTAssertFalse(futureDate.isToday, "The future date should return false for isInFuture.") + } + + func testBlockStatusMapping() { + XCTAssertEqual(BlockStatus.status(of: "course-start-date"), .courseStartDate, "Incorrect mapping for 'course-start-date'") + XCTAssertEqual(BlockStatus.status(of: "course-end-date"), .courseEndDate, "Incorrect mapping for 'course-end-date'") + XCTAssertEqual(BlockStatus.status(of: "certificate-available-date"), .certificateAvailbleDate, "Incorrect mapping for 'certificate-available-date'") + XCTAssertEqual(BlockStatus.status(of: "verification-deadline-date"), .verificationDeadlineDate, "Incorrect mapping for 'verification-deadline-date'") + XCTAssertEqual(BlockStatus.status(of: "verified-upgrade-deadline"), .verifiedUpgradeDeadline, "Incorrect mapping for 'verified-upgrade-deadline'") + XCTAssertEqual(BlockStatus.status(of: "assignment-due-date"), .assignment, "Incorrect mapping for 'assignment-due-date'") + XCTAssertEqual(BlockStatus.status(of: ""), .event, "Incorrect mapping for 'event'") + } +} diff --git a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift index 25d731196..332fbee8b 100644 --- a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift @@ -13,7 +13,8 @@ import Alamofire import SwiftUI final class CourseUnitViewModelTests: XCTestCase { - + var config = Config() + static let blocks = [ CourseBlock(blockId: "1", id: "1", @@ -24,8 +25,8 @@ final class CourseUnitViewModelTests: XCTestCase { type: .video, displayName: "Lesson 1", studentUrl: "", - videoUrl: nil, - youTubeUrl: nil), + encodedVideo: nil + ), CourseBlock(blockId: "2", id: "2", courseId: "123", @@ -35,8 +36,8 @@ final class CourseUnitViewModelTests: XCTestCase { type: .video, displayName: "Lesson 2", studentUrl: "2", - videoUrl: nil, - youTubeUrl: nil), + encodedVideo: nil + ), CourseBlock(blockId: "3", id: "3", courseId: "123", @@ -46,8 +47,8 @@ final class CourseUnitViewModelTests: XCTestCase { type: .unknown, displayName: "Lesson 3", studentUrl: "3", - videoUrl: nil, - youTubeUrl: nil), + encodedVideo: nil + ), CourseBlock(blockId: "4", id: "4", courseId: "123", @@ -57,8 +58,8 @@ final class CourseUnitViewModelTests: XCTestCase { type: .unknown, displayName: "4", studentUrl: "4", - videoUrl: nil, - youTubeUrl: nil), + encodedVideo: nil + ), ] let chapters = [ @@ -123,9 +124,11 @@ final class CourseUnitViewModelTests: XCTestCase { sequentialIndex: 0, verticalIndex: 0, interactor: interactor, + config: config, router: router, analytics: analytics, connectivity: connectivity, + storage: CourseStorageMock(), manager: DownloadManagerMock() ) @@ -151,9 +154,11 @@ final class CourseUnitViewModelTests: XCTestCase { sequentialIndex: 0, verticalIndex: 0, interactor: interactor, + config: config, router: router, analytics: analytics, - connectivity: connectivity, + connectivity: connectivity, + storage: CourseStorageMock(), manager: DownloadManagerMock() ) @@ -184,9 +189,11 @@ final class CourseUnitViewModelTests: XCTestCase { sequentialIndex: 0, verticalIndex: 0, interactor: interactor, + config: config, router: router, analytics: analytics, connectivity: connectivity, + storage: CourseStorageMock(), manager: DownloadManagerMock() ) @@ -219,9 +226,11 @@ final class CourseUnitViewModelTests: XCTestCase { sequentialIndex: 0, verticalIndex: 0, interactor: interactor, + config: config, router: router, analytics: analytics, connectivity: connectivity, + storage: CourseStorageMock(), manager: DownloadManagerMock() ) @@ -253,9 +262,11 @@ final class CourseUnitViewModelTests: XCTestCase { sequentialIndex: 0, verticalIndex: 0, interactor: interactor, + config: config, router: router, analytics: analytics, connectivity: connectivity, + storage: CourseStorageMock(), manager: DownloadManagerMock() ) diff --git a/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift b/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift index 83295daf7..2a6b2f722 100644 --- a/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift @@ -37,7 +37,8 @@ final class VideoPlayerViewModelTests: XCTestCase { courseID: "", languages: [], interactor: interactor, - router: router, + router: router, + appStorage: CoreStorageMock(), connectivity: connectivity) await viewModel.getSubtitles(subtitlesUrl: "url") @@ -64,6 +65,7 @@ final class VideoPlayerViewModelTests: XCTestCase { languages: [], interactor: interactor, router: router, + appStorage: CoreStorageMock(), connectivity: connectivity) await viewModel.getSubtitles(subtitlesUrl: "url") @@ -85,6 +87,7 @@ final class VideoPlayerViewModelTests: XCTestCase { languages: [], interactor: interactor, router: router, + appStorage: CoreStorageMock(), connectivity: connectivity) viewModel.languages = [ @@ -112,6 +115,7 @@ final class VideoPlayerViewModelTests: XCTestCase { languages: [], interactor: interactor, router: router, + appStorage: CoreStorageMock(), connectivity: connectivity) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willProduce: {_ in})) @@ -131,6 +135,7 @@ final class VideoPlayerViewModelTests: XCTestCase { languages: [], interactor: interactor, router: router, + appStorage: CoreStorageMock(), connectivity: connectivity) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: NSError())) @@ -155,6 +160,7 @@ final class VideoPlayerViewModelTests: XCTestCase { languages: [], interactor: interactor, router: router, + appStorage: CoreStorageMock(), connectivity: connectivity) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: noInternetError)) diff --git a/Course/Mockfile b/Course/Mockfile index 504794e7d..58cd4b263 100644 --- a/Course/Mockfile +++ b/Course/Mockfile @@ -1,5 +1,5 @@ -sourceryCommand: null -sourceryTemplate: null +sourceryCommand: mint run krzysztofzablocki/Sourcery@2.1.2 sourcery +sourceryTemplate: ../MockTemplate.swifttemplate unit.tests.mock: sources: include: diff --git a/Dashboard/Dashboard/Data/DashboardRepository.swift b/Dashboard/Dashboard/Data/DashboardRepository.swift index ce5721784..cff780083 100644 --- a/Dashboard/Dashboard/Data/DashboardRepository.swift +++ b/Dashboard/Dashboard/Data/DashboardRepository.swift @@ -17,10 +17,10 @@ public class DashboardRepository: DashboardRepositoryProtocol { private let api: API private let storage: CoreStorage - private let config: Config + private let config: ConfigProtocol private let persistence: DashboardPersistenceProtocol - public init(api: API, storage: CoreStorage, config: Config, persistence: DashboardPersistenceProtocol) { + public init(api: API, storage: CoreStorage, config: ConfigProtocol, persistence: DashboardPersistenceProtocol) { self.api = api self.storage = storage self.config = config diff --git a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift index d9e4dec06..1d6845214 100644 --- a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift +++ b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift @@ -15,7 +15,7 @@ enum DashboardEndpoint: EndPointType { var path: String { switch self { case let .getMyCourses(username, _): - return "/mobile_api_extensions/v1/users/\(username)/course_enrollments" + return "/api/mobile/v3/users/\(username)/course_enrollments" } } diff --git a/Dashboard/Dashboard/Presentation/DashboardView.swift b/Dashboard/Dashboard/Presentation/DashboardView.swift index 4be6e62e7..108f1dc59 100644 --- a/Dashboard/Dashboard/Presentation/DashboardView.swift +++ b/Dashboard/Dashboard/Presentation/DashboardView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme public struct DashboardView: View { private let dashboardCourses: some View = VStack(alignment: .leading) { @@ -18,6 +19,8 @@ public struct DashboardView: View { .foregroundColor(Theme.Colors.textPrimary) }.listRowBackground(Color.clear) .padding(.top, 24) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) @StateObject private var viewModel: DashboardViewModel @@ -91,7 +94,8 @@ public struct DashboardView: View { } } } - }.frameLimit() + }.accessibilityAction {} + .frameLimit() }.padding(.top, 8) // MARK: - Offline mode SnackBar diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index 27aebe250..2aa0f593d 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.8.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func login(externalToken: String, backend: String) throws -> User { + addInvocation(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) + let perform = methodPerformValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) as? (String, String) -> Void + perform?(`externalToken`, `backend`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) return Matcher.ComparisonResult(results) + case (.m_login__externalToken_externalTokenbackend_backend(let lhsExternaltoken, let lhsBackend), .m_login__externalToken_externalTokenbackend_backend(let rhsExternaltoken, let rhsBackend)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsExternaltoken, rhs: rhsExternaltoken, with: matcher), lhsExternaltoken, rhsExternaltoken, "externalToken")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) @@ -490,22 +539,28 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`controllers`) } - open func showMainScreen() { - addInvocation(.m_showMainScreen) - let perform = methodPerformValue(.m_showMainScreen) as? () -> Void - perform?() + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } - open func showLoginScreen() { - addInvocation(.m_showLoginScreen) - let perform = methodPerformValue(.m_showLoginScreen) as? () -> Void + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void perform?() } - open func showRegisterScreen() { - addInvocation(.m_showRegisterScreen) - let perform = methodPerformValue(.m_showRegisterScreen) as? () -> Void - perform?() + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } open func showForgotPasswordScreen() { @@ -514,6 +569,18 @@ open class BaseRouterMock: BaseRouter, Mock { perform?() } + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -545,10 +612,13 @@ open class BaseRouterMock: BaseRouter, Mock { case m_backWithFade case m_dismiss__animated_animated(Parameter) case m_removeLastView__controllers_controllers(Parameter) - case m_showMainScreen - case m_showLoginScreen - case m_showRegisterScreen + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) @@ -578,14 +648,37 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) return Matcher.ComparisonResult(results) - case (.m_showMainScreen, .m_showMainScreen): return .match + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showStartupScreen, .m_showStartupScreen): return .match - case (.m_showLoginScreen, .m_showLoginScreen): return .match + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) - case (.m_showRegisterScreen, .m_showRegisterScreen): return .match + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -630,10 +723,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return 0 case let .m_dismiss__animated_animated(p0): return p0.intValue case let .m_removeLastView__controllers_controllers(p0): return p0.intValue - case .m_showMainScreen: return 0 - case .m_showLoginScreen: return 0 - case .m_showRegisterScreen: return 0 + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue @@ -647,10 +743,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return ".backWithFade()" case .m_dismiss__animated_animated: return ".dismiss(animated:)" case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" - case .m_showMainScreen: return ".showMainScreen()" - case .m_showLoginScreen: return ".showLoginScreen()" - case .m_showRegisterScreen: return ".showRegisterScreen()" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" @@ -678,10 +777,13 @@ open class BaseRouterMock: BaseRouter, Mock { public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} - public static func showMainScreen() -> Verify { return Verify(method: .m_showMainScreen)} - public static func showLoginScreen() -> Verify { return Verify(method: .m_showLoginScreen)} - public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} @@ -707,18 +809,27 @@ open class BaseRouterMock: BaseRouter, Mock { public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) } - public static func showMainScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showMainScreen, performs: perform) + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) } - public static func showLoginScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showLoginScreen, performs: perform) + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } - public static func showRegisterScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showRegisterScreen, performs: perform) + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showForgotPasswordScreen, performs: perform) } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1453,6 +1564,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var currentDownloadTask: DownloadDataTask? { + get { invocations.append(.p_currentDownloadTask_get); return __p_currentDownloadTask ?? optionalGivenGetterValue(.p_currentDownloadTask_get, "DownloadManagerProtocolMock - stub value for currentDownloadTask was not defined") } + } + private var __p_currentDownloadTask: (DownloadDataTask)? + @@ -1471,29 +1587,44 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func eventPublisher() -> AnyPublisher { + addInvocation(.m_eventPublisher) + let perform = methodPerformValue(.m_eventPublisher) as? () -> Void + perform?() + var __value: AnyPublisher do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void - } catch MockError.notStubed { - // do nothing + __value = try methodReturnValue(.m_eventPublisher).casted() } catch { - throw error + onFatalFailure("Stub return value not specified for eventPublisher(). Use given") + Failure("Stub return value not specified for eventPublisher(). Use given") + } + return __value + } + + open func getDownloadTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasks) + let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasks(). Use given") + Failure("Stub return value not specified for getDownloadTasks(). Use given") } + return __value } - open func getDownloadsForCourse(_ courseId: String) -> [DownloadData] { - addInvocation(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) - let perform = methodPerformValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + open func getDownloadTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void perform?(`courseId`) - var __value: [DownloadData] + var __value: [DownloadDataTask] do { - __value = try methodReturnValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))).casted() + __value = try methodReturnValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))).casted() } catch { - onFatalFailure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") - Failure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") + onFatalFailure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") } return __value } @@ -1511,12 +1642,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() + open func cancelDownloading(task: DownloadDataTask) throws { + addInvocation(.m_cancelDownloading__task_task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_cancelDownloading__task_task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + _ = try methodReturnValue(.m_cancelDownloading__task_task(Parameter.value(`task`))).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -1524,10 +1655,17 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func pauseDownloading() { - addInvocation(.m_pauseDownloading) - let perform = methodPerformValue(.m_pauseDownloading) as? () -> Void - perform?() + open func cancelDownloading(courseId: String) throws { + addInvocation(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } } open func deleteFile(blocks: [CourseBlock]) { @@ -1555,28 +1693,72 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + addInvocation(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + Failure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + } + return __value + } + + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + fileprivate enum MethodType { case m_publisher - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_getDownloadsForCourse__courseId(Parameter) + case m_eventPublisher + case m_getDownloadTasks + case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) - case m_resumeDownloading - case m_pauseDownloading + case m_cancelDownloading__task_task(Parameter) + case m_cancelDownloading__courseId_courseId(Parameter) case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_resumeDownloading + case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { case (.m_publisher, .m_publisher): return .match - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_eventPublisher, .m_eventPublisher): return .match + + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match - case (.m_getDownloadsForCourse__courseId(let lhsCourseid), .m_getDownloadsForCourse__courseId(let rhsCourseid)): + case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) return Matcher.ComparisonResult(results) @@ -1587,9 +1769,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.m_cancelDownloading__task_task(let lhsTask), .m_cancelDownloading__task_task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "task")) + return Matcher.ComparisonResult(results) - case (.m_pauseDownloading, .m_pauseDownloading): return .match + case (.m_cancelDownloading__courseId_courseId(let lhsCourseid), .m_cancelDownloading__courseId_courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + return Matcher.ComparisonResult(results) case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] @@ -1602,6 +1790,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } } @@ -1609,27 +1810,37 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { func intValue() -> Int { switch self { case .m_publisher: return 0 - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_getDownloadsForCourse__courseId(p0): return p0.intValue + case .m_eventPublisher: return 0 + case .m_getDownloadTasks: return 0 + case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue - case .m_resumeDownloading: return 0 - case .m_pauseDownloading: return 0 + case let .m_cancelDownloading__task_task(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_resumeDownloading: return 0 + case .p_currentDownloadTask_get: return 0 } } func assertionName() -> String { switch self { case .m_publisher: return ".publisher()" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_getDownloadsForCourse__courseId: return ".getDownloadsForCourse(_:)" + case .m_eventPublisher: return ".eventPublisher()" + case .m_getDownloadTasks: return ".getDownloadTasks()" + case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" - case .m_resumeDownloading: return ".resumeDownloading()" - case .m_pauseDownloading: return ".pauseDownloading()" + case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" + case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_resumeDownloading: return ".resumeDownloading()" + case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } } @@ -1642,16 +1853,28 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { super.init(products) } + public static func currentDownloadTask(getter defaultValue: DownloadDataTask?...) -> PropertyStub { + return Given(method: .p_currentDownloadTask_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willReturn: AnyPublisher...) -> MethodStub { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getDownloadsForCourse(_ courseId: Parameter, willReturn: [DownloadData]...) -> MethodStub { - return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func eventPublisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { let willReturn: [AnyPublisher] = [] let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -1659,10 +1882,24 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func getDownloadsForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadData]>) -> Void) -> MethodStub { - let willReturn: [[DownloadData]] = [] - let given: Given = { return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([DownloadData]).self) + public static func eventPublisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func getDownloadTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) willProduce(stubber) return given } @@ -1673,13 +1910,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) willProduce(stubber) return given } @@ -1693,6 +1927,36 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func cancelDownloading(task: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(task: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func resumeDownloading(willThrow: Error...) -> MethodStub { return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) } @@ -1709,14 +1973,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { fileprivate var method: MethodType public static func publisher() -> Verify { return Verify(method: .m_publisher)} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func getDownloadsForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadsForCourse__courseId(`courseId`))} + public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} + public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} - public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} - public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} + public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} + public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } public struct Perform { @@ -1726,20 +1995,23 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_publisher, performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func eventPublisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_eventPublisher, performs: perform) + } + public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadTasks, performs: perform) } - public static func getDownloadsForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getDownloadsForCourse__courseId(`courseId`), performs: perform) + public static func getDownloadTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadTasksForCourse__courseId(`courseId`), performs: perform) } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, perform: @escaping (String, [CourseBlock]) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + public static func cancelDownloading(task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__task_task(`task`), performs: perform) } - public static func pauseDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_pauseDownloading, performs: perform) + public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) @@ -1750,6 +2022,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) + } + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) + } } public func given(_ method: Given) { diff --git a/Dashboard/Mockfile b/Dashboard/Mockfile index 053c2899f..f747b41e0 100644 --- a/Dashboard/Mockfile +++ b/Dashboard/Mockfile @@ -1,5 +1,5 @@ -sourceryCommand: null -sourceryTemplate: null +sourceryCommand: mint run krzysztofzablocki/Sourcery@2.1.2 sourcery +sourceryTemplate: ../MockTemplate.swifttemplate unit.tests.mock: sources: include: diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index 05974ad4f..769376097 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -11,9 +11,11 @@ 022D048B2976D7E100E0059B /* Discovery.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0727879928D34C03002E9142 /* Discovery.framework */; platformFilter = ios; }; 022D04982976DA8A00E0059B /* DiscoveryMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022D04972976DA8A00E0059B /* DiscoveryMock.generated.swift */; }; 0283347728D499BC00C828FC /* DiscoveryInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283347628D499BC00C828FC /* DiscoveryInteractor.swift */; }; - 0283347928D49A8700C828FC /* DiscoveryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283347828D49A8700C828FC /* DiscoveryViewModel.swift */; }; 0284DBFC28D4856A00830893 /* DiscoveryEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0284DBFB28D4856A00830893 /* DiscoveryEndpoint.swift */; }; 0284DC0328D4922900830893 /* DiscoveryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0284DC0228D4922900830893 /* DiscoveryRepository.swift */; }; + 029242E72AE6978400A940EC /* UpdateRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029242E62AE6978400A940EC /* UpdateRequiredView.swift */; }; + 029242E92AE6A3AB00A940EC /* UpdateRecommendedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029242E82AE6A3AB00A940EC /* UpdateRecommendedView.swift */; }; + 029242EB2AE6AB7B00A940EC /* UpdateNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029242EA2AE6AB7B00A940EC /* UpdateNotificationView.swift */; }; 029737402949FB070051696B /* DiscoveryCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0297373E2949FB070051696B /* DiscoveryCoreModel.xcdatamodeld */; }; 029737422949FB3B0051696B /* DiscoveryPersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029737412949FB3B0051696B /* DiscoveryPersistenceProtocol.swift */; }; 02EF39D128D867690058F6BD /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 02EF39D028D867690058F6BD /* swiftgen.yml */; }; @@ -22,13 +24,25 @@ 02F1752F2A4DA3B60019CD70 /* DiscoveryAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F1752E2A4DA3B60019CD70 /* DiscoveryAnalytics.swift */; }; 02F3BFDF29252F2F0051930C /* DiscoveryRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFDE29252F2F0051930C /* DiscoveryRouter.swift */; }; 072787AD28D34D15002E9142 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072787AC28D34D15002E9142 /* Core.framework */; }; - 072787B428D34D91002E9142 /* DiscoveryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072787B328D34D91002E9142 /* DiscoveryView.swift */; }; + 1402A0C92B61012F00A0A00B /* ProgramWebviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1402A0C72B61012F00A0A00B /* ProgramWebviewView.swift */; }; + 1402A0CA2B61012F00A0A00B /* ProgramWebviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1402A0C82B61012F00A0A00B /* ProgramWebviewViewModel.swift */; }; 63C6E9CBBF5E33B8B9B4DFEC /* Pods_App_Discovery_DiscoveryUnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 780FC373E1D479E58870BD85 /* Pods_App_Discovery_DiscoveryUnitTests.framework */; }; 9F47BCC672941A9854404EC7 /* Pods_App_Discovery.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 919E55130969D91EF03C4C0B /* Pods_App_Discovery.framework */; }; - CFC849432996A5150055E497 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC849422996A5150055E497 /* SearchView.swift */; }; - CFC849452996A52A0055E497 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC849442996A52A0055E497 /* SearchViewModel.swift */; }; CFC8494C299A66080055E497 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CFC8494E299A66080055E497 /* Localizable.stringsdict */; }; CFC84950299BE52C0055E497 /* SearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC8494F299BE52C0055E497 /* SearchViewModelTests.swift */; }; + E0B9F69C2B4D57F800168366 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F6962B4D57F800168366 /* SearchView.swift */; }; + E0B9F69D2B4D57F800168366 /* DiscoveryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F6972B4D57F800168366 /* DiscoveryViewModel.swift */; }; + E0B9F69E2B4D57F800168366 /* CourseDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F6982B4D57F800168366 /* CourseDetailsView.swift */; }; + E0B9F69F2B4D57F800168366 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F6992B4D57F800168366 /* SearchViewModel.swift */; }; + E0B9F6A02B4D57F800168366 /* DiscoveryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F69A2B4D57F800168366 /* DiscoveryView.swift */; }; + E0B9F6A12B4D57F800168366 /* CourseDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F69B2B4D57F800168366 /* CourseDetailsViewModel.swift */; }; + E0B9F6A42B4D59E000168366 /* CourseDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F6A32B4D59E000168366 /* CourseDetails.swift */; }; + E0B9F6A62B4D620100168366 /* Data_CourseDetailsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F6A52B4D620100168366 /* Data_CourseDetailsResponse.swift */; }; + E0B9F6AB2B4E718F00168366 /* CourseDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F6AA2B4E718F00168366 /* CourseDetailsViewModelTests.swift */; }; + E0D586202B300095009B4BA7 /* DiscoveryWebviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D5861E2B300095009B4BA7 /* DiscoveryWebviewViewModel.swift */; }; + E0D586212B300095009B4BA7 /* DiscoveryWebview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D5861F2B300095009B4BA7 /* DiscoveryWebview.swift */; }; + E0D586232B3000AD009B4BA7 /* DiscoveryURIDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586222B3000AD009B4BA7 /* DiscoveryURIDetails.swift */; }; + E0D586252B300134009B4BA7 /* URL+PathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586242B300134009B4BA7 /* URL+PathExtension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -46,9 +60,11 @@ 022D04892976D7E100E0059B /* DiscoveryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryViewModelTests.swift; sourceTree = ""; }; 022D04972976DA8A00E0059B /* DiscoveryMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DiscoveryMock.generated.swift; path = DiscoveryTests/DiscoveryMock.generated.swift; sourceTree = SOURCE_ROOT; }; 0283347628D499BC00C828FC /* DiscoveryInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryInteractor.swift; sourceTree = ""; }; - 0283347828D49A8700C828FC /* DiscoveryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryViewModel.swift; sourceTree = ""; }; 0284DBFB28D4856A00830893 /* DiscoveryEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryEndpoint.swift; sourceTree = ""; }; 0284DC0228D4922900830893 /* DiscoveryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryRepository.swift; sourceTree = ""; }; + 029242E62AE6978400A940EC /* UpdateRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateRequiredView.swift; sourceTree = ""; }; + 029242E82AE6A3AB00A940EC /* UpdateRecommendedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateRecommendedView.swift; sourceTree = ""; }; + 029242EA2AE6AB7B00A940EC /* UpdateNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateNotificationView.swift; sourceTree = ""; }; 0297373F2949FB070051696B /* DiscoveryCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DiscoveryCoreModel.xcdatamodel; sourceTree = ""; }; 029737412949FB3B0051696B /* DiscoveryPersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPersistenceProtocol.swift; sourceTree = ""; }; 02ED50C729A649C9008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; @@ -61,8 +77,9 @@ 0692409931272CDA39B10321 /* Pods-App-Discovery.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Discovery/Pods-App-Discovery.releasestage.xcconfig"; sourceTree = ""; }; 0727879928D34C03002E9142 /* Discovery.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Discovery.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 072787AC28D34D15002E9142 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 072787B328D34D91002E9142 /* DiscoveryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryView.swift; sourceTree = ""; }; 0C3850985F33C1AD72BF1B04 /* Pods-App-Discovery.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery.release.xcconfig"; path = "Target Support Files/Pods-App-Discovery/Pods-App-Discovery.release.xcconfig"; sourceTree = ""; }; + 1402A0C72B61012F00A0A00B /* ProgramWebviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramWebviewView.swift; sourceTree = ""; }; + 1402A0C82B61012F00A0A00B /* ProgramWebviewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramWebviewViewModel.swift; sourceTree = ""; }; 2334C76D248D0A95634AFFD9 /* Pods-App-Discovery-DiscoveryUnitTests.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.debugstage.xcconfig"; sourceTree = ""; }; 2760B1F234E01FFCB73F41C2 /* Pods-App-Discovery.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery.debug.xcconfig"; path = "Target Support Files/Pods-App-Discovery/Pods-App-Discovery.debug.xcconfig"; sourceTree = ""; }; 445F0675BF0E1DEB78F3CE73 /* Pods-App-Discovery-DiscoveryUnitTests.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.releasedev.xcconfig"; sourceTree = ""; }; @@ -76,10 +93,24 @@ 919E55130969D91EF03C4C0B /* Pods_App_Discovery.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Discovery.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9F968F74AD7F4B5F6E5A6084 /* Pods-App-Discovery-DiscoveryUnitTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.debugdev.xcconfig"; sourceTree = ""; }; AAC0D83F5D34491E9FABCABC /* Pods-App-Discovery-DiscoveryUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.debug.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.debug.xcconfig"; sourceTree = ""; }; - CFC849422996A5150055E497 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; - CFC849442996A52A0055E497 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; CFC8494D299A66080055E497 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; CFC8494F299BE52C0055E497 /* SearchViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModelTests.swift; sourceTree = ""; }; + E08D12E22B482D720096311A /* Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E0B9F6962B4D57F800168366 /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; + E0B9F6972B4D57F800168366 /* DiscoveryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryViewModel.swift; sourceTree = ""; }; + E0B9F6982B4D57F800168366 /* CourseDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseDetailsView.swift; sourceTree = ""; }; + E0B9F6992B4D57F800168366 /* SearchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; + E0B9F69A2B4D57F800168366 /* DiscoveryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryView.swift; sourceTree = ""; }; + E0B9F69B2B4D57F800168366 /* CourseDetailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseDetailsViewModel.swift; sourceTree = ""; }; + E0B9F6A32B4D59E000168366 /* CourseDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseDetails.swift; sourceTree = ""; }; + E0B9F6A52B4D620100168366 /* Data_CourseDetailsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data_CourseDetailsResponse.swift; sourceTree = ""; }; + E0B9F6AA2B4E718F00168366 /* CourseDetailsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseDetailsViewModelTests.swift; sourceTree = ""; }; + E0D586132B29F25A009B4BA7 /* Authorization.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Authorization.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E0D5861E2B300095009B4BA7 /* DiscoveryWebviewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryWebviewViewModel.swift; sourceTree = ""; }; + E0D5861F2B300095009B4BA7 /* DiscoveryWebview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryWebview.swift; sourceTree = ""; }; + E0D586222B3000AD009B4BA7 /* DiscoveryURIDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryURIDetails.swift; sourceTree = ""; }; + E0D586242B300134009B4BA7 /* URL+PathExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+PathExtension.swift"; sourceTree = ""; }; + E0D586282B302C3A009B4BA7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; E192F9B4A7EECED9665AB8A7 /* Pods-App-Discovery.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Discovery/Pods-App-Discovery.releasedev.xcconfig"; sourceTree = ""; }; F340BD73D38B0DF9E4EA6482 /* Pods-App-Discovery-DiscoveryUnitTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.releaseprod.xcconfig"; sourceTree = ""; }; FF565519B9BBC73E92249648 /* Pods-App-Discovery-DiscoveryUnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.release.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.release.xcconfig"; sourceTree = ""; }; @@ -128,6 +159,7 @@ 0284DBF828D4831000830893 /* Data */ = { isa = PBXGroup; children = ( + E0B9F6A22B4D59E000168366 /* Model */, 0284DBFA28D4832D00830893 /* Network */, 0284DC0228D4922900830893 /* DiscoveryRepository.swift */, 0208666829CC6CD600BC05B2 /* Persistence */, @@ -151,6 +183,16 @@ path = Domain; sourceTree = ""; }; + 029242E52AE6976E00A940EC /* UpdateViews */ = { + isa = PBXGroup; + children = ( + 029242E62AE6978400A940EC /* UpdateRequiredView.swift */, + 029242E82AE6A3AB00A940EC /* UpdateRecommendedView.swift */, + 029242EA2AE6AB7B00A940EC /* UpdateNotificationView.swift */, + ); + path = UpdateViews; + sourceTree = ""; + }; 02EF39CB28D866C50058F6BD /* SwiftGen */ = { isa = PBXGroup; children = ( @@ -162,10 +204,10 @@ 070019A228F6EF2700D5FC78 /* Presentation */ = { isa = PBXGroup; children = ( - 072787B328D34D91002E9142 /* DiscoveryView.swift */, - 0283347828D49A8700C828FC /* DiscoveryViewModel.swift */, - CFC849422996A5150055E497 /* SearchView.swift */, - CFC849442996A52A0055E497 /* SearchViewModel.swift */, + 1402A0C62B61011D00A0A00B /* WebPrograms */, + E0B9F6952B4D57F800168366 /* NativeDiscovery */, + E0D5861D2B300095009B4BA7 /* WebDiscovery */, + 029242E52AE6976E00A940EC /* UpdateViews */, 02F3BFDE29252F2F0051930C /* DiscoveryRouter.swift */, 02F1752E2A4DA3B60019CD70 /* DiscoveryAnalytics.swift */, ); @@ -196,6 +238,7 @@ 0727879B28D34C03002E9142 /* Discovery */ = { isa = PBXGroup; children = ( + E0D586282B302C3A009B4BA7 /* Info.plist */, 02EF39CB28D866C50058F6BD /* SwiftGen */, 0284DBF828D4831000830893 /* Data */, 0284DC0428D4996F00830893 /* Domain */, @@ -209,6 +252,8 @@ 072787AB28D34D15002E9142 /* Frameworks */ = { isa = PBXGroup; children = ( + E08D12E22B482D720096311A /* Course.framework */, + E0D586132B29F25A009B4BA7 /* Authorization.framework */, 072787AC28D34D15002E9142 /* Core.framework */, 919E55130969D91EF03C4C0B /* Pods_App_Discovery.framework */, 780FC373E1D479E58870BD85 /* Pods_App_Discovery_DiscoveryUnitTests.framework */, @@ -219,12 +264,22 @@ 0766DFD1299AD97300EBEF6A /* Presentation */ = { isa = PBXGroup; children = ( + E0B9F6AA2B4E718F00168366 /* CourseDetailsViewModelTests.swift */, 022D04892976D7E100E0059B /* DiscoveryViewModelTests.swift */, CFC8494F299BE52C0055E497 /* SearchViewModelTests.swift */, ); path = Presentation; sourceTree = ""; }; + 1402A0C62B61011D00A0A00B /* WebPrograms */ = { + isa = PBXGroup; + children = ( + 1402A0C72B61012F00A0A00B /* ProgramWebviewView.swift */, + 1402A0C82B61012F00A0A00B /* ProgramWebviewViewModel.swift */, + ); + path = WebPrograms; + sourceTree = ""; + }; 88B044C704F7C52F249CC424 /* Pods */ = { isa = PBXGroup; children = ( @@ -249,6 +304,39 @@ path = ../Pods; sourceTree = ""; }; + E0B9F6952B4D57F800168366 /* NativeDiscovery */ = { + isa = PBXGroup; + children = ( + E0B9F6962B4D57F800168366 /* SearchView.swift */, + E0B9F6972B4D57F800168366 /* DiscoveryViewModel.swift */, + E0B9F6982B4D57F800168366 /* CourseDetailsView.swift */, + E0B9F6992B4D57F800168366 /* SearchViewModel.swift */, + E0B9F69A2B4D57F800168366 /* DiscoveryView.swift */, + E0B9F69B2B4D57F800168366 /* CourseDetailsViewModel.swift */, + ); + path = NativeDiscovery; + sourceTree = ""; + }; + E0B9F6A22B4D59E000168366 /* Model */ = { + isa = PBXGroup; + children = ( + E0B9F6A52B4D620100168366 /* Data_CourseDetailsResponse.swift */, + E0B9F6A32B4D59E000168366 /* CourseDetails.swift */, + ); + path = Model; + sourceTree = ""; + }; + E0D5861D2B300095009B4BA7 /* WebDiscovery */ = { + isa = PBXGroup; + children = ( + E0D5861E2B300095009B4BA7 /* DiscoveryWebviewViewModel.swift */, + E0D5861F2B300095009B4BA7 /* DiscoveryWebview.swift */, + E0D586222B3000AD009B4BA7 /* DiscoveryURIDetails.swift */, + E0D586242B300134009B4BA7 /* URL+PathExtension.swift */, + ); + path = WebDiscovery; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -450,6 +538,7 @@ files = ( 022D048A2976D7E100E0059B /* DiscoveryViewModelTests.swift in Sources */, 022D04982976DA8A00E0059B /* DiscoveryMock.generated.swift in Sources */, + E0B9F6AB2B4E718F00168366 /* CourseDetailsViewModelTests.swift in Sources */, CFC84950299BE52C0055E497 /* SearchViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -458,18 +547,31 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CFC849452996A52A0055E497 /* SearchViewModel.swift in Sources */, - CFC849432996A5150055E497 /* SearchView.swift in Sources */, + 029242E92AE6A3AB00A940EC /* UpdateRecommendedView.swift in Sources */, + E0B9F6A42B4D59E000168366 /* CourseDetails.swift in Sources */, 0284DBFC28D4856A00830893 /* DiscoveryEndpoint.swift in Sources */, 029737402949FB070051696B /* DiscoveryCoreModel.xcdatamodeld in Sources */, + 029242E72AE6978400A940EC /* UpdateRequiredView.swift in Sources */, + E0B9F69F2B4D57F800168366 /* SearchViewModel.swift in Sources */, + E0B9F69E2B4D57F800168366 /* CourseDetailsView.swift in Sources */, + E0D586232B3000AD009B4BA7 /* DiscoveryURIDetails.swift in Sources */, + E0B9F69D2B4D57F800168366 /* DiscoveryViewModel.swift in Sources */, 0283347728D499BC00C828FC /* DiscoveryInteractor.swift in Sources */, + E0B9F6A02B4D57F800168366 /* DiscoveryView.swift in Sources */, + E0B9F6A12B4D57F800168366 /* CourseDetailsViewModel.swift in Sources */, 02F3BFDF29252F2F0051930C /* DiscoveryRouter.swift in Sources */, - 0283347928D49A8700C828FC /* DiscoveryViewModel.swift in Sources */, - 072787B428D34D91002E9142 /* DiscoveryView.swift in Sources */, + E0D586252B300134009B4BA7 /* URL+PathExtension.swift in Sources */, + E0B9F6A62B4D620100168366 /* Data_CourseDetailsResponse.swift in Sources */, + 1402A0C92B61012F00A0A00B /* ProgramWebviewView.swift in Sources */, 029737422949FB3B0051696B /* DiscoveryPersistenceProtocol.swift in Sources */, 0284DC0328D4922900830893 /* DiscoveryRepository.swift in Sources */, + 029242EB2AE6AB7B00A940EC /* UpdateNotificationView.swift in Sources */, 02EF39DC28D86BEF0058F6BD /* Strings.swift in Sources */, + 1402A0CA2B61012F00A0A00B /* ProgramWebviewViewModel.swift in Sources */, + E0B9F69C2B4D57F800168366 /* SearchView.swift in Sources */, + E0D586202B300095009B4BA7 /* DiscoveryWebviewViewModel.swift in Sources */, 02F1752F2A4DA3B60019CD70 /* DiscoveryAnalytics.swift in Sources */, + E0D586212B300095009B4BA7 /* DiscoveryWebview.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -709,6 +811,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -823,6 +926,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1000,6 +1104,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1035,6 +1140,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1133,6 +1239,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1232,6 +1339,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1325,6 +1433,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1417,6 +1526,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; diff --git a/Discovery/Discovery.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Discovery/Discovery.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/Discovery/Discovery.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Discovery/Discovery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Discovery/Discovery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Discovery/Discovery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Discovery/Discovery/Data/DiscoveryRepository.swift b/Discovery/Discovery/Data/DiscoveryRepository.swift index 4ba3fa74b..3b20f84a3 100644 --- a/Discovery/Discovery/Data/DiscoveryRepository.swift +++ b/Discovery/Discovery/Data/DiscoveryRepository.swift @@ -14,28 +14,31 @@ public protocol DiscoveryRepositoryProtocol { func getDiscovery(page: Int) async throws -> [CourseItem] func searchCourses(page: Int, searchTerm: String) async throws -> [CourseItem] func getDiscoveryOffline() throws -> [CourseItem] + func getCourseDetails(courseID: String) async throws -> CourseDetails + func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails + func enrollToCourse(courseID: String) async throws -> Bool } public class DiscoveryRepository: DiscoveryRepositoryProtocol { private let api: API - private let appStorage: CoreStorage - private let config: Config + private let coreStorage: CoreStorage + private let config: ConfigProtocol private let persistence: DiscoveryPersistenceProtocol public init(api: API, appStorage: CoreStorage, - config: Config, + config: ConfigProtocol, persistence: DiscoveryPersistenceProtocol) { self.api = api - self.appStorage = appStorage + self.coreStorage = appStorage self.config = config self.persistence = persistence } public func getDiscovery(page: Int) async throws -> [CourseItem] { let discoveryResponse = try await api.requestData(DiscoveryEndpoint.getDiscovery( - username: appStorage.user?.username ?? "", page: page) + username: coreStorage.user?.username ?? "", page: page) ).mapResponse(DataLayer.DiscoveryResponce.self).domain persistence.saveDiscovery(items: discoveryResponse) return discoveryResponse @@ -47,16 +50,75 @@ public class DiscoveryRepository: DiscoveryRepositoryProtocol { public func searchCourses(page: Int, searchTerm: String) async throws -> [CourseItem] { let searchResponse = try await api.requestData(DiscoveryEndpoint.searchCourses( - username: appStorage.user?.username ?? "", page: page, searchTerm: searchTerm) + username: coreStorage.user?.username ?? "", page: page, searchTerm: searchTerm) ).mapResponse(DataLayer.DiscoveryResponce.self).domain return searchResponse } + + public func getCourseDetails(courseID: String) async throws -> CourseDetails { + let response = try await api.requestData( + DiscoveryEndpoint.getCourseDetail(courseID: courseID, username: coreStorage.user?.username ?? "") + ).mapResponse(DataLayer.CourseDetailsResponse.self) + .domain(baseURL: config.baseURL.absoluteString) + + persistence.saveCourseDetails(course: response) + + return response + } + + public func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails { + return try persistence.loadCourseDetails(courseID: courseID) + } + + public func enrollToCourse(courseID: String) async throws -> Bool { + let enroll = try await api.request(DiscoveryEndpoint.enrollToCourse(courseID: courseID)) + return enroll.statusCode == 200 + } } // Mark - For testing and SwiftUI preview #if DEBUG class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { + + public func getCourseDetails(courseID: String) async throws -> CourseDetails { + return CourseDetails( + courseID: "courseID", + org: "Organization", + courseTitle: "Course title", + courseDescription: "Course description", + courseStart: Date(iso8601: "2021-05-26T12:13:14Z"), + courseEnd: Date(iso8601: "2022-05-26T12:13:14Z"), + enrollmentStart: nil, + enrollmentEnd: nil, + isEnrolled: false, + overviewHTML: "Course description

Lorem ipsum", + courseBannerURL: "courseBannerURL", + courseVideoURL: nil + ) + } + + func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails { + return CourseDetails( + courseID: "courseID", + org: "Organization", + courseTitle: "Course title", + courseDescription: "Course description", + courseStart: Date(iso8601: "2021-05-26T12:13:14Z"), + courseEnd: Date(iso8601: "2022-05-26T12:13:14Z"), + enrollmentStart: nil, + enrollmentEnd: nil, + isEnrolled: false, + overviewHTML: "Course description

Lorem ipsum", + courseBannerURL: "courseBannerURL", + courseVideoURL: nil + ) + } + + public func enrollToCourse(courseID: String) async throws -> Bool { + return true + } + func getDiscovery(page: Int) async throws -> [CourseItem] { var models: [CourseItem] = [] for i in 0...10 { diff --git a/Course/Course/Domain/Model/CourseDetails.swift b/Discovery/Discovery/Data/Model/CourseDetails.swift similarity index 94% rename from Course/Course/Domain/Model/CourseDetails.swift rename to Discovery/Discovery/Data/Model/CourseDetails.swift index 0edb58854..6769aff53 100644 --- a/Course/Course/Domain/Model/CourseDetails.swift +++ b/Discovery/Discovery/Data/Model/CourseDetails.swift @@ -11,7 +11,7 @@ public struct CourseDetails { public let courseID: String public let org: String public let courseTitle: String - public let courseDescription: String + public let courseDescription: String? public let courseStart: Date? public let courseEnd: Date? public let enrollmentStart: Date? @@ -24,7 +24,7 @@ public struct CourseDetails { public init(courseID: String, org: String, courseTitle: String, - courseDescription: String, + courseDescription: String?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, diff --git a/Course/Course/Data/Model/Data_CourseDetailsResponse.swift b/Discovery/Discovery/Data/Model/Data_CourseDetailsResponse.swift similarity index 98% rename from Course/Course/Data/Model/Data_CourseDetailsResponse.swift rename to Discovery/Discovery/Data/Model/Data_CourseDetailsResponse.swift index 306ca6f96..1047727e8 100644 --- a/Course/Course/Data/Model/Data_CourseDetailsResponse.swift +++ b/Discovery/Discovery/Data/Model/Data_CourseDetailsResponse.swift @@ -22,7 +22,7 @@ public extension DataLayer { public let name: String public let number: String public let org: String - public let shortDescription: String + public let shortDescription: String? public let start: String? public let startDisplay: String? public let startType: String? diff --git a/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift b/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift index 7ce334202..957f03112 100644 --- a/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift +++ b/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift @@ -12,13 +12,19 @@ import Alamofire enum DiscoveryEndpoint: EndPointType { case getDiscovery(username: String, page: Int) case searchCourses(username: String, page: Int, searchTerm: String) + case getCourseDetail(courseID: String, username: String) + case enrollToCourse(courseID: String) var path: String { switch self { case .getDiscovery: return "/api/courses/v1/courses/" case .searchCourses: - return "/mobile_api_extensions/courses/v1/courses/" + return "/api/courses/v1/courses/" + case .getCourseDetail(courseID: let courseID, _): + return "/api/courses/v1/courses/\(courseID)" + case .enrollToCourse: + return "/api/enrollment/v1/enrollment" } } @@ -26,6 +32,10 @@ enum DiscoveryEndpoint: EndPointType { switch self { case .getDiscovery, .searchCourses: return .get + case .getCourseDetail: + return .get + case .enrollToCourse: + return .post } } @@ -37,7 +47,6 @@ enum DiscoveryEndpoint: EndPointType { switch self { case let .getDiscovery(_, page): let params: Parameters = [ -// "username": username, "mobile": true, "permissions": ["enroll", "see_about_page", "see_in_catalog"], "page": page @@ -48,11 +57,25 @@ enum DiscoveryEndpoint: EndPointType { let params: Parameters = [ "username": username, "mobile": true, + "mobile_search": true, "page": page, "search_term": searchTerm ] return .requestParameters(parameters: params, encoding: URLEncoding.queryString) + + case .enrollToCourse(courseID: let courseID): + let params: [String: Any] = [ + "course_details": [ + "course_id": courseID, + "email_opt_in": true + ] + ] + return .requestParameters(parameters: params, encoding: JSONEncoding.default) + + case let .getCourseDetail(_, username): + let params: [String: Encodable] = ["username": username] + return .requestParameters(parameters: params, encoding: URLEncoding.queryString) } } } diff --git a/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents b/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents index dc2f9ce96..154df9ca8 100644 --- a/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents +++ b/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents @@ -1,5 +1,23 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift b/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift index a18338e3b..1c8b3fd6c 100644 --- a/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift +++ b/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift @@ -11,6 +11,8 @@ import Core public protocol DiscoveryPersistenceProtocol { func loadDiscovery() throws -> [CourseItem] func saveDiscovery(items: [CourseItem]) + func loadCourseDetails(courseID: String) throws -> CourseDetails + func saveCourseDetails(course: CourseDetails) } public final class DiscoveryBundle { diff --git a/Discovery/Discovery/Domain/DiscoveryInteractor.swift b/Discovery/Discovery/Domain/DiscoveryInteractor.swift index 1f2451d2b..a0bffe3ca 100644 --- a/Discovery/Discovery/Domain/DiscoveryInteractor.swift +++ b/Discovery/Discovery/Domain/DiscoveryInteractor.swift @@ -13,6 +13,9 @@ public protocol DiscoveryInteractorProtocol { func discovery(page: Int) async throws -> [CourseItem] func discoveryOffline() throws -> [CourseItem] func search(page: Int, searchTerm: String) async throws -> [CourseItem] + func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails + func getCourseDetails(courseID: String) async throws -> CourseDetails + func enrollToCourse(courseID: String) async throws -> Bool } public class DiscoveryInteractor: DiscoveryInteractorProtocol { @@ -34,6 +37,18 @@ public class DiscoveryInteractor: DiscoveryInteractorProtocol { public func discoveryOffline() throws -> [CourseItem] { return try repository.getDiscoveryOffline() } + + public func getCourseDetails(courseID: String) async throws -> CourseDetails { + return try await repository.getCourseDetails(courseID: courseID) + } + + public func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails { + return try await repository.getLoadedCourseDetails(courseID: courseID) + } + + public func enrollToCourse(courseID: String) async throws -> Bool { + return try await repository.enrollToCourse(courseID: courseID) + } } // Mark - For testing and SwiftUI preview diff --git a/Discovery/Discovery/Info.plist b/Discovery/Discovery/Info.plist new file mode 100644 index 000000000..f72a0f657 --- /dev/null +++ b/Discovery/Discovery/Info.plist @@ -0,0 +1,12 @@ + + + + + + diff --git a/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift b/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift index f86ef6341..bcb8190fc 100644 --- a/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift +++ b/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift @@ -12,6 +12,9 @@ public protocol DiscoveryAnalytics { func discoverySearchBarClicked() func discoveryCoursesSearch(label: String, coursesCount: Int) func discoveryCourseClicked(courseID: String, courseName: String) + func viewCourseClicked(courseId: String, courseName: String) + func courseEnrollClicked(courseId: String, courseName: String) + func courseEnrollSuccess(courseId: String, courseName: String) } #if DEBUG @@ -19,5 +22,8 @@ class DiscoveryAnalyticsMock: DiscoveryAnalytics { public func discoverySearchBarClicked() {} public func discoveryCoursesSearch(label: String, coursesCount: Int) {} public func discoveryCourseClicked(courseID: String, courseName: String) {} + public func viewCourseClicked(courseId: String, courseName: String) {} + public func courseEnrollClicked(courseId: String, courseName: String) {} + public func courseEnrollSuccess(courseId: String, courseName: String) {} } #endif diff --git a/Discovery/Discovery/Presentation/DiscoveryRouter.swift b/Discovery/Discovery/Presentation/DiscoveryRouter.swift index 8a0b68e14..cca463e95 100644 --- a/Discovery/Discovery/Presentation/DiscoveryRouter.swift +++ b/Discovery/Discovery/Presentation/DiscoveryRouter.swift @@ -9,9 +9,29 @@ import Foundation import Core public protocol DiscoveryRouter: BaseRouter { - func showCourseDetais(courseID: String, title: String) - func showDiscoverySearch() + func showWebDiscoveryDetails( + pathID: String, + discoveryType: DiscoveryWebviewType, + sourceScreen: LogistrationSourceScreen + ) + func showUpdateRequiredView(showAccountLink: Bool) + func showUpdateRecomendedView() + func showDiscoverySearch(searchQuery: String?) + func showCourseScreens( + courseID: String, + isActive: Bool?, + courseStart: Date?, + courseEnd: Date?, + enrollmentStart: Date?, + enrollmentEnd: Date?, + title: String + ) + + func showWebProgramDetails( + pathID: String, + viewType: ProgramViewType + ) } // Mark - For testing and SwiftUI preview @@ -21,7 +41,27 @@ public class DiscoveryRouterMock: BaseRouterMock, DiscoveryRouter { public override init() {} public func showCourseDetais(courseID: String, title: String) {} - public func showDiscoverySearch() {} + public func showWebDiscoveryDetails( + pathID: String, + discoveryType: DiscoveryWebviewType, + sourceScreen: LogistrationSourceScreen + ) {} + public func showUpdateRequiredView(showAccountLink: Bool) {} + public func showUpdateRecomendedView() {} + public func showDiscoverySearch(searchQuery: String? = nil) {} + public func showCourseScreens( + courseID: String, + isActive: Bool?, + courseStart: Date?, + courseEnd: Date?, + enrollmentStart: Date?, + enrollmentEnd: Date?, + title: String + ) {} + public func showWebProgramDetails( + pathID: String, + viewType: ProgramViewType + ) {} } #endif diff --git a/Discovery/Discovery/Presentation/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/DiscoveryViewModel.swift deleted file mode 100644 index 37514275b..000000000 --- a/Discovery/Discovery/Presentation/DiscoveryViewModel.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// DiscoveryViewModel.swift -// Discovery -// -// Created by  Stepanok Ivan on 16.09.2022. -// - -import Foundation -import Core -import SwiftUI - -public class DiscoveryViewModel: ObservableObject { - - public var nextPage = 1 - public var totalPages = 1 - public private(set) var fetchInProgress = false - - @Published var courses: [CourseItem] = [] - @Published var showError: Bool = false - - var errorMessage: String? { - didSet { - withAnimation { - showError = errorMessage != nil - } - } - } - - let connectivity: ConnectivityProtocol - private let interactor: DiscoveryInteractorProtocol - private let analytics: DiscoveryAnalytics - - public init( - interactor: DiscoveryInteractorProtocol, - connectivity: ConnectivityProtocol, - analytics: DiscoveryAnalytics - ) { - self.interactor = interactor - self.connectivity = connectivity - self.analytics = analytics - } - - @MainActor - public func getDiscoveryCourses(index: Int) async { - if !fetchInProgress { - if totalPages > 1 { - if index == courses.count - 3 { - if totalPages != 1 { - if nextPage <= totalPages { - await discovery(page: self.nextPage) - } - } - } - } - } - } - - @MainActor - func discovery(page: Int, withProgress: Bool = true) async { - fetchInProgress = withProgress - do { - if connectivity.isInternetAvaliable { - if page == 1 { - await courses = try interactor.discovery(page: page) - self.totalPages = 1 - self.nextPage = 1 - } else { - await courses += try interactor.discovery(page: page) - } - self.nextPage += 1 - if !courses.isEmpty { - totalPages = courses[0].numPages - } - - fetchInProgress = false - } else { - courses = try interactor.discoveryOffline() - self.nextPage += 1 - fetchInProgress = false - } - } catch let error { - fetchInProgress = false - if error.isInternetError || error is NoCachedDataError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } - } - } - - func discoveryCourseClicked(courseID: String, courseName: String) { - analytics.discoveryCourseClicked(courseID: courseID, courseName: courseName) - } - - func discoverySearchBarClicked() { - analytics.discoverySearchBarClicked() - } -} diff --git a/Course/Course/Presentation/Details/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift similarity index 81% rename from Course/Course/Presentation/Details/CourseDetailsView.swift rename to Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift index 168dd8de1..42e43fd3c 100644 --- a/Course/Course/Presentation/Details/CourseDetailsView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift @@ -9,11 +9,13 @@ import SwiftUI import Core import Kingfisher import WebKit +import Theme public struct CourseDetailsView: View { @ObservedObject private var viewModel: CourseDetailsViewModel @Environment(\.colorScheme) var colorScheme + @Environment(\.isHorizontal) var isHorizontal @State private var isOverviewRendering = true private var title: String private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @@ -54,7 +56,7 @@ public struct CourseDetailsView: View { if let courseDetails = viewModel.courseDetails { // MARK: - iPad - if idiom == .pad && viewModel.isHorisontal { + if viewModel.isHorisontal { HStack(alignment: .top) { VStack(alignment: .leading) { @@ -140,10 +142,30 @@ public struct CourseDetailsView: View { Spacer(minLength: 84) } } + if !viewModel.userloggedIn { + LogistrationBottomView { buttonAction in + switch buttonAction { + case .signIn: + viewModel.router.showLoginScreen( + sourceScreen: .courseDetail( + courseID, + viewModel.courseDetails?.courseTitle ?? "" + ) + ) + case .register: + viewModel.router.showRegisterScreen( + sourceScreen: .courseDetail( + courseID, + viewModel.courseDetails?.courseTitle ?? "" + ) + ) + } + } + } }.padding(.top, 8) .navigationBarHidden(false) .navigationBarBackButtonHidden(false) - .navigationTitle(CourseLocalization.Details.title) + .navigationTitle(DiscoveryLocalization.Details.title) .onReceive(NotificationCenter .Publisher(center: .default, @@ -197,31 +219,49 @@ private struct CourseStateView: View { var body: some View { switch viewModel.courseState() { case .enrollOpen: - StyledButton(CourseLocalization.Details.enrollNow, action: { - Task { - await viewModel.enrollToCourse(id: courseDetails.courseID) + StyledButton(DiscoveryLocalization.Details.enrollNow, action: { + if !viewModel.userloggedIn { + viewModel.router.showRegisterScreen( + sourceScreen: .courseDetail( + courseDetails.courseID, + courseDetails.courseTitle) + ) + } else { + Task { + await viewModel.enrollToCourse(id: courseDetails.courseID) + } } }) .padding(16) case .enrollClose: - Text(CourseLocalization.Details.enrollmentDateIsOver) + Text(DiscoveryLocalization.Details.enrollmentDateIsOver) .multilineTextAlignment(.center) .font(Theme.Fonts.titleSmall) .cardStyle() .padding(.vertical, 24) case .alreadyEnrolled: - StyledButton(CourseLocalization.Details.viewCourse, action: { - viewModel.viewCourseClicked(courseId: courseDetails.courseID, - courseName: courseDetails.courseTitle) - viewModel.router.showCourseScreens( - courseID: courseDetails.courseID, - isActive: nil, - courseStart: courseDetails.courseStart, - courseEnd: courseDetails.courseEnd, - enrollmentStart: courseDetails.enrollmentStart, - enrollmentEnd: courseDetails.enrollmentEnd, - title: title - ) + StyledButton(DiscoveryLocalization.Details.viewCourse, action: { + if !viewModel.userloggedIn { + viewModel.router.showRegisterScreen( + sourceScreen: .courseDetail( + courseDetails.courseID, + courseDetails.courseTitle) + ) + } else { + viewModel.viewCourseClicked( + courseId: courseDetails.courseID, + courseName: courseDetails.courseTitle + ) + viewModel.router.showCourseScreens( + courseID: courseDetails.courseID, + isActive: nil, + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, + title: title + ) + } }) .padding(16) } @@ -245,7 +285,7 @@ private struct CourseTitleView: View { var body: some View { VStack(alignment: .leading, spacing: 10) { - Text(courseDetails.courseDescription) + Text(courseDetails.courseDescription ?? "") .font(Theme.Fonts.labelSmall) .padding(.horizontal, 26) @@ -303,7 +343,7 @@ private struct CourseBannerView: View { .onFailureImage(CoreAssets.noCourseImage.image) .resizable() .aspectRatio(16/9, contentMode: .fill) - .frame(width: idiom == .pad ? 312 : proxy.size.width - 12) + .frame(width: 312) .opacity(animate ? 1 : 0) .onAppear { withAnimation(.linear(duration: 0.5)) { @@ -323,12 +363,13 @@ private struct CourseBannerView: View { struct CourseDetailsView_Previews: PreviewProvider { static var previews: some View { let vm = CourseDetailsViewModel( - interactor: CourseInteractor.mock, - router: CourseRouterMock(), - analytics: CourseAnalyticsMock(), + interactor: DiscoveryInteractor.mock, + router: DiscoveryRouterMock(), + analytics: DiscoveryAnalyticsMock(), config: ConfigMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity() + connectivity: Connectivity(), + storage: CoreStorageMock() ) CourseDetailsView( diff --git a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsViewModel.swift similarity index 85% rename from Course/Course/Presentation/Details/CourseDetailsViewModel.swift rename to Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsViewModel.swift index 6b1e6d747..eafab2542 100644 --- a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsViewModel.swift @@ -29,20 +29,26 @@ public class CourseDetailsViewModel: ObservableObject { } } - private let interactor: CourseInteractorProtocol - private let analytics: CourseAnalytics - let router: CourseRouter - let config: Config + private let interactor: DiscoveryInteractorProtocol + private let analytics: DiscoveryAnalytics + let router: DiscoveryRouter + let config: ConfigProtocol let cssInjector: CSSInjector let connectivity: ConnectivityProtocol + let storage: CoreStorage + + var userloggedIn: Bool { + return !(storage.user?.username?.isEmpty ?? true) + } public init( - interactor: CourseInteractorProtocol, - router: CourseRouter, - analytics: CourseAnalytics, - config: Config, + interactor: DiscoveryInteractorProtocol, + router: DiscoveryRouter, + analytics: DiscoveryAnalytics, + config: ConfigProtocol, cssInjector: CSSInjector, - connectivity: ConnectivityProtocol + connectivity: ConnectivityProtocol, + storage: CoreStorage ) { self.interactor = interactor self.router = router @@ -50,6 +56,7 @@ public class CourseDetailsViewModel: ObservableObject { self.config = config self.cssInjector = cssInjector self.connectivity = connectivity + self.storage = storage } @MainActor @@ -64,7 +71,7 @@ public class CourseDetailsViewModel: ObservableObject { isShowProgress = false } else { - courseDetails = try await interactor.getCourseDetailsOffline(courseID: courseID) + courseDetails = try await interactor.getLoadedCourseDetails(courseID: courseID) if let isEnrolled = courseDetails?.isEnrolled { self.courseDetails?.isEnrolled = isEnrolled } diff --git a/Discovery/Discovery/Presentation/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift similarity index 71% rename from Discovery/Discovery/Presentation/DiscoveryView.swift rename to Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index 8ca698572..461a705ea 100644 --- a/Discovery/Discovery/Presentation/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -7,14 +7,21 @@ import SwiftUI import Core +import Theme public struct DiscoveryView: View { @StateObject private var viewModel: DiscoveryViewModel - private let router: DiscoveryRouter + private var router: DiscoveryRouter + @State private var searchQuery: String = "" @State private var isRefreshing: Bool = false + private var sourceScreen: LogistrationSourceScreen + + @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.presentationMode) private var presentationMode + private let discoveryNew: some View = VStack(alignment: .leading) { Text(DiscoveryLocalization.Header.title1) .font(Theme.Fonts.displaySmall) @@ -23,10 +30,19 @@ public struct DiscoveryView: View { .font(Theme.Fonts.titleSmall) .foregroundColor(Theme.Colors.textPrimary) }.listRowBackground(Color.clear) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DiscoveryLocalization.Header.title1 + DiscoveryLocalization.Header.title2) - public init(viewModel: DiscoveryViewModel, router: DiscoveryRouter) { + public init( + viewModel: DiscoveryViewModel, + router: DiscoveryRouter, + searchQuery: String? = nil, + sourceScreen: LogistrationSourceScreen = .default + ) { self._viewModel = StateObject(wrappedValue: { viewModel }()) self.router = router + self._searchQuery = State(initialValue: searchQuery ?? "") + self.sourceScreen = sourceScreen } public var body: some View { @@ -45,11 +61,11 @@ public struct DiscoveryView: View { Spacer() } .onTapGesture { - router.showDiscoverySearch() + router.showDiscoverySearch(searchQuery: searchQuery) viewModel.discoverySearchBarClicked() } .frame(minHeight: 48) - .frame(maxWidth: 532) + .frame(maxWidth: .infinity) .background( Theme.Shapes.textInputShape .fill(Theme.Colors.textInputUnfocusedBackground) @@ -59,11 +75,14 @@ public struct DiscoveryView: View { .stroke(lineWidth: 1) .fill(Theme.Colors.textInputUnfocusedStroke) ).onTapGesture { - router.showDiscoverySearch() + router.showDiscoverySearch(searchQuery: searchQuery) viewModel.discoverySearchBarClicked() } + .padding(.top, 11.5) .padding(.horizontal, 24) .padding(.bottom, 20) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DiscoveryLocalization.search) ZStack { RefreshableScrollViewCompat(action: { @@ -72,7 +91,7 @@ public struct DiscoveryView: View { Task { await viewModel.discovery(page: 1, withProgress: false) } - }) { + }) { LazyVStack(spacing: 0) { HStack { discoveryNew @@ -97,7 +116,7 @@ public struct DiscoveryView: View { courseID: course.courseID, courseName: course.name ) - router.showCourseDetais( + viewModel.router.showCourseDetais( courseID: course.courseID, title: course.name ) @@ -114,7 +133,19 @@ public struct DiscoveryView: View { } VStack {}.frame(height: 40) } - }.frameLimit() + } + .frameLimit() + }.accessibilityAction {} + + if !viewModel.userloggedIn { + LogistrationBottomView { buttonAction in + switch buttonAction { + case .signIn: + viewModel.router.showLoginScreen(sourceScreen: .discovery) + case .register: + viewModel.router.showRegisterScreen(sourceScreen: .discovery) + } + } } }.padding(.top, 8) @@ -141,10 +172,19 @@ public struct DiscoveryView: View { } } } + .navigationBarHidden(sourceScreen != .startup) .onFirstAppear { + if !(searchQuery.isEmpty) { + router.showDiscoverySearch(searchQuery: searchQuery) + searchQuery = "" + } Task { await viewModel.discovery(page: 1) + if case let .courseDetail(courseID, courseTitle) = sourceScreen { + viewModel.router.showCourseDetais(courseID: courseID, title: courseTitle) + } } + viewModel.setupNotifications() } .background(Theme.Colors.background.ignoresSafeArea()) } @@ -153,8 +193,12 @@ public struct DiscoveryView: View { #if DEBUG struct DiscoveryView_Previews: PreviewProvider { static var previews: some View { - let vm = DiscoveryViewModel(interactor: DiscoveryInteractor.mock, connectivity: Connectivity(), - analytics: DiscoveryAnalyticsMock()) + let vm = DiscoveryViewModel(router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: DiscoveryInteractor.mock, + connectivity: Connectivity(), + analytics: DiscoveryAnalyticsMock(), + storage: CoreStorageMock()) let router = DiscoveryRouterMock() DiscoveryView(viewModel: vm, router: router) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift new file mode 100644 index 000000000..bcb8f1022 --- /dev/null +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift @@ -0,0 +1,164 @@ +// +// DiscoveryViewModel.swift +// Discovery +// +// Created by  Stepanok Ivan on 16.09.2022. +// + +import Combine +import Core +import SwiftUI + +public class DiscoveryViewModel: ObservableObject { + + var nextPage = 1 + var totalPages = 1 + private(set) var fetchInProgress = false + private var cancellables = Set() + private var updateShowedOnce: Bool = false + + @Published var courses: [CourseItem] = [] + @Published var showError: Bool = false + + var userloggedIn: Bool { + return !(storage.user?.username?.isEmpty ?? true) + } + + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + let router: DiscoveryRouter + let config: ConfigProtocol + let connectivity: ConnectivityProtocol + private let interactor: DiscoveryInteractorProtocol + private let analytics: DiscoveryAnalytics + private let storage: CoreStorage + + public init( + router: DiscoveryRouter, + config: ConfigProtocol, + interactor: DiscoveryInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DiscoveryAnalytics, + storage: CoreStorage + ) { + self.router = router + self.config = config + self.interactor = interactor + self.connectivity = connectivity + self.analytics = analytics + self.storage = storage + } + + @MainActor + public func getDiscoveryCourses(index: Int) async { + if !fetchInProgress { + if totalPages > 1 { + if index == courses.count - 3 { + if totalPages != 1 { + if nextPage <= totalPages { + await discovery(page: self.nextPage) + } + } + } + } + } + } + + func setupNotifications() { + NotificationCenter.default.publisher(for: .onActualVersionReceived) + .sink { [weak self] notification in + if let latestVersion = notification.object as? String { + if let info = Bundle.main.infoDictionary { + guard let currentVersion = info["CFBundleShortVersionString"] as? String, + let self else { return } + switch self.compareVersions(currentVersion, latestVersion) { + case .orderedAscending: + if self.updateShowedOnce == false { + DispatchQueue.main.async { + self.router.showUpdateRecomendedView() + } + self.updateShowedOnce = true + } + default: + return + } + } + } + }.store(in: &cancellables) + } + + @MainActor + func discovery(page: Int, withProgress: Bool = true) async { + fetchInProgress = withProgress + do { + if connectivity.isInternetAvaliable { + if page == 1 { + await courses = try interactor.discovery(page: page) + self.totalPages = 1 + self.nextPage = 1 + } else { + await courses += try interactor.discovery(page: page) + } + self.nextPage += 1 + if !courses.isEmpty { + totalPages = courses[0].numPages + } + + fetchInProgress = false + } else { + courses = try interactor.discoveryOffline() + self.nextPage += 1 + fetchInProgress = false + } + } catch let error { + fetchInProgress = false + if error.isInternetError || error is NoCachedDataError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else if error.isUpdateRequeiredError { + self.router.showUpdateRequiredView(showAccountLink: true) + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + func discoveryCourseClicked(courseID: String, courseName: String) { + analytics.discoveryCourseClicked(courseID: courseID, courseName: courseName) + } + + func discoverySearchBarClicked() { + analytics.discoverySearchBarClicked() + } + + private func compareVersions(_ version1: String, _ version2: String) -> ComparisonResult { + let components1 = version1.components(separatedBy: ".").prefix(2) + let components2 = version2.components(separatedBy: ".").prefix(2) + + guard let major1 = Int(components1.first ?? ""), + let minor1 = Int(components1.last ?? ""), + let major2 = Int(components2.first ?? ""), + let minor2 = Int(components2.last ?? "") else { + return .orderedSame + } + + if major1 < major2 { + return .orderedAscending + } else if major1 > major2 { + return .orderedDescending + } else { + if minor1 < minor2 { + return .orderedAscending + } else if minor1 > minor2 { + return .orderedDescending + } else { + return .orderedSame + } + } + } +} diff --git a/Discovery/Discovery/Presentation/SearchView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift similarity index 91% rename from Discovery/Discovery/Presentation/SearchView.swift rename to Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift index 09e4619cf..782956544 100644 --- a/Discovery/Discovery/Presentation/SearchView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift @@ -7,16 +7,21 @@ import SwiftUI import Core +import Theme public struct SearchView: View { + @FocusState + private var focused: Bool + @ObservedObject private var viewModel: SearchViewModel @State private var animated: Bool = false - @State private var becomeFirstResponderRunOnce = false - public init(viewModel: SearchViewModel) { + public init(viewModel: SearchViewModel, searchQuery: String? = nil) { self.viewModel = viewModel + self.viewModel.searchText = searchQuery ?? "" + self.viewModel.isSearchActive = !(searchQuery?.isEmpty ?? false) } public var body: some View { @@ -32,12 +37,13 @@ public struct SearchView: View { HStack(spacing: 11) { Image(systemName: "magnifyingglass") .padding(.leading, 16) - .padding(.top, -1) + .padding(.top, 1) .foregroundColor( viewModel.isSearchActive ? Theme.Colors.accentColor : Theme.Colors.textPrimary ) + .accessibilityHidden(true) TextField( !viewModel.isSearchActive @@ -47,13 +53,10 @@ public struct SearchView: View { onEditingChanged: { editing in viewModel.isSearchActive = editing } - ) - .introspect(.textField, on: .iOS(.v14, .v15, .v16, .v17), customize: { textField in - if !becomeFirstResponderRunOnce { - textField.becomeFirstResponder() - self.becomeFirstResponderRunOnce = true + ).focused($focused) + .onAppear { + self.focused = true } - }) .foregroundColor(Theme.Colors.textPrimary) Spacer() if !viewModel.searchText.trimmingCharacters(in: .whitespaces).isEmpty { @@ -68,7 +71,7 @@ public struct SearchView: View { } } .frame(minHeight: 48) - .frame(maxWidth: 532) + .frame(maxWidth: .infinity) .background( Theme.Shapes.textInputShape .fill(viewModel.isSearchActive @@ -156,6 +159,10 @@ public struct SearchView: View { } } } + + .onDisappear { + viewModel.searchText = "" + } .background(Theme.Colors.background.ignoresSafeArea()) .addTapToEndEditing(isForced: true) } @@ -169,6 +176,8 @@ public struct SearchView: View { .font(Theme.Fonts.titleSmall) .foregroundColor(Theme.Colors.textPrimary) }.listRowBackground(Color.clear) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DiscoveryLocalization.Search.title + searchDescription(viewModel: viewModel)) } private func searchDescription(viewModel: SearchViewModel) -> String { diff --git a/Discovery/Discovery/Presentation/SearchViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift similarity index 100% rename from Discovery/Discovery/Presentation/SearchViewModel.swift rename to Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift diff --git a/Discovery/Discovery/Presentation/UpdateViews/UpdateNotificationView.swift b/Discovery/Discovery/Presentation/UpdateViews/UpdateNotificationView.swift new file mode 100644 index 000000000..e25175759 --- /dev/null +++ b/Discovery/Discovery/Presentation/UpdateViews/UpdateNotificationView.swift @@ -0,0 +1,61 @@ +// +// UpdateNotificationView.swift +// Discovery +// +// Created by  Stepanok Ivan on 23.10.2023. +// + +import SwiftUI +import Core +import Theme + +public struct UpdateNotificationView: View { + + private let config: ConfigProtocol + + public init(config: ConfigProtocol) { + self.config = config + } + + public var body: some View { + ZStack { + VStack { + Spacer() + HStack(spacing: 10) { + Image(systemName: "arrow.up.circle") + .resizable() + .frame(width: 36, + height: 36) + .foregroundColor(Theme.Colors.white) + VStack(alignment: .leading) { + Text(DiscoveryLocalization.updateNeededTitle) + .font(Theme.Fonts.titleMedium) + Text(DiscoveryLocalization.updateNewAvaliable) + .font(Theme.Fonts.bodySmall) + }.foregroundColor(Theme.Colors.white) + Spacer() + } + .padding(16) + .background(Theme.Colors.accentColor) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .shadow(color: Color.black.opacity(0.4), radius: 12, x: 0, y: 0) + .padding(24) + + } + }.onTapGesture { + openAppStore() + } + } + private func openAppStore() { + guard let appStoreURL = URL(string: config.appStoreLink) else { return } + UIApplication.shared.open(appStoreURL) + } +} + +#if DEBUG +struct UpdateNotificationView_Previews: PreviewProvider { + static var previews: some View { + UpdateNotificationView(config: ConfigMock()) + } +} +#endif diff --git a/Discovery/Discovery/Presentation/UpdateViews/UpdateRecommendedView.swift b/Discovery/Discovery/Presentation/UpdateViews/UpdateRecommendedView.swift new file mode 100644 index 000000000..c2716aa82 --- /dev/null +++ b/Discovery/Discovery/Presentation/UpdateViews/UpdateRecommendedView.swift @@ -0,0 +1,86 @@ +// +// UpdateRecommendedView.swift +// Discovery +// +// Created by  Stepanok Ivan on 23.10.2023. +// + +import SwiftUI +import Core +import Theme + +public struct UpdateRecommendedView: View { + + @Environment (\.isHorizontal) private var isHorizontal + private let router: DiscoveryRouter + private let config: ConfigProtocol + + public init(router: DiscoveryRouter, config: ConfigProtocol) { + self.router = router + self.config = config + } + + public var body: some View { + ZStack { + Color.black.opacity(0.5) + .ignoresSafeArea() + .onTapGesture { + router.dismiss(animated: true) + NotificationCenter.default.post(name: .onNewVersionAvaliable, object: nil) + } + VStack(spacing: 10) { + Image(systemName: "arrow.up.circle") + .resizable() + .frame(width: isHorizontal ? 50 : 110, + height: isHorizontal ? 50 : 110) + .foregroundColor(Theme.Colors.accentColor) + .padding(.bottom, isHorizontal ? 0 : 20) + Text(DiscoveryLocalization.updateNeededTitle) + .font(Theme.Fonts.titleMedium) + Text(DiscoveryLocalization.updateNeededDescription) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.avatarStroke) + .multilineTextAlignment(.center) + + HStack(spacing: 28) { + Button(action: { + router.dismiss(animated: true) + NotificationCenter.default.post(name: .onNewVersionAvaliable, object: nil) + }, label: { + HStack { + Text(DiscoveryLocalization.updateNeededNotNow) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.accentColor) + }.padding(8) + }) + + StyledButton(DiscoveryLocalization.updateButton, action: { + openAppStore() + }).fixedSize() + }.padding(.top, isHorizontal ? 0 : 44) + + }.padding(isHorizontal ? 40 : 40) + .background(Theme.Colors.background) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .frame(maxWidth: 400, maxHeight: 400) + .padding(24) + .shadow(color: Color.black.opacity(0.4), radius: 12, x: 0, y: 0) + }.navigationTitle(DiscoveryLocalization.updateDeprecatedApp) + } + + private func openAppStore() { + guard let appStoreURL = URL(string: config.appStoreLink) else { return } + UIApplication.shared.open(appStoreURL) + } +} + +#if DEBUG +struct UpdateRecommendedView_Previews: PreviewProvider { + static var previews: some View { + UpdateRecommendedView( + router: DiscoveryRouterMock(), + config: ConfigMock() + ) + } +} +#endif diff --git a/Discovery/Discovery/Presentation/UpdateViews/UpdateRequiredView.swift b/Discovery/Discovery/Presentation/UpdateViews/UpdateRequiredView.swift new file mode 100644 index 000000000..cbadd0ebf --- /dev/null +++ b/Discovery/Discovery/Presentation/UpdateViews/UpdateRequiredView.swift @@ -0,0 +1,77 @@ +// +// UpdateRequiredView.swift +// Discovery +// +// Created by  Stepanok Ivan on 23.10.2023. +// + +import SwiftUI +import Core +import Theme + +public struct UpdateRequiredView: View { + + @Environment (\.isHorizontal) private var isHorizontal + private let router: DiscoveryRouter + private let config: ConfigProtocol + private let showAccountLink: Bool + + public init(router: DiscoveryRouter, config: ConfigProtocol, showAccountLink: Bool = true) { + self.router = router + self.config = config + self.showAccountLink = showAccountLink + } + + public var body: some View { + ZStack { + VStack(spacing: 10) { + CoreAssets.warningFilled.swiftUIImage + .resizable() + .frame(width: isHorizontal ? 50 : 110, + height: isHorizontal ? 50 : 110) + Text(DiscoveryLocalization.updateRequiredTitle) + .font(Theme.Fonts.titleMedium) + Text(DiscoveryLocalization.updateRequiredDescription) + .font(Theme.Fonts.titleSmall) + .multilineTextAlignment(.center) + + HStack(spacing: 28) { + if showAccountLink { + Button(action: { + NotificationCenter.default.post(name: .onAppUpgradeAccountSettingsTapped, object: "block") + router.back(animated: false) + }, label: { + HStack { + Text(DiscoveryLocalization.updateAccountSettings) + .font(Theme.Fonts.labelLarge) + }.padding(8) + }) + } + StyledButton(DiscoveryLocalization.updateButton, action: { + openAppStore() + }).fixedSize() + }.padding(.top, isHorizontal ? 10 : 44) + + }.padding(40) + .frame(maxWidth: 400) + }.navigationTitle(DiscoveryLocalization.updateDeprecatedApp) + .navigationBarBackButtonHidden() + } + + private func openAppStore() { + guard let appStoreURL = URL(string: config.appStoreLink) else { return } + UIApplication.shared.open(appStoreURL) + } +} + +#if DEBUG +struct UpdateRequiredView_Previews: PreviewProvider { + static var previews: some View { + UpdateRequiredView( + router: DiscoveryRouterMock(), + config: ConfigMock() + ) + .loadFonts() + } +} +#endif diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift new file mode 100644 index 000000000..2fa9071bd --- /dev/null +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift @@ -0,0 +1,30 @@ +// +// DiscoveryURIDetails.swift +// Discovery +// +// Created by SaeedBashir on 12/18/23. +// + +import Foundation +import Core + +// Define your uri scheme +public enum URIString: String { + case pathPlaceHolder = "{path_id}" +} + +public enum URLParameterKeys: String, RawStringExtractable { + case pathId = "path_id" + case courseId = "course_id" + case emailOptIn = "email_opt_in" +} + +// Define your hosts +public enum WebviewActions: String { + case courseEnrollment = "enroll" + case courseDetail = "course_info" + case enrolledCourseDetail = "enrolled_course_info" + case enrolledProgramDetail = "enrolled_program_info" + case programDetail = "program_info" + case courseProgram = "course" +} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift new file mode 100644 index 000000000..a9d801f4d --- /dev/null +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift @@ -0,0 +1,183 @@ +// +// DiscoveryWebview.swift +// Discovery +// +// Created by SaeedBashir on 12/16/23. +// + +import Foundation +import SwiftUI +import Theme +import Core + +public enum DiscoveryWebviewType: Equatable { + case discovery + case courseDetail(String) + case programDetail(String) +} + +public struct DiscoveryWebview: View { + @State private var searchQuery: String = "" + @State private var isLoading: Bool = true + + @ObservedObject private var viewModel: DiscoveryWebviewViewModel + private var router: DiscoveryRouter + private var discoveryType: DiscoveryWebviewType + private var pathID: String + + private var URLString: String { + switch discoveryType { + case .discovery: + if !searchQuery.isEmpty { + let baseURL = viewModel.config.discovery.webview.baseURL ?? "" + return buildQuery(baseURL: baseURL, params: ["q": searchQuery]) + } + + return viewModel.config.discovery.webview.baseURL ?? "" + case .courseDetail: + let template = viewModel.config.discovery.webview.courseDetailTemplate + return template?.replacingOccurrences( + of: URIString.pathPlaceHolder.rawValue, + with: pathID + ) ?? "" + + case .programDetail: + let template = viewModel.config.discovery.webview.programDetailTemplate + return template?.replacingOccurrences( + of: URIString.pathPlaceHolder.rawValue, + with: pathID + ) ?? "" + } + } + + private func buildQuery(baseURL: String, params: [String: String]) -> String { + var query = baseURL + for param in params { + let join = query.contains("?") ? "&" : "?" + let value = param.key + "=" + param.value + if !query.contains(find: value) { + query = query + join + value + } + } + + return query + } + + public init( + viewModel: DiscoveryWebviewViewModel, + router: DiscoveryRouter, + searchQuery: String? = nil, + discoveryType: DiscoveryWebviewType = .discovery, + pathID: String = "" + ) { + self.viewModel = viewModel + self.router = router + self._searchQuery = State(initialValue: searchQuery ?? "") + self.discoveryType = discoveryType + self.pathID = pathID + + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } + + public var body: some View { + GeometryReader { proxy in + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "" + ), + isLoading: $isLoading, + refreshCookies: {}, + navigationDelegate: viewModel + ) + + if isLoading || viewModel.showProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + + if !viewModel.userloggedIn, !isLoading { + LogistrationBottomView { buttonAction in + switch buttonAction { + case .signIn: + viewModel.router.showLoginScreen(sourceScreen: sourceScreen) + case .register: + viewModel.router.showRegisterScreen(sourceScreen: sourceScreen) + } + } + } + } + + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + NotificationCenter.default.post( + name: .webviewReloadNotification, + object: nil + ) + }) + } + .navigationBarHidden(viewModel.sourceScreen == .default && discoveryType == .discovery) + .navigationTitle(CoreLocalization.Mainscreen.discovery) + .background(Theme.Colors.background.ignoresSafeArea()) + .onFirstAppear { + if case let .courseDetail(pathID, _) = viewModel.sourceScreen { + viewModel.router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .courseDetail(pathID), + sourceScreen: .discovery + ) + } else if case let .programDetails(pathID) = viewModel.sourceScreen { + viewModel.router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .programDetail(pathID), + sourceScreen: .discovery + ) + } + + // Reseting the source screen + viewModel.sourceScreen = .discovery + } + } + + private var sourceScreen: LogistrationSourceScreen { + switch discoveryType { + case .discovery: + return .discovery + case .courseDetail(let pathID): + return .courseDetail(pathID, "") + case .programDetail(let pathID): + return .programDetails(pathID) + } + } +} + +fileprivate extension String { + func contains(find: String) -> Bool { + return range(of: find) != nil + } +} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift new file mode 100644 index 000000000..99cd937b0 --- /dev/null +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -0,0 +1,236 @@ +// +// DiscoveryWebviewViewModel.swift +// Discovery +// +// Created by SaeedBashir on 12/16/23. +// + +import Foundation +import Core +import SwiftUI +import WebKit + +public class DiscoveryWebviewViewModel: ObservableObject { + @Published var courseDetails: CourseDetails? + @Published private(set) var showProgress = false + @Published var showError: Bool = false + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + let router: DiscoveryRouter + let config: ConfigProtocol + let connectivity: ConnectivityProtocol + private let interactor: DiscoveryInteractorProtocol + private let analytics: DiscoveryAnalytics + var request: URLRequest? + private let storage: CoreStorage + var sourceScreen: LogistrationSourceScreen + + var userloggedIn: Bool { + return storage.user?.username?.isEmpty == false + } + + public init( + router: DiscoveryRouter, + config: ConfigProtocol, + interactor: DiscoveryInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DiscoveryAnalytics, + storage: CoreStorage, + sourceScreen: LogistrationSourceScreen = .default + ) { + self.router = router + self.config = config + self.interactor = interactor + self.connectivity = connectivity + self.analytics = analytics + self.storage = storage + self.sourceScreen = sourceScreen + } + + @MainActor + func getCourseDetail(courseID: String) async throws -> CourseDetails? { + return try await interactor.getCourseDetails(courseID: courseID) + } + + @MainActor + func enrollTo(courseID: String) async { + do { + guard userloggedIn else { + router.showRegisterScreen(sourceScreen: .discovery) + return + } + + showProgress = true + if courseDetails == nil { + courseDetails = try await getCourseDetail(courseID: courseID) + } + + if courseDetails?.isEnrolled ?? false || courseState == .alreadyEnrolled { + showProgress = false + showCourseDetails() + return + } + + analytics.courseEnrollClicked(courseId: courseID, courseName: courseDetails?.courseTitle ?? "") + _ = try await interactor.enrollToCourse(courseID: courseID) + analytics.courseEnrollSuccess(courseId: courseID, courseName: courseDetails?.courseTitle ?? "") + courseDetails?.isEnrolled = true + showProgress = false + NotificationCenter.default.post(name: .onCourseEnrolled, object: courseID) + showCourseDetails() + } catch let error { + showProgress = false + if error.isInternetError || error is NoCachedDataError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + private var courseState: CourseState? { + guard courseDetails?.isEnrolled == false else { return nil } + + if let enrollmentStart = courseDetails?.enrollmentStart, let enrollmentEnd = courseDetails?.enrollmentEnd { + let enrollmentsRange = DateInterval(start: enrollmentStart, end: enrollmentEnd) + if enrollmentsRange.contains(Date()) { + return .enrollOpen + } else { + return .enrollClose + } + } else { + return .enrollOpen + } + } +} + +extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { + @MainActor + public func webView( + _ webView: WKWebView, + shouldLoad request: URLRequest, + navigationAction: WKNavigationAction + ) async -> Bool { + guard let URL = request.url else { return false } + + if let urlAction = urlAction(from: URL), + await handleNavigation(url: URL, urlAction: urlAction) { + return true + } + + let capturedLink = navigationAction.navigationType == .linkActivated + let outsideLink = (request.mainDocumentURL?.host != self.request?.url?.host) + var externalLink = false + + if let queryParameters = request.url?.queryParameters, + let externalLinkValue = queryParameters["external_link"] as? String, + externalLinkValue.caseInsensitiveCompare("true") == .orderedSame { + externalLink = true + } + + if let url = request.url, outsideLink || capturedLink || externalLink, UIApplication.shared.canOpenURL(url) { + router.presentAlert( + alertTitle: DiscoveryLocalization.Alert.leavingAppTitle, + alertMessage: DiscoveryLocalization.Alert.leavingAppMessage, + positiveAction: CoreLocalization.Webview.Alert.continue, + onCloseTapped: { [weak self] in + self?.router.dismiss(animated: true) + }, okTapped: { + UIApplication.shared.open(url, options: [:]) + }, type: .default(positiveAction: CoreLocalization.Webview.Alert.continue, image: nil) + ) + return true + } + + return false + } + + private func urlAction(from url: URL) -> WebviewActions? { + guard isValidAppURLScheme(url), + let url = WebviewActions(rawValue: url.appURLHost) else { return nil } + return url + } + + @MainActor + private func handleNavigation(url: URL, urlAction: WebviewActions) async -> Bool { + switch urlAction { + case .courseEnrollment: + if let urlData = parse(url: url), let courseID = urlData.courseId { + await enrollTo(courseID: courseID) + } + case .courseDetail: + guard let pathID = detailPathID(from: url) else { return false } + router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .courseDetail(pathID), + sourceScreen: sourceScreen + ) + case .enrolledCourseDetail: + return showCourseDetails() + + case .programDetail: + guard let pathID = programDetailPathId(from: url) else { return false } + router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .programDetail(pathID), + sourceScreen: sourceScreen + ) + + default: + break + } + + return true + } + + private func detailPathID(from url: URL) -> String? { + guard isValidAppURLScheme(url), + let path = url.queryParameters?[URLParameterKeys.pathId] as? String, + url.appURLHost == WebviewActions.courseDetail.rawValue else { return nil } + + return path + } + + private func parse(url: URL) -> (courseId: String?, emailOptIn: Bool)? { + guard isValidAppURLScheme(url) else { return nil } + + let courseId = url.queryParameters?[URLParameterKeys.courseId] as? String + let emailOptIn = (url.queryParameters?[URLParameterKeys.emailOptIn] as? String).flatMap {Bool($0)} + + return (courseId, emailOptIn ?? false) + } + + private func programDetailPathId(from url: URL) -> String? { + guard isValidAppURLScheme(url), + let path = url.queryParameters?[URLParameterKeys.pathId] as? String, + url.appURLHost == WebviewActions.programDetail.rawValue else { return nil } + + return path + } + + @discardableResult private func showCourseDetails() -> Bool { + guard let courseDetails = courseDetails else { return false } + + router.showCourseScreens( + courseID: courseDetails.courseID, + isActive: nil, + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, + title: courseDetails.courseTitle + ) + + return true + } + + private func isValidAppURLScheme(_ url: URL) -> Bool { + return url.scheme ?? "" == config.URIScheme + } +} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/URL+PathExtension.swift b/Discovery/Discovery/Presentation/WebDiscovery/URL+PathExtension.swift new file mode 100644 index 000000000..09cde0882 --- /dev/null +++ b/Discovery/Discovery/Presentation/WebDiscovery/URL+PathExtension.swift @@ -0,0 +1,32 @@ +// +// URL+PathExtension.swift +// Discovery +// +// Created by SaeedBashir on 12/18/23. +// + +import Foundation + +public extension URL { + var appURLHost: String { + return host ?? "" + } + + var queryParameters: [String: Any]? { + guard let queryString = query else { + return nil + } + var queryParameters = [String: Any]() + let parameters = queryString.components(separatedBy: "&") + for parameter in parameters { + let keyValuePair = parameter.components(separatedBy: "=") + // Parameter will be ignored if invalid data for keyValuePair + if keyValuePair.count == 2 { + let key = keyValuePair[0] + let value = keyValuePair[1] + queryParameters[key] = value + } + } + return queryParameters + } +} diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift new file mode 100644 index 000000000..7a710b897 --- /dev/null +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift @@ -0,0 +1,113 @@ +// +// ProgramWebviewView.swift +// Discovery +// +// Created by SaeedBashir on 1/23/24. +// + +import Foundation +import SwiftUI +import Theme +import Core + +public enum ProgramViewType: Equatable { + case program + case programDetail +} + +public struct ProgramWebviewView: View { + @State private var isLoading: Bool = true + + @ObservedObject private var viewModel: ProgramWebviewViewModel + private var router: DiscoveryRouter + private var viewType: ProgramViewType + private var pathID: String + + private var URLString: String { + switch viewType { + case .program: + return viewModel.config.program.webview.baseURL ?? "" + case .programDetail: + let template = viewModel.config.program.webview.programDetailTemplate + return template?.replacingOccurrences( + of: URIString.pathPlaceHolder.rawValue, + with: pathID + ) ?? "" + } + } + + public init( + viewModel: ProgramWebviewViewModel, + router: DiscoveryRouter, + viewType: ProgramViewType = .program, + pathID: String = "" + ) { + self.viewModel = viewModel + self.router = router + self.viewType = viewType + self.pathID = pathID + + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } + + public var body: some View { + GeometryReader { proxy in + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "" + ), + isLoading: $isLoading, + refreshCookies: { + await viewModel.updateCookies( + force: true + ) + }, + navigationDelegate: viewModel + ) + + if isLoading || viewModel.showProgress || viewModel.updatingCookies { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + } + + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + NotificationCenter.default.post( + name: .webviewReloadNotification, + object: nil + ) + }) + } + .navigationBarHidden(viewType == .program) + .navigationTitle(CoreLocalization.Mainscreen.programs) + .background(Theme.Colors.background.ignoresSafeArea()) + .animation(.default, value: viewModel.showError) + } +} diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift new file mode 100644 index 000000000..82c234882 --- /dev/null +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift @@ -0,0 +1,236 @@ +// +// ProgramWebviewViewModel.swift +// Discovery +// +// Created by SaeedBashir on 1/23/24. +// + +import Foundation +import Core +import SwiftUI +import WebKit + +public class ProgramWebviewViewModel: ObservableObject, WebviewCookiesUpdateProtocol { + @Published var courseDetails: CourseDetails? + @Published private(set) var showProgress = false + @Published var showError: Bool = false + @Published public var updatingCookies: Bool = false + @Published public var cookiesReady: Bool = false + + public var errorMessage: String? { + didSet { + showError = errorMessage != nil + } + } + + let router: DiscoveryRouter + let config: ConfigProtocol + let connectivity: ConnectivityProtocol + private let interactor: DiscoveryInteractorProtocol + private let analytics: DiscoveryAnalytics + var request: URLRequest? + public let authInteractor: AuthInteractorProtocol + + public init( + router: DiscoveryRouter, + config: ConfigProtocol, + interactor: DiscoveryInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DiscoveryAnalytics, + authInteractor: AuthInteractorProtocol + ) { + self.router = router + self.config = config + self.interactor = interactor + self.connectivity = connectivity + self.analytics = analytics + self.authInteractor = authInteractor + } + + @MainActor + func getCourseDetail(courseID: String) async throws -> CourseDetails? { + return try await interactor.getCourseDetails(courseID: courseID) + } + + @MainActor + func enrollTo(courseID: String) async { + do { + showProgress = true + if courseDetails == nil { + courseDetails = try await getCourseDetail(courseID: courseID) + } + + if courseDetails?.isEnrolled ?? false || courseState == .alreadyEnrolled { + showProgress = false + showCourseDetails() + return + } + + analytics.courseEnrollClicked(courseId: courseID, courseName: courseDetails?.courseTitle ?? "") + _ = try await interactor.enrollToCourse(courseID: courseID) + analytics.courseEnrollSuccess(courseId: courseID, courseName: courseDetails?.courseTitle ?? "") + courseDetails?.isEnrolled = true + showProgress = false + NotificationCenter.default.post(name: .onCourseEnrolled, object: courseID) + showCourseDetails() + courseDetails = nil + } catch let error { + showProgress = false + if error.isInternetError || error is NoCachedDataError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + private var courseState: CourseState? { + guard courseDetails?.isEnrolled == false else { return nil } + + if let enrollmentStart = courseDetails?.enrollmentStart, let enrollmentEnd = courseDetails?.enrollmentEnd { + let enrollmentsRange = DateInterval(start: enrollmentStart, end: enrollmentEnd) + if enrollmentsRange.contains(Date()) { + return .enrollOpen + } else { + return .enrollClose + } + } else { + return .enrollOpen + } + } +} + +extension ProgramWebviewViewModel: WebViewNavigationDelegate { + @MainActor + public func webView( + _ webView: WKWebView, + shouldLoad request: URLRequest, + navigationAction: WKNavigationAction + ) async -> Bool { + guard let URL = request.url else { return false } + + if let urlAction = urlAction(from: URL), + await handleNavigation(url: URL, urlAction: urlAction) { + return true + } + + let capturedLink = navigationAction.navigationType == .linkActivated + let outsideLink = (request.mainDocumentURL?.host != self.request?.url?.host) + var externalLink = false + + if let queryParameters = request.url?.queryParameters, + let externalLinkValue = queryParameters["external_link"] as? String, + externalLinkValue.caseInsensitiveCompare("true") == .orderedSame { + externalLink = true + } + + if let url = request.url, outsideLink || capturedLink || externalLink, UIApplication.shared.canOpenURL(url) { + router.presentAlert( + alertTitle: DiscoveryLocalization.Alert.leavingAppTitle, + alertMessage: DiscoveryLocalization.Alert.leavingAppMessage, + positiveAction: CoreLocalization.Webview.Alert.continue, + onCloseTapped: { [weak self] in + self?.router.dismiss(animated: true) + }, okTapped: { + UIApplication.shared.open(url, options: [:]) + }, type: .default(positiveAction: CoreLocalization.Webview.Alert.continue, image: nil) + ) + return true + } + + return false + } + + private func urlAction(from url: URL) -> WebviewActions? { + guard isValidAppURLScheme(url), + let url = WebviewActions(rawValue: url.appURLHost) else { return nil } + return url + } + + @MainActor + private func handleNavigation(url: URL, urlAction: WebviewActions) async -> Bool { + switch urlAction { + case .courseEnrollment: + if let urlData = parse(url: url), let courseID = urlData.courseId { + await enrollTo(courseID: courseID) + } + case .courseDetail: + guard let pathID = detailPathID(from: url) else { return false } + router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .courseDetail(pathID), + sourceScreen: .default + ) + case .enrolledCourseDetail: + if let urlData = parse(url: url), let courseID = urlData.courseId { + showProgress = true + do { + courseDetails = try await getCourseDetail(courseID: courseID) + showCourseDetails() + courseDetails = nil + showProgress = false + } catch let error { + showProgress = false + if error.isInternetError || error is NoCachedDataError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + case .programDetail: + guard let pathID = detailPathID(from: url) else { return false } + router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .programDetail(pathID), + sourceScreen: .default + ) + case .enrolledProgramDetail: + guard let pathID = detailPathID(from: url) else { return false } + router.showWebProgramDetails(pathID: pathID, viewType: .programDetail) + + default: + break + } + + return true + } + + private func detailPathID(from url: URL) -> String? { + guard isValidAppURLScheme(url), + let path = url.queryParameters?[URLParameterKeys.pathId] as? String + else { return nil } + + return path + } + + private func parse(url: URL) -> (courseId: String?, emailOptIn: Bool)? { + guard isValidAppURLScheme(url) else { return nil } + + let courseId = url.queryParameters?[URLParameterKeys.courseId] as? String + let emailOptIn = (url.queryParameters?[URLParameterKeys.emailOptIn] as? String).flatMap {Bool($0)} + + return (courseId, emailOptIn ?? false) + } + + @discardableResult private func showCourseDetails() -> Bool { + guard let courseDetails = courseDetails else { return false } + + router.showCourseScreens( + courseID: courseDetails.courseID, + isActive: nil, + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, + title: courseDetails.courseTitle + ) + + return true + } + + private func isValidAppURLScheme(_ url: URL) -> Bool { + return url.scheme ?? "" == config.URIScheme + } +} diff --git a/Discovery/Discovery/SwiftGen/Strings.swift b/Discovery/Discovery/SwiftGen/Strings.swift index b3abc48df..8bd26400c 100644 --- a/Discovery/Discovery/SwiftGen/Strings.swift +++ b/Discovery/Discovery/SwiftGen/Strings.swift @@ -21,6 +21,42 @@ public enum DiscoveryLocalization { /// /// Created by  Stepanok Ivan on 19.09.2022. public static let title = DiscoveryLocalization.tr("Localizable", "TITLE", fallback: "Discover") + /// Account Settings + public static let updateAccountSettings = DiscoveryLocalization.tr("Localizable", "UPDATE_ACCOUNT_SETTINGS", fallback: "Account Settings") + /// Update + public static let updateButton = DiscoveryLocalization.tr("Localizable", "UPDATE_BUTTON", fallback: "Update") + /// Deprecated App Version + public static let updateDeprecatedApp = DiscoveryLocalization.tr("Localizable", "UPDATE_DEPRECATED_APP", fallback: "Deprecated App Version") + /// We recommend that you update to the latest version. Upgrade now to receive the latest features and fixes. + public static let updateNeededDescription = DiscoveryLocalization.tr("Localizable", "UPDATE_NEEDED_DESCRIPTION", fallback: "We recommend that you update to the latest version. Upgrade now to receive the latest features and fixes.") + /// Not Now + public static let updateNeededNotNow = DiscoveryLocalization.tr("Localizable", "UPDATE_NEEDED_NOT_NOW", fallback: "Not Now") + /// App Update + public static let updateNeededTitle = DiscoveryLocalization.tr("Localizable", "UPDATE_NEEDED_TITLE", fallback: "App Update") + /// New update available! Upgrade now to receive the latest features and fixes + public static let updateNewAvaliable = DiscoveryLocalization.tr("Localizable", "UPDATE_NEW_AVALIABLE", fallback: "New update available! Upgrade now to receive the latest features and fixes") + /// This version of the OpenEdX app is out-of-date. To continue learning and get the latest features and fixes, please upgrade to the latest version. + public static let updateRequiredDescription = DiscoveryLocalization.tr("Localizable", "UPDATE_REQUIRED_DESCRIPTION", fallback: "This version of the OpenEdX app is out-of-date. To continue learning and get the latest features and fixes, please upgrade to the latest version.") + /// App Update Required + public static let updateRequiredTitle = DiscoveryLocalization.tr("Localizable", "UPDATE_REQUIRED_TITLE", fallback: "App Update Required") + /// Why do I need to update? + public static let updateWhyNeed = DiscoveryLocalization.tr("Localizable", "UPDATE_WHY_NEED", fallback: "Why do I need to update?") + public enum Alert { + /// You are now leaving the app and opening a browser + public static let leavingAppMessage = DiscoveryLocalization.tr("Localizable", "ALERT.LEAVING_APP_MESSAGE", fallback: "You are now leaving the app and opening a browser") + /// Leaving the app + public static let leavingAppTitle = DiscoveryLocalization.tr("Localizable", "ALERT.LEAVING_APP_TITLE", fallback: "Leaving the app") + } + public enum Details { + /// Enroll now + public static let enrollNow = DiscoveryLocalization.tr("Localizable", "DETAILS.ENROLL_NOW", fallback: "Enroll now") + /// You cannot enroll in this course because the enrollment date is over. + public static let enrollmentDateIsOver = DiscoveryLocalization.tr("Localizable", "DETAILS.ENROLLMENT_DATE_IS_OVER", fallback: "You cannot enroll in this course because the enrollment date is over.") + /// Course details + public static let title = DiscoveryLocalization.tr("Localizable", "DETAILS.TITLE", fallback: "Course details") + /// View course + public static let viewCourse = DiscoveryLocalization.tr("Localizable", "DETAILS.VIEW_COURSE", fallback: "View course") + } public enum Header { /// Discover new public static let title1 = DiscoveryLocalization.tr("Localizable", "HEADER.TITLE_1", fallback: "Discover new") diff --git a/Discovery/Discovery/en.lproj/Localizable.strings b/Discovery/Discovery/en.lproj/Localizable.strings index d5502912c..09bd81912 100644 --- a/Discovery/Discovery/en.lproj/Localizable.strings +++ b/Discovery/Discovery/en.lproj/Localizable.strings @@ -13,3 +13,24 @@ "SEARCH.TITLE" = "Search results"; "SEARCH.EMPTY_DESCRIPTION" = "Start typing to find the course"; + +"UPDATE_REQUIRED_TITLE" = "App Update Required"; +"UPDATE_REQUIRED_DESCRIPTION" = "This version of the OpenEdX app is out-of-date. To continue learning and get the latest features and fixes, please upgrade to the latest version."; +"UPDATE_WHY_NEED" = "Why do I need to update?"; +"UPDATE_DEPRECATED_APP" = "Deprecated App Version"; +"UPDATE_BUTTON" = "Update"; +"UPDATE_ACCOUNT_SETTINGS" = "Account Settings"; + +"UPDATE_NEEDED_TITLE" = "App Update"; +"UPDATE_NEEDED_DESCRIPTION" = "We recommend that you update to the latest version. Upgrade now to receive the latest features and fixes."; +"UPDATE_NEEDED_NOT_NOW" = "Not Now"; + +"UPDATE_NEW_AVALIABLE" = "New update available! Upgrade now to receive the latest features and fixes"; + +"ALERT.LEAVING_APP_TITLE" = "Leaving the app"; +"ALERT.LEAVING_APP_MESSAGE" = "You are now leaving the app and opening a browser"; + +"DETAILS.TITLE" = "Course details"; +"DETAILS.VIEW_COURSE" = "View course"; +"DETAILS.ENROLL_NOW" = "Enroll now"; +"DETAILS.ENROLLMENT_DATE_IS_OVER" = "You cannot enroll in this course because the enrollment date is over."; diff --git a/Discovery/Discovery/uk.lproj/Localizable.strings b/Discovery/Discovery/uk.lproj/Localizable.strings index 8f5218a53..28f832e27 100644 --- a/Discovery/Discovery/uk.lproj/Localizable.strings +++ b/Discovery/Discovery/uk.lproj/Localizable.strings @@ -13,3 +13,24 @@ "SEARCH.TITLE" = "Результати пошуку"; "SEARCH.EMPTY_DESCRIPTION" = "Почніть вводити текст, щоб знайти курс"; + +"UPDATE_REQUIRED_TITLE" = "Потрібне оновлення додатка"; +"UPDATE_REQUIRED_DESCRIPTION" = "Ця версія додатка OpenEdX застаріла. Щоб продовжити навчання та отримати останні функції та виправлення, оновіться до останньої версії."; +"UPDATE_WHY_NEED" = "Чому я маю оновити програму?"; +"UPDATE_DEPRECATED_APP" = "Застаріла версія додатка"; +"UPDATE_BUTTON" = "Оновити"; +"UPDATE_ACCOUNT_SETTINGS" = "Налаштування"; + +"UPDATE_NEEDED_TITLE" = "Оновлення додатку"; +"UPDATE_NEEDED_DESCRIPTION" = "Ми рекомендуємо вам оновити додаток до останньої версії. Оновіть зараз, щоб отримати нові функції та виправлення."; +"UPDATE_NEEDED_NOT_NOW" = "Не зараз"; + +"UPDATE_NEW_AVALIABLE" = "Доступне нове оновлення! Оновіть зараз, щоб отримати найновіші функції та виправлення"; + +"ALERT.LEAVING_APP_TITLE" = "Leaving the app"; +"ALERT.LEAVING_APP_MESSAGE" = "You are now leaving the app and opening a browser"; + +"DETAILS.TITLE" = "Деталі курсу"; +"DETAILS.VIEW_COURSE" = "Переглянути курс"; +"DETAILS.ENROLL_NOW" = "Зареєструватися"; +"DETAILS.ENROLLMENT_DATE_IS_OVER" = "Ви не можете зареєструватися на цей курс, оскільки дата реєстрації закінчилася."; diff --git a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift index 1eb44a322..3522e8691 100644 --- a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift +++ b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.8.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func login(externalToken: String, backend: String) throws -> User { + addInvocation(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) + let perform = methodPerformValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) as? (String, String) -> Void + perform?(`externalToken`, `backend`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) return Matcher.ComparisonResult(results) + case (.m_login__externalToken_externalTokenbackend_backend(let lhsExternaltoken, let lhsBackend), .m_login__externalToken_externalTokenbackend_backend(let rhsExternaltoken, let rhsBackend)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsExternaltoken, rhs: rhsExternaltoken, with: matcher), lhsExternaltoken, rhsExternaltoken, "externalToken")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) @@ -490,22 +539,28 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`controllers`) } - open func showMainScreen() { - addInvocation(.m_showMainScreen) - let perform = methodPerformValue(.m_showMainScreen) as? () -> Void - perform?() + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } - open func showLoginScreen() { - addInvocation(.m_showLoginScreen) - let perform = methodPerformValue(.m_showLoginScreen) as? () -> Void + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void perform?() } - open func showRegisterScreen() { - addInvocation(.m_showRegisterScreen) - let perform = methodPerformValue(.m_showRegisterScreen) as? () -> Void - perform?() + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } open func showForgotPasswordScreen() { @@ -514,6 +569,18 @@ open class BaseRouterMock: BaseRouter, Mock { perform?() } + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -545,10 +612,13 @@ open class BaseRouterMock: BaseRouter, Mock { case m_backWithFade case m_dismiss__animated_animated(Parameter) case m_removeLastView__controllers_controllers(Parameter) - case m_showMainScreen - case m_showLoginScreen - case m_showRegisterScreen + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) @@ -578,14 +648,37 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) return Matcher.ComparisonResult(results) - case (.m_showMainScreen, .m_showMainScreen): return .match + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showStartupScreen, .m_showStartupScreen): return .match - case (.m_showLoginScreen, .m_showLoginScreen): return .match + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) - case (.m_showRegisterScreen, .m_showRegisterScreen): return .match + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -630,10 +723,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return 0 case let .m_dismiss__animated_animated(p0): return p0.intValue case let .m_removeLastView__controllers_controllers(p0): return p0.intValue - case .m_showMainScreen: return 0 - case .m_showLoginScreen: return 0 - case .m_showRegisterScreen: return 0 + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue @@ -647,10 +743,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return ".backWithFade()" case .m_dismiss__animated_animated: return ".dismiss(animated:)" case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" - case .m_showMainScreen: return ".showMainScreen()" - case .m_showLoginScreen: return ".showLoginScreen()" - case .m_showRegisterScreen: return ".showRegisterScreen()" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" @@ -678,10 +777,13 @@ open class BaseRouterMock: BaseRouter, Mock { public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} - public static func showMainScreen() -> Verify { return Verify(method: .m_showMainScreen)} - public static func showLoginScreen() -> Verify { return Verify(method: .m_showLoginScreen)} - public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} @@ -707,18 +809,27 @@ open class BaseRouterMock: BaseRouter, Mock { public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) } - public static func showMainScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showMainScreen, performs: perform) + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } - public static func showLoginScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showLoginScreen, performs: perform) + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) } - public static func showRegisterScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showRegisterScreen, performs: perform) + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showForgotPasswordScreen, performs: perform) } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1064,11 +1175,32 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { perform?(`courseID`, `courseName`) } + open func viewCourseClicked(courseId: String, courseName: String) { + addInvocation(.m_viewCourseClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_viewCourseClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func courseEnrollClicked(courseId: String, courseName: String) { + addInvocation(.m_courseEnrollClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseEnrollClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + + open func courseEnrollSuccess(courseId: String, courseName: String) { + addInvocation(.m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + fileprivate enum MethodType { case m_discoverySearchBarClicked case m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(Parameter, Parameter) case m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(Parameter, Parameter) + case m_viewCourseClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_courseEnrollClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1085,6 +1217,24 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) return Matcher.ComparisonResult(results) + + case (.m_viewCourseClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_viewCourseClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_courseEnrollClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + + case (.m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -1094,6 +1244,9 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { case .m_discoverySearchBarClicked: return 0 case let .m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(p0, p1): return p0.intValue + p1.intValue case let .m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_viewCourseClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { @@ -1101,6 +1254,9 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { case .m_discoverySearchBarClicked: return ".discoverySearchBarClicked()" case .m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount: return ".discoveryCoursesSearch(label:coursesCount:)" case .m_discoveryCourseClicked__courseID_courseIDcourseName_courseName: return ".discoveryCourseClicked(courseID:courseName:)" + case .m_viewCourseClicked__courseId_courseIdcourseName_courseName: return ".viewCourseClicked(courseId:courseName:)" + case .m_courseEnrollClicked__courseId_courseIdcourseName_courseName: return ".courseEnrollClicked(courseId:courseName:)" + case .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName: return ".courseEnrollSuccess(courseId:courseName:)" } } } @@ -1122,6 +1278,9 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { public static func discoverySearchBarClicked() -> Verify { return Verify(method: .m_discoverySearchBarClicked)} public static func discoveryCoursesSearch(label: Parameter, coursesCount: Parameter) -> Verify { return Verify(method: .m_discoveryCoursesSearch__label_labelcoursesCount_coursesCount(`label`, `coursesCount`))} public static func discoveryCourseClicked(courseID: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(`courseID`, `courseName`))} + public static func viewCourseClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_viewCourseClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func courseEnrollClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func courseEnrollSuccess(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} } public struct Perform { @@ -1137,6 +1296,15 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { public static func discoveryCourseClicked(courseID: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_discoveryCourseClicked__courseID_courseIDcourseName_courseName(`courseID`, `courseName`), performs: perform) } + public static func viewCourseClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_viewCourseClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func courseEnrollClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseEnrollClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } + public static func courseEnrollSuccess(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } } public func given(_ method: Given) { @@ -1304,11 +1472,62 @@ open class DiscoveryInteractorProtocolMock: DiscoveryInteractorProtocol, Mock { return __value } + open func getLoadedCourseDetails(courseID: String) throws -> CourseDetails { + addInvocation(.m_getLoadedCourseDetails__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_getLoadedCourseDetails__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: CourseDetails + do { + __value = try methodReturnValue(.m_getLoadedCourseDetails__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getLoadedCourseDetails(courseID: String). Use given") + Failure("Stub return value not specified for getLoadedCourseDetails(courseID: String). Use given") + } catch { + throw error + } + return __value + } + + open func getCourseDetails(courseID: String) throws -> CourseDetails { + addInvocation(.m_getCourseDetails__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_getCourseDetails__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: CourseDetails + do { + __value = try methodReturnValue(.m_getCourseDetails__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getCourseDetails(courseID: String). Use given") + Failure("Stub return value not specified for getCourseDetails(courseID: String). Use given") + } catch { + throw error + } + return __value + } + + open func enrollToCourse(courseID: String) throws -> Bool { + addInvocation(.m_enrollToCourse__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_enrollToCourse__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: Bool + do { + __value = try methodReturnValue(.m_enrollToCourse__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for enrollToCourse(courseID: String). Use given") + Failure("Stub return value not specified for enrollToCourse(courseID: String). Use given") + } catch { + throw error + } + return __value + } + fileprivate enum MethodType { case m_discovery__page_page(Parameter) case m_discoveryOffline case m_search__page_pagesearchTerm_searchTerm(Parameter, Parameter) + case m_getLoadedCourseDetails__courseID_courseID(Parameter) + case m_getCourseDetails__courseID_courseID(Parameter) + case m_enrollToCourse__courseID_courseID(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1324,6 +1543,21 @@ open class DiscoveryInteractorProtocolMock: DiscoveryInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPage, rhs: rhsPage, with: matcher), lhsPage, rhsPage, "page")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchterm, rhs: rhsSearchterm, with: matcher), lhsSearchterm, rhsSearchterm, "searchTerm")) return Matcher.ComparisonResult(results) + + case (.m_getLoadedCourseDetails__courseID_courseID(let lhsCourseid), .m_getLoadedCourseDetails__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_getCourseDetails__courseID_courseID(let lhsCourseid), .m_getCourseDetails__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_enrollToCourse__courseID_courseID(let lhsCourseid), .m_enrollToCourse__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -1333,6 +1567,9 @@ open class DiscoveryInteractorProtocolMock: DiscoveryInteractorProtocol, Mock { case let .m_discovery__page_page(p0): return p0.intValue case .m_discoveryOffline: return 0 case let .m_search__page_pagesearchTerm_searchTerm(p0, p1): return p0.intValue + p1.intValue + case let .m_getLoadedCourseDetails__courseID_courseID(p0): return p0.intValue + case let .m_getCourseDetails__courseID_courseID(p0): return p0.intValue + case let .m_enrollToCourse__courseID_courseID(p0): return p0.intValue } } func assertionName() -> String { @@ -1340,6 +1577,9 @@ open class DiscoveryInteractorProtocolMock: DiscoveryInteractorProtocol, Mock { case .m_discovery__page_page: return ".discovery(page:)" case .m_discoveryOffline: return ".discoveryOffline()" case .m_search__page_pagesearchTerm_searchTerm: return ".search(page:searchTerm:)" + case .m_getLoadedCourseDetails__courseID_courseID: return ".getLoadedCourseDetails(courseID:)" + case .m_getCourseDetails__courseID_courseID: return ".getCourseDetails(courseID:)" + case .m_enrollToCourse__courseID_courseID: return ".enrollToCourse(courseID:)" } } } @@ -1362,6 +1602,15 @@ open class DiscoveryInteractorProtocolMock: DiscoveryInteractorProtocol, Mock { public static func search(page: Parameter, searchTerm: Parameter, willReturn: [CourseItem]...) -> MethodStub { return Given(method: .m_search__page_pagesearchTerm_searchTerm(`page`, `searchTerm`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func getLoadedCourseDetails(courseID: Parameter, willReturn: CourseDetails...) -> MethodStub { + return Given(method: .m_getLoadedCourseDetails__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getCourseDetails(courseID: Parameter, willReturn: CourseDetails...) -> MethodStub { + return Given(method: .m_getCourseDetails__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func enrollToCourse(courseID: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_enrollToCourse__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func discovery(page: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_discovery__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -1392,6 +1641,36 @@ open class DiscoveryInteractorProtocolMock: DiscoveryInteractorProtocol, Mock { willProduce(stubber) return given } + public static func getLoadedCourseDetails(courseID: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getLoadedCourseDetails__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getLoadedCourseDetails(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getLoadedCourseDetails__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (CourseDetails).self) + willProduce(stubber) + return given + } + public static func getCourseDetails(courseID: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getCourseDetails__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getCourseDetails(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getCourseDetails__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (CourseDetails).self) + willProduce(stubber) + return given + } + public static func enrollToCourse(courseID: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_enrollToCourse__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func enrollToCourse(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_enrollToCourse__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } } public struct Verify { @@ -1400,6 +1679,9 @@ open class DiscoveryInteractorProtocolMock: DiscoveryInteractorProtocol, Mock { public static func discovery(page: Parameter) -> Verify { return Verify(method: .m_discovery__page_page(`page`))} public static func discoveryOffline() -> Verify { return Verify(method: .m_discoveryOffline)} public static func search(page: Parameter, searchTerm: Parameter) -> Verify { return Verify(method: .m_search__page_pagesearchTerm_searchTerm(`page`, `searchTerm`))} + public static func getLoadedCourseDetails(courseID: Parameter) -> Verify { return Verify(method: .m_getLoadedCourseDetails__courseID_courseID(`courseID`))} + public static func getCourseDetails(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseDetails__courseID_courseID(`courseID`))} + public static func enrollToCourse(courseID: Parameter) -> Verify { return Verify(method: .m_enrollToCourse__courseID_courseID(`courseID`))} } public struct Perform { @@ -1415,6 +1697,15 @@ open class DiscoveryInteractorProtocolMock: DiscoveryInteractorProtocol, Mock { public static func search(page: Parameter, searchTerm: Parameter, perform: @escaping (Int, String) -> Void) -> Perform { return Perform(method: .m_search__page_pagesearchTerm_searchTerm(`page`, `searchTerm`), performs: perform) } + public static func getLoadedCourseDetails(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getLoadedCourseDetails__courseID_courseID(`courseID`), performs: perform) + } + public static func getCourseDetails(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getCourseDetails__courseID_courseID(`courseID`), performs: perform) + } + public static func enrollToCourse(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_enrollToCourse__courseID_courseID(`courseID`), performs: perform) + } } public func given(_ method: Given) { @@ -1530,6 +1821,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var currentDownloadTask: DownloadDataTask? { + get { invocations.append(.p_currentDownloadTask_get); return __p_currentDownloadTask ?? optionalGivenGetterValue(.p_currentDownloadTask_get, "DownloadManagerProtocolMock - stub value for currentDownloadTask was not defined") } + } + private var __p_currentDownloadTask: (DownloadDataTask)? + @@ -1548,29 +1844,44 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func eventPublisher() -> AnyPublisher { + addInvocation(.m_eventPublisher) + let perform = methodPerformValue(.m_eventPublisher) as? () -> Void + perform?() + var __value: AnyPublisher do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void - } catch MockError.notStubed { - // do nothing + __value = try methodReturnValue(.m_eventPublisher).casted() } catch { - throw error + onFatalFailure("Stub return value not specified for eventPublisher(). Use given") + Failure("Stub return value not specified for eventPublisher(). Use given") } + return __value } - open func getDownloadsForCourse(_ courseId: String) -> [DownloadData] { - addInvocation(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) - let perform = methodPerformValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + open func getDownloadTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasks) + let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasks(). Use given") + Failure("Stub return value not specified for getDownloadTasks(). Use given") + } + return __value + } + + open func getDownloadTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void perform?(`courseId`) - var __value: [DownloadData] + var __value: [DownloadDataTask] do { - __value = try methodReturnValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))).casted() + __value = try methodReturnValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))).casted() } catch { - onFatalFailure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") - Failure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") + onFatalFailure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") } return __value } @@ -1588,12 +1899,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() + open func cancelDownloading(task: DownloadDataTask) throws { + addInvocation(.m_cancelDownloading__task_task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_cancelDownloading__task_task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + _ = try methodReturnValue(.m_cancelDownloading__task_task(Parameter.value(`task`))).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -1601,10 +1912,17 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func pauseDownloading() { - addInvocation(.m_pauseDownloading) - let perform = methodPerformValue(.m_pauseDownloading) as? () -> Void - perform?() + open func cancelDownloading(courseId: String) throws { + addInvocation(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } } open func deleteFile(blocks: [CourseBlock]) { @@ -1632,28 +1950,72 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + addInvocation(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + Failure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + } + return __value + } + + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + fileprivate enum MethodType { case m_publisher - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_getDownloadsForCourse__courseId(Parameter) + case m_eventPublisher + case m_getDownloadTasks + case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) - case m_resumeDownloading - case m_pauseDownloading + case m_cancelDownloading__task_task(Parameter) + case m_cancelDownloading__courseId_courseId(Parameter) case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_resumeDownloading + case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { case (.m_publisher, .m_publisher): return .match - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_eventPublisher, .m_eventPublisher): return .match + + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match - case (.m_getDownloadsForCourse__courseId(let lhsCourseid), .m_getDownloadsForCourse__courseId(let rhsCourseid)): + case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) return Matcher.ComparisonResult(results) @@ -1664,9 +2026,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.m_cancelDownloading__task_task(let lhsTask), .m_cancelDownloading__task_task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "task")) + return Matcher.ComparisonResult(results) - case (.m_pauseDownloading, .m_pauseDownloading): return .match + case (.m_cancelDownloading__courseId_courseId(let lhsCourseid), .m_cancelDownloading__courseId_courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + return Matcher.ComparisonResult(results) case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] @@ -1679,6 +2047,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } } @@ -1686,27 +2067,37 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { func intValue() -> Int { switch self { case .m_publisher: return 0 - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_getDownloadsForCourse__courseId(p0): return p0.intValue + case .m_eventPublisher: return 0 + case .m_getDownloadTasks: return 0 + case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue - case .m_resumeDownloading: return 0 - case .m_pauseDownloading: return 0 + case let .m_cancelDownloading__task_task(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_resumeDownloading: return 0 + case .p_currentDownloadTask_get: return 0 } } func assertionName() -> String { switch self { case .m_publisher: return ".publisher()" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_getDownloadsForCourse__courseId: return ".getDownloadsForCourse(_:)" + case .m_eventPublisher: return ".eventPublisher()" + case .m_getDownloadTasks: return ".getDownloadTasks()" + case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" - case .m_resumeDownloading: return ".resumeDownloading()" - case .m_pauseDownloading: return ".pauseDownloading()" + case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" + case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_resumeDownloading: return ".resumeDownloading()" + case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } } @@ -1719,16 +2110,28 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { super.init(products) } + public static func currentDownloadTask(getter defaultValue: DownloadDataTask?...) -> PropertyStub { + return Given(method: .p_currentDownloadTask_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willReturn: AnyPublisher...) -> MethodStub { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getDownloadsForCourse(_ courseId: Parameter, willReturn: [DownloadData]...) -> MethodStub { - return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func eventPublisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { let willReturn: [AnyPublisher] = [] let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -1736,10 +2139,24 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func getDownloadsForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadData]>) -> Void) -> MethodStub { - let willReturn: [[DownloadData]] = [] - let given: Given = { return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([DownloadData]).self) + public static func eventPublisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func getDownloadTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) willProduce(stubber) return given } @@ -1750,13 +2167,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) willProduce(stubber) return given } @@ -1770,6 +2184,36 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func cancelDownloading(task: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(task: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func resumeDownloading(willThrow: Error...) -> MethodStub { return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) } @@ -1786,14 +2230,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { fileprivate var method: MethodType public static func publisher() -> Verify { return Verify(method: .m_publisher)} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func getDownloadsForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadsForCourse__courseId(`courseId`))} + public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} + public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} - public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} - public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} + public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} + public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } public struct Perform { @@ -1803,20 +2252,23 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_publisher, performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func eventPublisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_eventPublisher, performs: perform) } - public static func getDownloadsForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getDownloadsForCourse__courseId(`courseId`), performs: perform) + public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadTasks, performs: perform) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadTasksForCourse__courseId(`courseId`), performs: perform) } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, perform: @escaping (String, [CourseBlock]) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + public static func cancelDownloading(task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__task_task(`task`), performs: perform) } - public static func pauseDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_pauseDownloading, performs: perform) + public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) @@ -1827,6 +2279,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) + } + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) + } } public func given(_ method: Given) { diff --git a/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/CourseDetailsViewModelTests.swift similarity index 83% rename from Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift rename to Discovery/DiscoveryTests/Presentation/CourseDetailsViewModelTests.swift index 108fc104b..ba30a66ce 100644 --- a/Course/CourseTests/Presentation/Details/CourseDetailsViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/CourseDetailsViewModelTests.swift @@ -8,16 +8,16 @@ import SwiftyMocky import XCTest @testable import Core -@testable import Course +@testable import Discovery import Alamofire import SwiftUI final class CourseDetailsViewModelTests: XCTestCase { func testGetCourseDetailSuccess() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let analytics = CourseAnalyticsMock() + let interactor = DiscoveryInteractorProtocolMock() + let router = DiscoveryRouterMock() + let analytics = DiscoveryAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -29,7 +29,8 @@ final class CourseDetailsViewModelTests: XCTestCase { analytics: analytics, config: config, cssInjector: cssInjector, - connectivity: connectivity) + connectivity: connectivity, + storage: CoreStorageMock()) let courseDetails = CourseDetails( courseID: "123", @@ -60,9 +61,9 @@ final class CourseDetailsViewModelTests: XCTestCase { } func testGetCourseDetailSuccessOffline() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let analytics = CourseAnalyticsMock() + let interactor = DiscoveryInteractorProtocolMock() + let router = DiscoveryRouterMock() + let analytics = DiscoveryAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -74,7 +75,8 @@ final class CourseDetailsViewModelTests: XCTestCase { analytics: analytics, config: config, cssInjector: cssInjector, - connectivity: connectivity) + connectivity: connectivity, + storage: CoreStorageMock()) let courseDetails = CourseDetails( courseID: "123", @@ -91,12 +93,12 @@ final class CourseDetailsViewModelTests: XCTestCase { courseVideoURL: nil ) - Given(interactor, .getCourseDetailsOffline(courseID: "123", + Given(interactor, .getLoadedCourseDetails(courseID: "123", willReturn: courseDetails)) await viewModel.getCourseDetail(courseID: "123") - Verify(interactor, 1, .getCourseDetailsOffline(courseID: .any)) + Verify(interactor, 1, .getLoadedCourseDetails(courseID: .any)) XCTAssertFalse(viewModel.isShowProgress) XCTAssertNil(viewModel.errorMessage) @@ -104,9 +106,9 @@ final class CourseDetailsViewModelTests: XCTestCase { } func testGetCourseDetailNoInternetError() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let analytics = CourseAnalyticsMock() + let interactor = DiscoveryInteractorProtocolMock() + let router = DiscoveryRouterMock() + let analytics = DiscoveryAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -118,7 +120,8 @@ final class CourseDetailsViewModelTests: XCTestCase { analytics: analytics, config: config, cssInjector: cssInjector, - connectivity: connectivity) + connectivity: connectivity, + storage: CoreStorageMock()) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -135,9 +138,9 @@ final class CourseDetailsViewModelTests: XCTestCase { } func testGetCourseDetailNoCacheError() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let analytics = CourseAnalyticsMock() + let interactor = DiscoveryInteractorProtocolMock() + let router = DiscoveryRouterMock() + let analytics = DiscoveryAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -149,7 +152,8 @@ final class CourseDetailsViewModelTests: XCTestCase { analytics: analytics, config: config, cssInjector: cssInjector, - connectivity: connectivity) + connectivity: connectivity, + storage: CoreStorageMock()) Given(interactor, .getCourseDetails(courseID: "123", willThrow: NoCachedDataError())) @@ -164,9 +168,9 @@ final class CourseDetailsViewModelTests: XCTestCase { } func testGetCourseDetailUnknownError() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let analytics = CourseAnalyticsMock() + let interactor = DiscoveryInteractorProtocolMock() + let router = DiscoveryRouterMock() + let analytics = DiscoveryAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -178,7 +182,8 @@ final class CourseDetailsViewModelTests: XCTestCase { analytics: analytics, config: config, cssInjector: cssInjector, - connectivity: connectivity) + connectivity: connectivity, + storage: CoreStorageMock()) Given(interactor, .getCourseDetails(courseID: "123", willThrow: NSError())) @@ -193,9 +198,9 @@ final class CourseDetailsViewModelTests: XCTestCase { } func testEnrollToCourseSuccess() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let analytics = CourseAnalyticsMock() + let interactor = DiscoveryInteractorProtocolMock() + let router = DiscoveryRouterMock() + let analytics = DiscoveryAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -207,7 +212,8 @@ final class CourseDetailsViewModelTests: XCTestCase { analytics: analytics, config: config, cssInjector: cssInjector, - connectivity: connectivity) + connectivity: connectivity, + storage: CoreStorageMock()) Given(interactor, .enrollToCourse(courseID: "123", willReturn: true)) @@ -223,9 +229,9 @@ final class CourseDetailsViewModelTests: XCTestCase { } func testEnrollToCourseUnknownError() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let analytics = CourseAnalyticsMock() + let interactor = DiscoveryInteractorProtocolMock() + let router = DiscoveryRouterMock() + let analytics = DiscoveryAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -237,7 +243,8 @@ final class CourseDetailsViewModelTests: XCTestCase { analytics: analytics, config: config, cssInjector: cssInjector, - connectivity: connectivity) + connectivity: connectivity, + storage: CoreStorageMock()) Given(interactor, .enrollToCourse(courseID: "123", willThrow: AFError.explicitlyCancelled)) @@ -253,9 +260,9 @@ final class CourseDetailsViewModelTests: XCTestCase { } func testEnrollToCourseNoInternetError() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let analytics = CourseAnalyticsMock() + let interactor = DiscoveryInteractorProtocolMock() + let router = DiscoveryRouterMock() + let analytics = DiscoveryAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -267,7 +274,8 @@ final class CourseDetailsViewModelTests: XCTestCase { analytics: analytics, config: config, cssInjector: cssInjector, - connectivity: connectivity) + connectivity: connectivity, + storage: CoreStorageMock()) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -284,9 +292,9 @@ final class CourseDetailsViewModelTests: XCTestCase { } func testEnrollToCourseNoCacheError() async throws { - let interactor = CourseInteractorProtocolMock() - let router = CourseRouterMock() - let analytics = CourseAnalyticsMock() + let interactor = DiscoveryInteractorProtocolMock() + let router = DiscoveryRouterMock() + let analytics = DiscoveryAnalyticsMock() let config = ConfigMock() let cssInjector = CSSInjectorMock() let connectivity = ConnectivityProtocolMock() @@ -298,7 +306,8 @@ final class CourseDetailsViewModelTests: XCTestCase { analytics: analytics, config: config, cssInjector: cssInjector, - connectivity: connectivity) + connectivity: connectivity, + storage: CoreStorageMock()) Given(interactor, .enrollToCourse(courseID: "123", willThrow: NoCachedDataError())) diff --git a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift index a31924505..30b6b5706 100644 --- a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift @@ -26,7 +26,12 @@ final class DiscoveryViewModelTests: XCTestCase { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() let analytics = DiscoveryAnalyticsMock() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock()) let items = [ CourseItem(name: "Test", @@ -71,8 +76,12 @@ final class DiscoveryViewModelTests: XCTestCase { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() let analytics = DiscoveryAnalyticsMock() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) - + let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock()) let items = [ CourseItem(name: "Test", org: "org", @@ -115,8 +124,12 @@ final class DiscoveryViewModelTests: XCTestCase { let interactor = DiscoveryInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DiscoveryAnalyticsMock() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) - + let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock()) let items = [ CourseItem(name: "Test", org: "org", @@ -161,7 +174,12 @@ final class DiscoveryViewModelTests: XCTestCase { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() let analytics = DiscoveryAnalyticsMock() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock()) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -180,7 +198,12 @@ final class DiscoveryViewModelTests: XCTestCase { let interactor = DiscoveryInteractorProtocolMock() let connectivity = Connectivity() let analytics = DiscoveryAnalyticsMock() - let viewModel = DiscoveryViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock()) let noInternetError = AFError.sessionInvalidated(error: NSError()) diff --git a/Discovery/Mockfile b/Discovery/Mockfile index 3940f8cf2..638dccd32 100644 --- a/Discovery/Mockfile +++ b/Discovery/Mockfile @@ -1,5 +1,5 @@ -sourceryCommand: null -sourceryTemplate: null +sourceryCommand: mint run krzysztofzablocki/Sourcery@2.1.2 sourcery +sourceryTemplate: ../MockTemplate.swifttemplate unit.tests.mock: sources: include: diff --git a/Discussion/Discussion.xcodeproj.xcworkspace/contents.xcworkspacedata b/Discussion/Discussion.xcodeproj.xcworkspace/contents.xcworkspacedata index 85b36c90c..d64d30457 100644 --- a/Discussion/Discussion.xcodeproj.xcworkspace/contents.xcworkspacedata +++ b/Discussion/Discussion.xcodeproj.xcworkspace/contents.xcworkspacedata @@ -28,4 +28,7 @@ + + diff --git a/Discussion/Discussion.xcodeproj/project.pbxproj b/Discussion/Discussion.xcodeproj/project.pbxproj index 9a659e4a7..26c1a5518 100644 --- a/Discussion/Discussion.xcodeproj/project.pbxproj +++ b/Discussion/Discussion.xcodeproj/project.pbxproj @@ -35,7 +35,6 @@ 029B78F7292526D60097ACD8 /* BaseResponsesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029B78F6292526D50097ACD8 /* BaseResponsesViewModel.swift */; }; 02BE57BC29890D4A00197812 /* DiscussionTopicsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BE57BB29890D4A00197812 /* DiscussionTopicsViewModelTests.swift */; }; 02BE57BE298910BD00197812 /* BaseResponsesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BE57BD298910BD00197812 /* BaseResponsesViewModelTests.swift */; }; - 02CF2C8E291FA76E00FC1596 /* CheckBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CF2C8D291FA76E00FC1596 /* CheckBoxView.swift */; }; 02D1266E28F73BA700C8E689 /* DiscussionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D1266D28F73BA700C8E689 /* DiscussionRepository.swift */; }; 02D1267428F75BB700C8E689 /* Data_TopicsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D1267328F75BB700C8E689 /* Data_TopicsResponse.swift */; }; 02D1267628F76F5D00C8E689 /* DiscussionTopicsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D1267528F76F5D00C8E689 /* DiscussionTopicsView.swift */; }; @@ -101,7 +100,6 @@ 029B78F6292526D50097ACD8 /* BaseResponsesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseResponsesViewModel.swift; sourceTree = ""; }; 02BE57BB29890D4A00197812 /* DiscussionTopicsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionTopicsViewModelTests.swift; sourceTree = ""; }; 02BE57BD298910BD00197812 /* BaseResponsesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseResponsesViewModelTests.swift; sourceTree = ""; }; - 02CF2C8D291FA76E00FC1596 /* CheckBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxView.swift; sourceTree = ""; }; 02D1266D28F73BA700C8E689 /* DiscussionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionRepository.swift; sourceTree = ""; }; 02D1267328F75BB700C8E689 /* Data_TopicsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_TopicsResponse.swift; sourceTree = ""; }; 02D1267528F76F5D00C8E689 /* DiscussionTopicsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionTopicsView.swift; sourceTree = ""; }; @@ -245,7 +243,6 @@ 02F28A5C28FF23BD00AFDE1B /* Comments */, 0282DA5D28F893B7003C3F07 /* Posts */, 0282DA5C28F89397003C3F07 /* DiscussionTopics */, - 02CF2C8D291FA76E00FC1596 /* CheckBoxView.swift */, 02F3BFE22925302A0051930C /* DiscussionRouter.swift */, 02F175382A4DD5AA0019CD70 /* DiscussionAnalytics.swift */, ); @@ -688,7 +685,6 @@ 02F3BFEB2926A5B50051930C /* Data_CommentsResponse.swift in Sources */, 075DBBB329267D1D00E56134 /* PostState.swift in Sources */, 023F14A9291BC02200FD0EFF /* ParentCommentView.swift in Sources */, - 02CF2C8E291FA76E00FC1596 /* CheckBoxView.swift in Sources */, 02D1267628F76F5D00C8E689 /* DiscussionTopicsView.swift in Sources */, 02F28A6028FF23F300AFDE1B /* ThreadViewModel.swift in Sources */, 0766DFBE299AA18A00EBEF6A /* DiscussionPost.swift in Sources */, diff --git a/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift b/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift index 88e8d698f..1cf9ade1e 100644 --- a/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift +++ b/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift @@ -41,7 +41,7 @@ enum DiscussionEndpoint: EndPointType { case let .getCommentResponses(commentID, _): return "/api/discussion/v1/comments/\(commentID)" case .addCommentTo: - return "/mobile_api_extensions/discussion/v1/comments/" + return "/api/discussion/v1/comments/" case let .voteThread(_, threadID): return "/api/discussion/v1/threads/\(threadID)/" case let .voteResponse(_, responseID): diff --git a/Discussion/Discussion/Data/Network/DiscussionRepository.swift b/Discussion/Discussion/Data/Network/DiscussionRepository.swift index 18530e784..ab8686ad7 100644 --- a/Discussion/Discussion/Data/Network/DiscussionRepository.swift +++ b/Discussion/Discussion/Data/Network/DiscussionRepository.swift @@ -37,10 +37,10 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { private let api: API private let appStorage: CoreStorage - private let config: Config + private let config: ConfigProtocol private let router: DiscussionRouter - public init(api: API, appStorage: CoreStorage, config: Config, router: DiscussionRouter) { + public init(api: API, appStorage: CoreStorage, config: ConfigProtocol, router: DiscussionRouter) { self.api = api self.appStorage = appStorage self.config = config @@ -181,7 +181,7 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { #if DEBUG // swiftlint:disable all public class DiscussionRepositoryMock: DiscussionRepositoryProtocol { - + var comments = [ UserComment(authorName: "Bill", authorAvatar: "", diff --git a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift index bc2572e8d..4a955aa2e 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift @@ -43,13 +43,13 @@ public class BaseResponsesViewModel { internal let interactor: DiscussionInteractorProtocol internal let router: DiscussionRouter - internal let config: Config + internal let config: ConfigProtocol internal let addPostSubject = CurrentValueSubject(nil) init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: Config + config: ConfigProtocol ) { self.interactor = interactor self.router = router diff --git a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift index f818590d2..acfb41b5a 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift @@ -8,11 +8,13 @@ import SwiftUI import Core import Kingfisher +import Theme public struct CommentCell: View { private let comment: Post private let addCommentAvailable: Bool + private var onAvatarTap: ((String) -> Void) private var onLikeTap: (() -> Void) private var onReportTap: (() -> Void) private var onCommentsTap: (() -> Void) @@ -25,6 +27,7 @@ public struct CommentCell: View { comment: Post, addCommentAvailable: Bool, leftLineEnabled: Bool = false, + onAvatarTap: @escaping (String) -> Void, onLikeTap: @escaping () -> Void, onReportTap: @escaping () -> Void, onCommentsTap: @escaping () -> Void, @@ -33,6 +36,7 @@ public struct CommentCell: View { self.comment = comment self.addCommentAvailable = addCommentAvailable self.leftLineEnabled = leftLineEnabled + self.onAvatarTap = onAvatarTap self.onLikeTap = onLikeTap self.onReportTap = onReportTap self.onCommentsTap = onCommentsTap @@ -42,11 +46,15 @@ public struct CommentCell: View { public var body: some View { VStack(alignment: .leading) { HStack { + Button(action: { + onAvatarTap(comment.authorName) + }, label: { KFImage(URL(string: comment.authorAvatar)) .onFailureImage(KFCrossPlatformImage(systemName: "person.circle")) .resizable() .frame(width: 32, height: 32) .cornerRadius(16) + }) VStack(alignment: .leading) { Text(comment.authorName) @@ -170,6 +178,7 @@ struct CommentView_Previews: PreviewProvider { comment: comment, addCommentAvailable: true, leftLineEnabled: false, + onAvatarTap: {_ in}, onLikeTap: {}, onReportTap: {}, onCommentsTap: {}, @@ -178,6 +187,7 @@ struct CommentView_Previews: PreviewProvider { comment: comment, addCommentAvailable: true, leftLineEnabled: false, + onAvatarTap: {_ in}, onLikeTap: {}, onReportTap: {}, onCommentsTap: {}, @@ -192,6 +202,7 @@ struct CommentView_Previews: PreviewProvider { comment: comment, addCommentAvailable: true, leftLineEnabled: false, + onAvatarTap: {_ in}, onLikeTap: {}, onReportTap: {}, onCommentsTap: {}, @@ -200,6 +211,7 @@ struct CommentView_Previews: PreviewProvider { comment: comment, addCommentAvailable: true, leftLineEnabled: false, + onAvatarTap: {_ in}, onLikeTap: {}, onReportTap: {}, onCommentsTap: {}, diff --git a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift index ab6aa3455..1706bb75c 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift @@ -8,11 +8,13 @@ import SwiftUI import Core import Kingfisher +import Theme public struct ParentCommentView: View { private let comments: Post private var isThread: Bool + private var onAvatarTap: ((String) -> Void) private var onLikeTap: (() -> Void) private var onReportTap: (() -> Void) private var onFollowTap: (() -> Void) @@ -22,12 +24,14 @@ public struct ParentCommentView: View { public init( comments: Post, isThread: Bool, + onAvatarTap: @escaping (String) -> Void, onLikeTap: @escaping () -> Void, onReportTap: @escaping () -> Void, onFollowTap: @escaping () -> Void ) { self.comments = comments self.isThread = isThread + self.onAvatarTap = onAvatarTap self.onLikeTap = onLikeTap self.onReportTap = onReportTap self.onFollowTap = onFollowTap @@ -36,12 +40,16 @@ public struct ParentCommentView: View { public var body: some View { VStack(alignment: .leading) { HStack { + Button(action: { + onAvatarTap(comments.authorName) + }, label: { KFImage(URL(string: comments.authorAvatar)) .onFailureImage(KFCrossPlatformImage(systemName: "person")) .resizable() .background(Color.gray) .frame(width: 48, height: 48) .cornerRadius(isThread ? 8 : 24) + }) VStack(alignment: .leading) { Text(comments.authorName) .font(Theme.Fonts.titleMedium) @@ -68,6 +76,7 @@ public struct ParentCommentView: View { .font(Theme.Fonts.titleLarge) Text(comments.postBodyHtml.hideHtmlTagsAndUrls()) .font(Theme.Fonts.bodyMedium) + .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 8) ForEach(Array(comments.postBody.extractURLs().enumerated()), id: \.offset) { _, url in if url.isImage() { @@ -156,6 +165,7 @@ struct ParentCommentView_Previews: PreviewProvider { ParentCommentView( comments: comment, isThread: true, + onAvatarTap: {_ in}, onLikeTap: {}, onReportTap: {}, onFollowTap: {} diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift index 69e666844..9ccb56e34 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift @@ -8,6 +8,7 @@ import SwiftUI import Core import Combine +import Theme public struct ResponsesView: View { @@ -52,7 +53,9 @@ public struct ResponsesView: View { if let comments = viewModel.postComments { ParentCommentView( comments: comments, - isThread: false, + isThread: false, onAvatarTap: { username in + viewModel.router.showUserDetails(username: username) + }, onLikeTap: { Task { if await viewModel.vote( @@ -93,7 +96,10 @@ public struct ResponsesView: View { ) { index, comment in CommentCell( comment: comment, - addCommentAvailable: false, leftLineEnabled: true, + addCommentAvailable: false, leftLineEnabled: true, + onAvatarTap: { username in + viewModel.router.showUserDetails(username: username) + }, onLikeTap: { Task { await viewModel.vote( @@ -157,7 +163,7 @@ public struct ResponsesView: View { } } } - ) + ).ignoresSafeArea(.all, edges: .horizontal) } } } @@ -192,6 +198,7 @@ public struct ResponsesView: View { } } } + .ignoresSafeArea(.all, edges: .horizontal) .navigationBarHidden(false) .navigationBarBackButtonHidden(false) .navigationTitle(title) diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift index 92555f692..cc7ec0820 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift @@ -19,7 +19,7 @@ public class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: Config, + config: ConfigProtocol, threadStateSubject: CurrentValueSubject ) { self.threadStateSubject = threadStateSubject diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index bdc5ae96a..2249cf192 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme public struct ThreadView: View { @@ -41,7 +42,10 @@ public struct ThreadView: View { if let comments = viewModel.postComments { ParentCommentView( comments: comments, - isThread: true, + isThread: true, + onAvatarTap: { username in + viewModel.router.showUserDetails(username: username) + }, onLikeTap: { Task { if await viewModel.vote( @@ -91,7 +95,9 @@ public struct ThreadView: View { CommentCell( comment: comment, addCommentAvailable: true, - onLikeTap: { + onAvatarTap: { username in + viewModel.router.showUserDetails(username: username) + }, onLikeTap: { Task { await viewModel.vote( id: comment.commentID, @@ -157,7 +163,7 @@ public struct ThreadView: View { } } } - ) + ).ignoresSafeArea(.all, edges: .horizontal) } } .onReceive(viewModel.addPostSubject, perform: { newComment in @@ -198,7 +204,7 @@ public struct ThreadView: View { Text(viewModel.alertMessage ?? "") .shadowCardStyle( bgColor: Theme.Colors.accentColor, - textColor: .white + textColor: Theme.Colors.white ) .padding(.top, 80) Spacer() @@ -212,6 +218,7 @@ public struct ThreadView: View { } } } + .ignoresSafeArea(.all, edges: .horizontal) .navigationBarHidden(false) .navigationBarBackButtonHidden(false) .navigationTitle(title) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index db10d8039..bc9be9fac 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -21,7 +21,7 @@ public class ThreadViewModel: BaseResponsesViewModel, ObservableObject { public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: Config, + config: ConfigProtocol, postStateSubject: CurrentValueSubject ) { self.postStateSubject = postStateSubject diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift index 64e0c487a..5bb082051 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme public struct CreateNewThreadView: View { @@ -37,7 +38,7 @@ public struct CreateNewThreadView: View { await viewModel.getTopics(courseID: courseID) } UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(Theme.Colors.accentColor) - UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected) + UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: Theme.Colors.white.uiColor()], for: .selected) } public var body: some View { diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift index 95c907300..46d12d614 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadViewModel.swift @@ -26,12 +26,12 @@ public class CreateNewThreadViewModel: ObservableObject { public let interactor: DiscussionInteractorProtocol public let router: DiscussionRouter - public let config: Config + public let config: ConfigProtocol public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: Config + config: ConfigProtocol ) { self.interactor = interactor self.router = router diff --git a/Discussion/Discussion/Presentation/DiscussionRouter.swift b/Discussion/Discussion/Presentation/DiscussionRouter.swift index f57e883b9..5cc0fed94 100644 --- a/Discussion/Discussion/Presentation/DiscussionRouter.swift +++ b/Discussion/Discussion/Presentation/DiscussionRouter.swift @@ -12,6 +12,8 @@ import Combine //sourcery: AutoMockable public protocol DiscussionRouter: BaseRouter { + func showUserDetails(username: String) + func showThreads(courseID: String, topics: Topics, title: String, type: ThreadType) func showThread(thread: UserThread, postStateSubject: CurrentValueSubject) @@ -33,6 +35,8 @@ public class DiscussionRouterMock: BaseRouterMock, DiscussionRouter { public override init() {} + public func showUserDetails(username: String) {} + public func showThreads(courseID: String, topics: Topics, title: String, type: ThreadType) {} public func showThread(thread: UserThread, postStateSubject: CurrentValueSubject) {} diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift index 24d4335da..8091016fe 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift @@ -7,12 +7,15 @@ import SwiftUI import Core +import Theme public struct DiscussionSearchTopicsView: View { + @FocusState + private var focused: Bool + @ObservedObject private var viewModel: DiscussionSearchTopicsViewModel @State private var animated: Bool = false - @State private var becomeFirstResponderRunOnce = false public init(viewModel: DiscussionSearchTopicsViewModel) { self.viewModel = viewModel @@ -44,13 +47,10 @@ public struct DiscussionSearchTopicsView: View { onEditingChanged: { editing in viewModel.isSearchActive = editing } - ) - .introspect(.textField, on: .iOS(.v14, .v15, .v16, .v17), customize: { textField in - if !becomeFirstResponderRunOnce { - textField.becomeFirstResponder() - self.becomeFirstResponderRunOnce = true + ).focused($focused) + .onAppear { + self.focused = true } - }) .foregroundColor(Theme.Colors.textPrimary) Spacer() if !viewModel.searchText.trimmingCharacters(in: .whitespaces).isEmpty { diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift index 722e1b9fd..5b9d946fa 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import Core +import Theme public struct DiscussionTopicsView: View { @@ -49,6 +50,8 @@ public struct DiscussionTopicsView: View { } .padding(.horizontal, 24) .padding(.bottom, 20) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DiscussionLocalization.Topics.search) // MARK: - Page Body VStack { diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift index 9364f4d6b..85ec229e8 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift @@ -30,13 +30,13 @@ public class DiscussionTopicsViewModel: ObservableObject { let interactor: DiscussionInteractorProtocol let router: DiscussionRouter let analytics: DiscussionAnalytics - let config: Config + let config: ConfigProtocol public init(title: String, interactor: DiscussionInteractorProtocol, router: DiscussionRouter, analytics: DiscussionAnalytics, - config: Config) { + config: ConfigProtocol) { self.title = title self.interactor = interactor self.router = router diff --git a/Discussion/Discussion/Presentation/Posts/PostsView.swift b/Discussion/Discussion/Presentation/Posts/PostsView.swift index dc7145a3a..b99b81287 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsView.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsView.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import Core +import Theme public struct PostsView: View { @@ -20,8 +21,16 @@ public struct PostsView: View { private let courseID: String private var showTopMenu: Bool - public init(courseID: String, currentBlockID: String, topics: Topics, title: String, type: ThreadType, - viewModel: PostsViewModel, router: DiscussionRouter, showTopMenu: Bool = true) { + public init( + courseID: String, + currentBlockID: String, + topics: Topics, + title: String, + type: ThreadType, + viewModel: PostsViewModel, + router: DiscussionRouter, + showTopMenu: Bool = true + ) { self.courseID = courseID self.title = title self.currentBlockID = currentBlockID @@ -114,7 +123,7 @@ public struct PostsView: View { .font(Theme.Fonts.labelLarge) .padding(6) } - .foregroundColor(.white) + .foregroundColor(Theme.Colors.white) .background( Circle() .foregroundColor(Theme.Colors.accentColor) @@ -155,24 +164,31 @@ public struct PostsView: View { .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding(.top, 12) - StyledButton(DiscussionLocalization.Posts.NoDiscussion.createbutton, - action: { - router.createNewThread(courseID: courseID, - selectedTopic: currentBlockID, - onPostCreated: { - reloadPage(onSuccess: { - withAnimation { - scroll.scrollTo(1) - } + StyledButton( + DiscussionLocalization.Posts.NoDiscussion.createbutton, + action: { + router.createNewThread(courseID: courseID, + selectedTopic: currentBlockID, + onPostCreated: { + reloadPage(onSuccess: { + withAnimation { + scroll.scrollTo(1) + } + }) }) - }) - }).frame(width: 215).padding(.top, 40) + }, + isTransparent: true) + .frame(width: 215) + .padding(.top, 40) + .colorMultiply(.accentColor) + }.padding(24) .padding(.top, 100) } } } - }.frameLimit() + }.accessibilityAction {} + .frameLimit() .animation(nil) .onRightSwipeGesture { router.back() diff --git a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift index baad91ffc..04b02c788 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift @@ -76,14 +76,14 @@ public class PostsViewModel: ObservableObject { private var threads: ThreadLists = ThreadLists(threads: []) private let interactor: DiscussionInteractorProtocol private let router: DiscussionRouter - private let config: Config + private let config: ConfigProtocol internal let postStateSubject = CurrentValueSubject(nil) private var cancellable: AnyCancellable? public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: Config + config: ConfigProtocol ) { self.interactor = interactor self.router = router diff --git a/Discussion/Discussion/SwiftGen/Strings.swift b/Discussion/Discussion/SwiftGen/Strings.swift index 61b06aecd..1ad256ce1 100644 --- a/Discussion/Discussion/SwiftGen/Strings.swift +++ b/Discussion/Discussion/SwiftGen/Strings.swift @@ -138,8 +138,8 @@ public enum DiscussionLocalization { public static let allPosts = DiscussionLocalization.tr("Localizable", "TOPICS.ALL_POSTS", fallback: "All Posts") /// Main categories public static let mainCategories = DiscussionLocalization.tr("Localizable", "TOPICS.MAIN_CATEGORIES", fallback: "Main categories") - /// Posts i'm following - public static let postImFollowing = DiscussionLocalization.tr("Localizable", "TOPICS.POST_IM_FOLLOWING", fallback: "Posts i'm following") + /// Posts I'm following + public static let postImFollowing = DiscussionLocalization.tr("Localizable", "TOPICS.POST_IM_FOLLOWING", fallback: "Posts I'm following") /// Search all posts public static let search = DiscussionLocalization.tr("Localizable", "TOPICS.SEARCH", fallback: "Search all posts") } diff --git a/Discussion/Discussion/en.lproj/Localizable.strings b/Discussion/Discussion/en.lproj/Localizable.strings index 110b0cae5..553b74123 100644 --- a/Discussion/Discussion/en.lproj/Localizable.strings +++ b/Discussion/Discussion/en.lproj/Localizable.strings @@ -10,7 +10,7 @@ "TOPICS.SEARCH" = "Search all posts"; "TOPICS.ALL_POSTS" = "All Posts"; -"TOPICS.POST_IM_FOLLOWING" = "Posts i'm following"; +"TOPICS.POST_IM_FOLLOWING" = "Posts I'm following"; "TOPICS.MAIN_CATEGORIES" = "Main categories"; "POSTS.SORT.RECENT_ACTIVITY" = "Recent Activity"; diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index bcb9ac4df..fad4812d4 100644 --- a/Discussion/DiscussionTests/DiscussionMock.generated.swift +++ b/Discussion/DiscussionTests/DiscussionMock.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.8.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func login(externalToken: String, backend: String) throws -> User { + addInvocation(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) + let perform = methodPerformValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) as? (String, String) -> Void + perform?(`externalToken`, `backend`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) return Matcher.ComparisonResult(results) + case (.m_login__externalToken_externalTokenbackend_backend(let lhsExternaltoken, let lhsBackend), .m_login__externalToken_externalTokenbackend_backend(let rhsExternaltoken, let rhsBackend)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsExternaltoken, rhs: rhsExternaltoken, with: matcher), lhsExternaltoken, rhsExternaltoken, "externalToken")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) @@ -490,22 +539,28 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`controllers`) } - open func showMainScreen() { - addInvocation(.m_showMainScreen) - let perform = methodPerformValue(.m_showMainScreen) as? () -> Void - perform?() + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } - open func showLoginScreen() { - addInvocation(.m_showLoginScreen) - let perform = methodPerformValue(.m_showLoginScreen) as? () -> Void + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void perform?() } - open func showRegisterScreen() { - addInvocation(.m_showRegisterScreen) - let perform = methodPerformValue(.m_showRegisterScreen) as? () -> Void - perform?() + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } open func showForgotPasswordScreen() { @@ -514,6 +569,18 @@ open class BaseRouterMock: BaseRouter, Mock { perform?() } + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -545,10 +612,13 @@ open class BaseRouterMock: BaseRouter, Mock { case m_backWithFade case m_dismiss__animated_animated(Parameter) case m_removeLastView__controllers_controllers(Parameter) - case m_showMainScreen - case m_showLoginScreen - case m_showRegisterScreen + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) @@ -578,14 +648,37 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) return Matcher.ComparisonResult(results) - case (.m_showMainScreen, .m_showMainScreen): return .match + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) - case (.m_showLoginScreen, .m_showLoginScreen): return .match + case (.m_showStartupScreen, .m_showStartupScreen): return .match - case (.m_showRegisterScreen, .m_showRegisterScreen): return .match + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -630,10 +723,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return 0 case let .m_dismiss__animated_animated(p0): return p0.intValue case let .m_removeLastView__controllers_controllers(p0): return p0.intValue - case .m_showMainScreen: return 0 - case .m_showLoginScreen: return 0 - case .m_showRegisterScreen: return 0 + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue @@ -647,10 +743,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return ".backWithFade()" case .m_dismiss__animated_animated: return ".dismiss(animated:)" case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" - case .m_showMainScreen: return ".showMainScreen()" - case .m_showLoginScreen: return ".showLoginScreen()" - case .m_showRegisterScreen: return ".showRegisterScreen()" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" @@ -678,10 +777,13 @@ open class BaseRouterMock: BaseRouter, Mock { public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} - public static func showMainScreen() -> Verify { return Verify(method: .m_showMainScreen)} - public static func showLoginScreen() -> Verify { return Verify(method: .m_showLoginScreen)} - public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} @@ -707,18 +809,27 @@ open class BaseRouterMock: BaseRouter, Mock { public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) } - public static func showMainScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showMainScreen, performs: perform) + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } - public static func showLoginScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showLoginScreen, performs: perform) + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) } - public static func showRegisterScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showRegisterScreen, performs: perform) + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showForgotPasswordScreen, performs: perform) } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1967,6 +2078,12 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { + open func showUserDetails(username: String) { + addInvocation(.m_showUserDetails__username_username(Parameter.value(`username`))) + let perform = methodPerformValue(.m_showUserDetails__username_username(Parameter.value(`username`))) as? (String) -> Void + perform?(`username`) + } + open func showThreads(courseID: String, topics: Topics, title: String, type: ThreadType) { addInvocation(.m_showThreads__courseID_courseIDtopics_topicstitle_titletype_type(Parameter.value(`courseID`), Parameter.value(`topics`), Parameter.value(`title`), Parameter.value(`type`))) let perform = methodPerformValue(.m_showThreads__courseID_courseIDtopics_topicstitle_titletype_type(Parameter.value(`courseID`), Parameter.value(`topics`), Parameter.value(`title`), Parameter.value(`type`))) as? (String, Topics, String, ThreadType) -> Void @@ -2027,22 +2144,28 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { perform?(`controllers`) } - open func showMainScreen() { - addInvocation(.m_showMainScreen) - let perform = methodPerformValue(.m_showMainScreen) as? () -> Void - perform?() + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } - open func showLoginScreen() { - addInvocation(.m_showLoginScreen) - let perform = methodPerformValue(.m_showLoginScreen) as? () -> Void + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void perform?() } - open func showRegisterScreen() { - addInvocation(.m_showRegisterScreen) - let perform = methodPerformValue(.m_showRegisterScreen) as? () -> Void - perform?() + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } open func showForgotPasswordScreen() { @@ -2051,6 +2174,18 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { perform?() } + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -2077,6 +2212,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { fileprivate enum MethodType { + case m_showUserDetails__username_username(Parameter) case m_showThreads__courseID_courseIDtopics_topicstitle_titletype_type(Parameter, Parameter, Parameter, Parameter) case m_showThread__thread_threadpostStateSubject_postStateSubject(Parameter, Parameter>) case m_showDiscussionsSearch__courseID_courseID(Parameter) @@ -2087,10 +2223,13 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case m_backWithFade case m_dismiss__animated_animated(Parameter) case m_removeLastView__controllers_controllers(Parameter) - case m_showMainScreen - case m_showLoginScreen - case m_showRegisterScreen + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) @@ -2098,6 +2237,11 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { + case (.m_showUserDetails__username_username(let lhsUsername), .m_showUserDetails__username_username(let rhsUsername)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUsername, rhs: rhsUsername, with: matcher), lhsUsername, rhsUsername, "username")) + return Matcher.ComparisonResult(results) + case (.m_showThreads__courseID_courseIDtopics_topicstitle_titletype_type(let lhsCourseid, let lhsTopics, let lhsTitle, let lhsType), .m_showThreads__courseID_courseIDtopics_topicstitle_titletype_type(let rhsCourseid, let rhsTopics, let rhsTitle, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) @@ -2153,14 +2297,37 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) return Matcher.ComparisonResult(results) - case (.m_showMainScreen, .m_showMainScreen): return .match + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) - case (.m_showLoginScreen, .m_showLoginScreen): return .match + case (.m_showStartupScreen, .m_showStartupScreen): return .match - case (.m_showRegisterScreen, .m_showRegisterScreen): return .match + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -2200,6 +2367,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { func intValue() -> Int { switch self { + case let .m_showUserDetails__username_username(p0): return p0.intValue case let .m_showThreads__courseID_courseIDtopics_topicstitle_titletype_type(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_showThread__thread_threadpostStateSubject_postStateSubject(p0, p1): return p0.intValue + p1.intValue case let .m_showDiscussionsSearch__courseID_courseID(p0): return p0.intValue @@ -2210,10 +2378,13 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case .m_backWithFade: return 0 case let .m_dismiss__animated_animated(p0): return p0.intValue case let .m_removeLastView__controllers_controllers(p0): return p0.intValue - case .m_showMainScreen: return 0 - case .m_showLoginScreen: return 0 - case .m_showRegisterScreen: return 0 + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue @@ -2222,6 +2393,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { } func assertionName() -> String { switch self { + case .m_showUserDetails__username_username: return ".showUserDetails(username:)" case .m_showThreads__courseID_courseIDtopics_topicstitle_titletype_type: return ".showThreads(courseID:topics:title:type:)" case .m_showThread__thread_threadpostStateSubject_postStateSubject: return ".showThread(thread:postStateSubject:)" case .m_showDiscussionsSearch__courseID_courseID: return ".showDiscussionsSearch(courseID:)" @@ -2232,10 +2404,13 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case .m_backWithFade: return ".backWithFade()" case .m_dismiss__animated_animated: return ".dismiss(animated:)" case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" - case .m_showMainScreen: return ".showMainScreen()" - case .m_showLoginScreen: return ".showLoginScreen()" - case .m_showRegisterScreen: return ".showRegisterScreen()" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" @@ -2258,6 +2433,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { public struct Verify { fileprivate var method: MethodType + public static func showUserDetails(username: Parameter) -> Verify { return Verify(method: .m_showUserDetails__username_username(`username`))} public static func showThreads(courseID: Parameter, topics: Parameter, title: Parameter, type: Parameter) -> Verify { return Verify(method: .m_showThreads__courseID_courseIDtopics_topicstitle_titletype_type(`courseID`, `topics`, `title`, `type`))} public static func showThread(thread: Parameter, postStateSubject: Parameter>) -> Verify { return Verify(method: .m_showThread__thread_threadpostStateSubject_postStateSubject(`thread`, `postStateSubject`))} public static func showDiscussionsSearch(courseID: Parameter) -> Verify { return Verify(method: .m_showDiscussionsSearch__courseID_courseID(`courseID`))} @@ -2268,10 +2444,13 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} - public static func showMainScreen() -> Verify { return Verify(method: .m_showMainScreen)} - public static func showLoginScreen() -> Verify { return Verify(method: .m_showLoginScreen)} - public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} @@ -2282,6 +2461,9 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { fileprivate var method: MethodType var performs: Any + public static func showUserDetails(username: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showUserDetails__username_username(`username`), performs: perform) + } public static func showThreads(courseID: Parameter, topics: Parameter, title: Parameter, type: Parameter, perform: @escaping (String, Topics, String, ThreadType) -> Void) -> Perform { return Perform(method: .m_showThreads__courseID_courseIDtopics_topicstitle_titletype_type(`courseID`, `topics`, `title`, `type`), performs: perform) } @@ -2312,18 +2494,27 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) } - public static func showMainScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showMainScreen, performs: perform) + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } - public static func showLoginScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showLoginScreen, performs: perform) + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) } - public static func showRegisterScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showRegisterScreen, performs: perform) + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showForgotPasswordScreen, performs: perform) } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -2451,6 +2642,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var currentDownloadTask: DownloadDataTask? { + get { invocations.append(.p_currentDownloadTask_get); return __p_currentDownloadTask ?? optionalGivenGetterValue(.p_currentDownloadTask_get, "DownloadManagerProtocolMock - stub value for currentDownloadTask was not defined") } + } + private var __p_currentDownloadTask: (DownloadDataTask)? + @@ -2469,29 +2665,44 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func eventPublisher() -> AnyPublisher { + addInvocation(.m_eventPublisher) + let perform = methodPerformValue(.m_eventPublisher) as? () -> Void + perform?() + var __value: AnyPublisher do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void - } catch MockError.notStubed { - // do nothing + __value = try methodReturnValue(.m_eventPublisher).casted() } catch { - throw error + onFatalFailure("Stub return value not specified for eventPublisher(). Use given") + Failure("Stub return value not specified for eventPublisher(). Use given") } + return __value } - open func getDownloadsForCourse(_ courseId: String) -> [DownloadData] { - addInvocation(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) - let perform = methodPerformValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + open func getDownloadTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasks) + let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasks(). Use given") + Failure("Stub return value not specified for getDownloadTasks(). Use given") + } + return __value + } + + open func getDownloadTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void perform?(`courseId`) - var __value: [DownloadData] + var __value: [DownloadDataTask] do { - __value = try methodReturnValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))).casted() + __value = try methodReturnValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))).casted() } catch { - onFatalFailure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") - Failure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") + onFatalFailure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") } return __value } @@ -2509,12 +2720,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() + open func cancelDownloading(task: DownloadDataTask) throws { + addInvocation(.m_cancelDownloading__task_task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_cancelDownloading__task_task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + _ = try methodReturnValue(.m_cancelDownloading__task_task(Parameter.value(`task`))).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -2522,10 +2733,17 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func pauseDownloading() { - addInvocation(.m_pauseDownloading) - let perform = methodPerformValue(.m_pauseDownloading) as? () -> Void - perform?() + open func cancelDownloading(courseId: String) throws { + addInvocation(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } } open func deleteFile(blocks: [CourseBlock]) { @@ -2553,28 +2771,72 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + addInvocation(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + Failure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + } + return __value + } + + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + fileprivate enum MethodType { case m_publisher - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_getDownloadsForCourse__courseId(Parameter) + case m_eventPublisher + case m_getDownloadTasks + case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) - case m_resumeDownloading - case m_pauseDownloading + case m_cancelDownloading__task_task(Parameter) + case m_cancelDownloading__courseId_courseId(Parameter) case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_resumeDownloading + case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { case (.m_publisher, .m_publisher): return .match - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_eventPublisher, .m_eventPublisher): return .match - case (.m_getDownloadsForCourse__courseId(let lhsCourseid), .m_getDownloadsForCourse__courseId(let rhsCourseid)): + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match + + case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) return Matcher.ComparisonResult(results) @@ -2585,9 +2847,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.m_cancelDownloading__task_task(let lhsTask), .m_cancelDownloading__task_task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "task")) + return Matcher.ComparisonResult(results) - case (.m_pauseDownloading, .m_pauseDownloading): return .match + case (.m_cancelDownloading__courseId_courseId(let lhsCourseid), .m_cancelDownloading__courseId_courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + return Matcher.ComparisonResult(results) case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] @@ -2600,6 +2868,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } } @@ -2607,27 +2888,37 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { func intValue() -> Int { switch self { case .m_publisher: return 0 - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_getDownloadsForCourse__courseId(p0): return p0.intValue + case .m_eventPublisher: return 0 + case .m_getDownloadTasks: return 0 + case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue - case .m_resumeDownloading: return 0 - case .m_pauseDownloading: return 0 + case let .m_cancelDownloading__task_task(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_resumeDownloading: return 0 + case .p_currentDownloadTask_get: return 0 } } func assertionName() -> String { switch self { case .m_publisher: return ".publisher()" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_getDownloadsForCourse__courseId: return ".getDownloadsForCourse(_:)" + case .m_eventPublisher: return ".eventPublisher()" + case .m_getDownloadTasks: return ".getDownloadTasks()" + case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" - case .m_resumeDownloading: return ".resumeDownloading()" - case .m_pauseDownloading: return ".pauseDownloading()" + case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" + case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_resumeDownloading: return ".resumeDownloading()" + case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } } @@ -2640,16 +2931,28 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { super.init(products) } + public static func currentDownloadTask(getter defaultValue: DownloadDataTask?...) -> PropertyStub { + return Given(method: .p_currentDownloadTask_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willReturn: AnyPublisher...) -> MethodStub { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getDownloadsForCourse(_ courseId: Parameter, willReturn: [DownloadData]...) -> MethodStub { - return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func eventPublisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { let willReturn: [AnyPublisher] = [] let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -2657,10 +2960,24 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func getDownloadsForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadData]>) -> Void) -> MethodStub { - let willReturn: [[DownloadData]] = [] - let given: Given = { return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([DownloadData]).self) + public static func eventPublisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func getDownloadTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) willProduce(stubber) return given } @@ -2671,13 +2988,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) willProduce(stubber) return given } @@ -2691,6 +3005,36 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func cancelDownloading(task: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(task: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func resumeDownloading(willThrow: Error...) -> MethodStub { return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) } @@ -2707,14 +3051,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { fileprivate var method: MethodType public static func publisher() -> Verify { return Verify(method: .m_publisher)} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func getDownloadsForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadsForCourse__courseId(`courseId`))} + public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} + public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} - public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} - public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} + public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} + public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } public struct Perform { @@ -2724,20 +3073,23 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_publisher, performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func eventPublisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_eventPublisher, performs: perform) } - public static func getDownloadsForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getDownloadsForCourse__courseId(`courseId`), performs: perform) + public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadTasks, performs: perform) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadTasksForCourse__courseId(`courseId`), performs: perform) } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, perform: @escaping (String, [CourseBlock]) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + public static func cancelDownloading(task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__task_task(`task`), performs: perform) } - public static func pauseDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_pauseDownloading, performs: perform) + public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) @@ -2748,6 +3100,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) + } + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) + } } public func given(_ method: Given) { diff --git a/Discussion/Mockfile b/Discussion/Mockfile index 4b981a270..dc4c39594 100644 --- a/Discussion/Mockfile +++ b/Discussion/Mockfile @@ -1,5 +1,5 @@ -sourceryCommand: null -sourceryTemplate: null +sourceryCommand: mint run krzysztofzablocki/Sourcery@2.1.2 sourcery +sourceryTemplate: ../MockTemplate.swifttemplate unit.tests.mock: sources: include: diff --git a/Documentation/CONFIGURATION_MANAGEMENT.md b/Documentation/CONFIGURATION_MANAGEMENT.md new file mode 100644 index 000000000..b7db2e6d9 --- /dev/null +++ b/Documentation/CONFIGURATION_MANAGEMENT.md @@ -0,0 +1,108 @@ +# Configuration Management + +This documentation provides a comprehensive solution for integrating and managing configuration files in OpenEdx iOS project. + +## Features + +- **Build Phase Script Integration:** Adds a script to the Build Phase of Xcode. It calls the Xcode build phase run script, which takes care of the virtual environment and installing dependencies and executes a Python script `process_config.py` with `$CONFIGURATION` and `scheme_mappings` argument. +- **Python Script for Configuration:** Utilizes `process_config.py` for: + - Adding essential keys to `Info.plist` (e.g., Facebook, Microsoft keys). + - Creating `GoogleServices.plist` with Firebase keys. + - Generating `config.plist` from `ios.yaml` and `shared.yaml`. + +Inside `Config.swift`, parsing and populating relevant keys and classes are done, e.g. `AgreementConfig.swift` and `FirebaseConfig.swift`. + +## Getting Started + +### Configuration Setup + +Edit a `config_settings.yaml` in the `default_config` folder. It should contain data as follows: + +```yaml +config_directory: '{path_to_config_folder}' +config_mapping: + prod: 'prod' + stage: 'stage' + dev: 'dev' +# These mappings are configurable, e.g. dev: 'prod_test' +``` + +- `config_directory` provides the path of the config directory. +- `config_mappings` provides mappings that can be utilized to map the Xcode build scheme to a defined folder within the config directory, and it will be referenced. + +### Configuration Files + +Two main configuration files are used: `ios.yaml` and `shared.yaml`, placed under the folder defined in `config_mappings`. Additionally, a `mappings.yaml` file is required in the same directory, specifying the YAML files to be processed. Its structure is as follows: + +```yaml +ios: + files: + - {file_one.yaml} + - {file_two.yaml} +``` + +- `ios.yaml` will contain config data specific to iOS, e.g., Firebase keys, Facebook keys, etc. +- `shared.yaml` will contain config data that is shared, e.g., `API_HOST_URL`, `OAUTH_CLIENT_ID`, `TOKEN_TYPE`, etc. + +## Future Support + +- To add config related to some other service, create a class, e.g. `ServiceNameConfig.swift`, to be able to populate related fields. +- Create an `extension` to `Config.swift` to be able to add the newly created service as a variable to the main Config. +- If needed, make a protocol to be referenced inside the scope of `ConfigProtocol` so that the config is available using `ConfigProtocol` service. + +Example: + +```swift +private let key = "KEY" +extension Config { + public var serviceNameConfig: ServiceNameConfig { + return ServiceNameConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} +``` + +## Note + +If Firebase Configuration is provided the updated `FirebaseCrashlytics` build phase script extracts `googleAppID` from the newly generated `GoogleService-Info.plist` and runs the Crashlytics script with the provifing id. + +## Examples of Config Files + +`ios.yaml`: + +```yaml +OAUTH_CLIENT_ID: '' + +FIREBASE: + ENABLED: true + API_KEY: "testApiKey" + BUNDLE_ID: "testBundleID" + CLIENT_ID: "testClientID" + DATABASE_URL: "https://test.database.url" + GCM_SENDER_ID: "testGCMSenderID" + GOOGLE_APP_ID: "testGoogleAppID" + PROJECT_ID: "testProjectID" + REVERSED_CLIENT_ID: "testReversedClientID" + STORAGE_BUCKET: "testStorageBucket" + ANALYTICS_SOURCE: "firebase" + +MICROSOFT: + ENABLED: true + APP_ID: "microsoftAppID" +``` + +`shared.yaml`: + +```yaml +API_HOST_URL: "https://www.example.com" +FEEDBACK_EMAIL_ADDRESS: "example@mail.com" +TOKEN_TYPE: "JWT" + +AGREEMENT_URLS: + PRIVACY_POLICY_URL: "https://www.example.com/privacy" + TOS_URL: "https://www.example.com/tos" + +# Features +WHATS_NEW_ENABLED: false +``` + +The `default_config` directory is added to the project to provide an idea of how to write config YAML files. diff --git a/Documentation/Theming_implementation.md b/Documentation/Theming_implementation.md new file mode 100644 index 000000000..c92f32be0 --- /dev/null +++ b/Documentation/Theming_implementation.md @@ -0,0 +1,114 @@ +# Theming Implementation +This documentation provides instructions on how to implement Theme assets for the OpenEdX iOS project. + +## Python dependecies +The `whitelabel.py` theming script requires the following Python dependencies to be installed: +- `pip3 install coloredlogs` +- `pip3 install pillow` +- `pip3 install pyyaml` + +## How to Run the Script +The theming script `whitelabel.py` can be run from the OpenEdX iOS root project folder with the following command: +```bash +python3 config_script/whitelabel.py --config-file=path/to/configfile/whitelabel.yaml -v +``` +where +- `config_script/whitelabel.py` is the path to the `whitelabel.py` script +- `--config-file=path/to/configfile/whitelabel.yaml` is the path to the configuration file `whitelabel.yaml` +- `-v` sets the log level (all messages if '-v' is present and errors only if is not). + +## Example of whitelabel.yaml +You can get example of `whitelabel.yaml` file by run next command: +```bash +python3 config_script/whitelabel.py --help-config-file +``` +Just copy script's output to your `whitelabel.yaml` file. + +## Config Options +The config file `whitelabel.yaml` can be created by yourself or obtained from some config repo. +This config can contain the following options: +### Folder with source assets +This is the folder where all image assets, which should be copied into the project, are placed (can be relative or absolute): +```yaml +images_import_dir: 'path/to/images/source' +``` +### Xcode Project Settings +The theming script can change the app name, version, development team and app bundle ID: +```yaml +project_config: + project_path: 'path/to/project/project.pbxproj' # path to project.pbxproj file + dev_team: '1234567890' # Apple development team ID + project_extra_targets: ['Target1', 'Target2'] # targets in the workspace other than 'OpenEdX' in which the new dev_team should be set + marketing_version: '1.0.1' # App marketing version + current_project_version: '2' # App build number + configurations: + config1: # Build Configuration name in project + app_bundle_id: "bundle.id.app.new1" # Bundle ID to be set + product_name: "Mobile App Name1" # App Name to be set + config2: # Build Configuration name in project + app_bundle_id: "bundle.id.app.new2" # Bundle ID to be set + product_name: "Mobile App Name2" # App Name to be set +``` +### Assets +The config `whitelabel.yaml` can contain a few Asset items (every added Xcode project can have its own Assets). +Every Asset item can be configured with images, colors, and app Icon Assets: +```yaml +assets: + AssetName: + images_path: 'Theme/Theme/Assets.xcassets' # path where images are placed in this Asset + colors_path: 'Theme/Theme/Assets.xcassets/Colors' # path where colors are placed in this Asset + icon_path: 'Theme/Assets.xcassets' # path where app icon is placed in this Asset + images: + image1: # Asset name + image_name: 'some_image.svg' # image to replace the existing one for image1 Asset (light/universal) + image2: # Asset name + current_path: 'SomeFolder' # Path to image2.imageset inside Assets.xcassets + image_name: 'Rectangle.png' # image to replace the existing one for image2 Asset (light/universal) + dark_image_name: 'RectangleDark.png' # image to replace the existing dark appearance for image2 Asset (dark) + colors: + LoginBackground: # color asset name in Assets + current_path: '' # optional: path to color inside colors_path + light: '#FFFFFF' + dark: '#ED5C13' + icon: + AppIcon: + current_path: '' # optional: path to icon inside icon_path + image_name: 'appIcon.jpg' # image to replace the current AppIcon - png or jpg are supported +``` + +### Font +The `whitelabel.yaml` configuration may contain the path to a font file, and an existing font in the project will be replaced with this font. +This ttf file must contain multiple ttf fonts "merged" into a single ttf file. Font types used in the application: +- regular +- medium +- semiBold +- bold + +For this function, the configuration must contain the following parameters: +```yaml +font: + font_import_file_path: 'path/to/importing/Font_file.ttf' # path to ttf font file what should be imported to project + project_font_file_path: 'path/to/font/file/in/project/font.ttf' # path to existing ttf font file in project + project_font_names_json_path: 'path/to/names/file/in project/fonts.json' # path to existing font names json-file in project + font_names: + regular: 'FontName-Regular' + medium: 'FontName-Medium' + semiBold: 'FontName-Semibold' + bold: 'FontName-Bold' +``` + +### What's New +The `whitelabel.yaml` configuration may contain the path to a What's New json file, and an existing json file in the project will be replaced with this json file. + +For this function, the configuration must contain the following parameters: +```yaml +whatsnew: + whatsnew_import_file_path: 'path/to/importing/whats_new.json' # path to what's new json file what should be imported to project + project_whatsnew_file_path: 'path/to/json/file/in/project/whats_new.json' # path to existing json-file in project +``` + + +### Log level +You can set the log level to 'DEBUG' by adding the `-v` parameter to the script running. +The default log level is 'WARN' +## diff --git a/MockTemplate.swifttemplate b/MockTemplate.swifttemplate new file mode 100644 index 000000000..95d0d857a --- /dev/null +++ b/MockTemplate.swifttemplate @@ -0,0 +1,2133 @@ +<%_ +let mockTypeName = "Mock" +func swiftLintRules(_ arguments: [String: Any]) -> [String] { + return stringArray(fromArguments: arguments, forKey: "excludedSwiftLintRules").map { rule in + return "//swiftlint:disable \(rule)" + } +} + +func projectImports(_ arguments: [String: Any]) -> [String] { + return imports(arguments) + testableImports(arguments) +} + +func imports(_ arguments: [String: Any]) -> [String] { + return stringArray(fromArguments: arguments, forKey: "import") + .map { return "import \($0)" } +} + +func testableImports(_ arguments: [String: Any]) -> [String] { + return stringArray(fromArguments: arguments, forKey: "testable") + .map { return "@testable import \($0)" } +} + +/// [Internal] Get value from dictionary +/// - Parameters: +/// - fromArguments: dictionary +/// - forKey: dictionary key +/// - Returns: array of strings, if key not found, returns empty array. +/// - Note: If sourcery arguments containts only one element, then single value is stored, otherwise array of elements. This method always gets array of elements. +func stringArray(fromArguments arguments: [String: Any], forKey key: String) -> [String] { + + if let argument = arguments[key] as? String { + return [argument] + } else if let manyArguments = arguments[key] as? [String] { + return manyArguments + } else { + return [] + } +} +_%> +// Generated with SwiftyMocky 4.2.0 +// Required Sourcery: 1.8.0 + +<%_ for rule in swiftLintRules(argument) { -%> + <%_ %><%= rule %> +<%_ } -%> + +import SwiftyMocky +import XCTest +<%# ================================================== IMPORTS -%><%_ -%> + <%_ for projectImport in projectImports(argument) { -%> + <%_ %><%= projectImport %> + <%_ } -%> + <%# ============================ IMPORTS InAPP (aggregated argument) -%><%_ -%> + <%_ if let swiftyMockyArgs = argument["swiftyMocky"] as? [String: Any] { -%> + <%_ for projectImport in projectImports(swiftyMockyArgs) { -%> + <%_ %><%= projectImport %> + <%_ } -%> + <%_ } -%> +<%_ +class Current { + static var selfType: String = "Self" + static var accessModifier: String = "open" +} +// Collision management +func areThereCollisions(between methods: [MethodWrapper]) -> Bool { + let givenSet = Set(methods.map({ $0.givenConstructorName(prefix: "") })) + guard givenSet.count == methods.count else { return true } // there would be conflicts in Given + let verifySet = Set(methods.map({ $0.verificationProxyConstructorName(prefix: "") })) + guard verifySet.count == methods.count else { return true } // there would be conflicts in Verify + return false +} + +// herlpers +func uniques(methods: [SourceryRuntime.Method]) -> [SourceryRuntime.Method] { + func returnTypeStripped(_ method: SourceryRuntime.Method) -> String { + let returnTypeRaw = "\(method.returnTypeName)" + var stripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + stripped = stripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + return stripped + } + + func areSameParams(_ p1: SourceryRuntime.MethodParameter, _ p2: SourceryRuntime.MethodParameter) -> Bool { + guard p1.argumentLabel == p2.argumentLabel else { return false } + guard p1.name == p2.name else { return false } + guard p1.argumentLabel == p2.argumentLabel else { return false } + guard p1.typeName.name == p2.typeName.name else { return false } + guard p1.actualTypeName?.name == p2.actualTypeName?.name else { return false } + return true + } + + func areSameMethods(_ m1: SourceryRuntime.Method, _ m2: SourceryRuntime.Method) -> Bool { + guard m1.name != m2.name else { return m1.returnTypeName == m2.returnTypeName } + guard m1.selectorName == m2.selectorName else { return false } + guard m1.parameters.count == m2.parameters.count else { return false } + + let p1 = m1.parameters + let p2 = m2.parameters + + for i in 0.. [SourceryRuntime.Method] in + guard !result.contains(where: { areSameMethods($0,element) }) else { return result } + return result + [element] + }) +} + +func uniquesWithoutGenericConstraints(methods: [SourceryRuntime.Method]) -> [SourceryRuntime.Method] { + func returnTypeStripped(_ method: SourceryRuntime.Method) -> String { + let returnTypeRaw = "\(method.returnTypeName)" + var stripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + stripped = stripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + return stripped + } + + func areSameParams(_ p1: SourceryRuntime.MethodParameter, _ p2: SourceryRuntime.MethodParameter) -> Bool { + guard p1.argumentLabel == p2.argumentLabel else { return false } + guard p1.name == p2.name else { return false } + guard p1.argumentLabel == p2.argumentLabel else { return false } + guard p1.typeName.name == p2.typeName.name else { return false } + guard p1.actualTypeName?.name == p2.actualTypeName?.name else { return false } + return true + } + + func areSameMethods(_ m1: SourceryRuntime.Method, _ m2: SourceryRuntime.Method) -> Bool { + guard m1.name != m2.name else { return returnTypeStripped(m1) == returnTypeStripped(m2) } + guard m1.selectorName == m2.selectorName else { return false } + guard m1.parameters.count == m2.parameters.count else { return false } + + let p1 = m1.parameters + let p2 = m2.parameters + + for i in 0.. [SourceryRuntime.Method] in + guard !result.contains(where: { areSameMethods($0,element) }) else { return result } + return result + [element] + }) +} + +func uniques(variables: [SourceryRuntime.Variable]) -> [SourceryRuntime.Variable] { + return variables.reduce([], { (result, element) -> [SourceryRuntime.Variable] in + guard !result.contains(where: { $0.name == element.name }) else { return result } + return result + [element] + }) +} + +func wrapMethod(_ method: SourceryRuntime.Method) -> MethodWrapper { + return MethodWrapper(method) +} + +func wrapSubscript(_ wrapped: SourceryRuntime.Subscript) -> SubscriptWrapper { + return SubscriptWrapper(wrapped) +} + +func justWrap(_ variable: SourceryRuntime.Variable) -> VariableWrapper { return wrapProperty(variable) } +func wrapProperty(_ variable: SourceryRuntime.Variable, _ scope: String = "") -> VariableWrapper { + return VariableWrapper(variable, scope: scope) +} + +func stubProperty(_ variable: SourceryRuntime.Variable, _ scope: String) -> String { + let wrapper = VariableWrapper(variable, scope: scope) + return "\(wrapper.prototype)\n\t\(wrapper.privatePrototype)" +} + +func propertyTypes(_ variable: SourceryRuntime.Variable) -> String { + let wrapper = VariableWrapper(variable, scope: "scope") + return "\(wrapper.propertyGet())" + (wrapper.readonly ? "" : "\n\t\t\(wrapper.propertySet())") +} + +func propertyMethodTypes(_ variable: SourceryRuntime.Variable) -> String { + let wrapper = VariableWrapper(variable, scope: "") + return "\(wrapper.propertyCaseGet())" + (wrapper.readonly ? "" : "\n\t\t\(wrapper.propertyCaseSet())") +} + +func propertyMethodTypesIntValue(_ variable: SourceryRuntime.Variable) -> String { + let wrapper = VariableWrapper(variable, scope: "") + return "\(wrapper.propertyCaseGetIntValue())" + (wrapper.readonly ? "" : "\n\t\t\t\(wrapper.propertyCaseSetIntValue())") +} + +func propertyRegister(_ variable: SourceryRuntime.Variable) { + let wrapper = VariableWrapper(variable, scope: "") + MethodWrapper.register(wrapper.propertyCaseGetName,wrapper.propertyCaseGetName,wrapper.propertyCaseGetName) + guard !wrapper.readonly else { return } + MethodWrapper.register(wrapper.propertyCaseSetName,wrapper.propertyCaseSetName,wrapper.propertyCaseGetName) +} +class Helpers { + static func split(_ string: String, byFirstOccurenceOf word: String) -> (String, String) { + guard let wordRange = string.range(of: word) else { return (string, "") } + let selfRange = string.range(of: string)! + let before = String(string[selfRange.lowerBound.. [String]? { + if let types = annotated.annotations["associatedtype"] as? [String] { + return types.reversed() + } else if let type = annotated.annotations["associatedtype"] as? String { + return [type] + } else { + return nil + } + } + static func extractWhereClause(from annotated: SourceryRuntime.Annotated) -> String? { + if let constraints = annotated.annotations["where"] as? [String] { + return " where \(constraints.reversed().joined(separator: ", "))" + } else if let constraint = annotated.annotations["where"] as? String { + return " where \(constraint)" + } else { + return nil + } + } + /// Extract all typealiases from "annotations" + static func extractTypealiases(from annotated: SourceryRuntime.Annotated) -> [String] { + if let types = annotated.annotations["typealias"] as? [String] { + return types.reversed() + } else if let type = annotated.annotations["typealias"] as? String { + return [type] + } else { + return [] + } + } + static func extractGenericsList(_ associatedTypes: [String]?) -> [String] { + return associatedTypes?.flatMap { + split($0, byFirstOccurenceOf: " where ").0.replacingOccurrences(of: " ", with: "").split(separator: ":").map(String.init).first + }.map { "\($0)" } ?? [] + } + static func extractGenericTypesModifier(_ associatedTypes: [String]?) -> String { + let all = extractGenericsList(associatedTypes) + guard !all.isEmpty else { return "" } + return "<\(all.joined(separator: ","))>" + } + static func extractGenericTypesConstraints(_ associatedTypes: [String]?) -> String { + guard let all = associatedTypes else { return "" } + let constraints = all.flatMap { t -> String? in + let splitted = split(t, byFirstOccurenceOf: " where ") + let constraint = splitted.0.replacingOccurrences(of: " ", with: "").split(separator: ":").map(String.init) + guard constraint.count == 2 else { return nil } + let adopts = constraint[1].split(separator: ",").map(String.init) + var mapped = adopts.map { "\(constraint[0]): \($0)" } + if !splitted.1.isEmpty { + mapped.append(splitted.1) + } + return mapped.joined(separator: ", ") + } + .joined(separator: ", ") + guard !constraints.isEmpty else { return "" } + return " where \(constraints)" + } + static func extractAttributes( + from attributes: [String: [SourceryRuntime.Attribute]], + filterOutStartingWith disallowedPrefixes: [String] = [] + ) -> String { + return attributes + .reduce([SourceryRuntime.Attribute]()) { $0 + $1.1 } + .map { $0.description } + .filter { !["private", "internal", "public", "open", "optional"].contains($0) } + .filter { element in + !disallowedPrefixes.contains(where: element.hasPrefix) + } + .sorted() + .joined(separator: " ") + } +} +class ParameterWrapper { + let parameter: MethodParameter + + var isVariadic = false + + var wrappedForCall: String { + let typeString = "\(type.actualTypeName ?? type)" + let isEscaping = typeString.contains("@escaping") + let isOptional = (type.actualTypeName ?? type).isOptional + if parameter.isClosure && !isEscaping && !isOptional { + return "\(nestedType).any" + } else { + return "\(nestedType).value(\(escapedName))" + } + } + var nestedType: String { + return "\(TypeWrapper(type, isVariadic).nestedParameter)" + } + var justType: String { + return "\(TypeWrapper(type, isVariadic).replacingSelf())" + } + var justPerformType: String { + return "\(TypeWrapper(type, isVariadic).replacingSelfRespectingVariadic())".replacingOccurrences(of: "!", with: "?") + } + var genericType: String { + return isVariadic ? "Parameter<[GenericAttribute]>" : "Parameter" + } + var typeErasedType: String { + return isVariadic ? "Parameter<[TypeErasedAttribute]>" : "Parameter" + } + var type: SourceryRuntime.TypeName { + return parameter.typeName + } + var name: String { + return parameter.name + } + var escapedName: String { + return "`\(parameter.name)`" + } + var comparator: String { + return "guard Parameter.compare(lhs: lhs\(parameter.name.capitalized), rhs: rhs\(parameter.name.capitalized), with: matcher) else { return false }" + } + func comparatorResult() -> String { + let lhsName = "lhs\(parameter.name.capitalized)" + let rhsName = "rhs\(parameter.name.capitalized)" + return "results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: \(lhsName), rhs: \(rhsName), with: matcher), \(lhsName), \(rhsName), \"\(labelAndName())\"))" + } + + init(_ parameter: SourceryRuntime.MethodParameter, _ variadics: [String] = []) { + self.parameter = parameter + self.isVariadic = !variadics.isEmpty && variadics.contains(parameter.name) + } + + func isGeneric(_ types: [String]) -> Bool { + return TypeWrapper(type).isGeneric(types) + } + + func wrappedForProxy(_ generics: [String], _ availability: Bool = false) -> String { + if isGeneric(generics) { + return "\(escapedName).wrapAsGeneric()" + } + if (availability) { + return "\(escapedName).typeErasedAttribute()" + } + return "\(escapedName)" + } + func wrappedForCalls(_ generics: [String], _ availability: Bool = false) -> String { + if isGeneric(generics) { + return "\(wrappedForCall).wrapAsGeneric()" + } + if (availability) { + return "\(wrappedForCall).typeErasedAttribute()" + } + return "\(wrappedForCall)" + } + + func asMethodArgument() -> String { + if parameter.argumentLabel != parameter.name { + return "\(parameter.argumentLabel ?? "_") \(parameter.name): \(parameter.typeName)" + } else { + return "\(parameter.name): \(parameter.typeName)" + } + } + func labelAndName() -> String { + let label = parameter.argumentLabel ?? "_" + return label != parameter.name ? "\(label) \(parameter.name)" : label + } + func sanitizedForEnumCaseName() -> String { + if let label = parameter.argumentLabel, label != parameter.name { + return "\(label)_\(parameter.name)".replacingOccurrences(of: "`", with: "") + } else { + return "\(parameter.name)".replacingOccurrences(of: "`", with: "") + } + } +} +class TypeWrapper { + let type: SourceryRuntime.TypeName + let isVariadic: Bool + + var vPref: String { return isVariadic ? "[" : "" } + var vSuff: String { return isVariadic ? "]" : "" } + + var unwrapped: String { + return type.unwrappedTypeName + } + var unwrappedReplacingSelf: String { + return replacingSelf(unwrap: true) + } + var stripped: String { + if type.isImplicitlyUnwrappedOptional { + return "\(vPref)\(unwrappedReplacingSelf)?\(vSuff)" + } else if type.isOptional { + return "\(vPref)\(unwrappedReplacingSelf)?\(vSuff)" + } else { + return "\(vPref)\(unwrappedReplacingSelf)\(vSuff)" + } + } + var nestedParameter: String { + if type.isImplicitlyUnwrappedOptional { + return "Parameter<\(vPref)\(unwrappedReplacingSelf)?\(vSuff)>" + } else if type.isOptional { + return "Parameter<\(vPref)\(unwrappedReplacingSelf)?\(vSuff)>" + } else { + return "Parameter<\(vPref)\(unwrappedReplacingSelf)\(vSuff)>" + } + } + var isSelfType: Bool { + return unwrapped == "Self" + } + func isSelfTypeRecursive() -> Bool { + if let tuple = type.tuple { + for element in tuple.elements { + guard !TypeWrapper(element.typeName).isSelfTypeRecursive() else { return true } + } + } else if let array = type.array { + return TypeWrapper(array.elementTypeName).isSelfTypeRecursive() + } else if let dictionary = type.dictionary { + guard !TypeWrapper(dictionary.valueTypeName).isSelfTypeRecursive() else { return true } + guard !TypeWrapper(dictionary.keyTypeName).isSelfTypeRecursive() else { return true } + } else if let closure = type.closure { + guard !TypeWrapper(closure.actualReturnTypeName).isSelfTypeRecursive() else { return true } + for parameter in closure.parameters { + guard !TypeWrapper(parameter.typeName).isSelfTypeRecursive() else { return true } + } + } + + return isSelfType + } + + init(_ type: SourceryRuntime.TypeName, _ isVariadic: Bool = false) { + self.type = type + self.isVariadic = isVariadic + } + + func isGeneric(_ types: [String]) -> Bool { + guard !type.isVoid else { return false } + + return isGeneric(name: unwrapped, generics: types) + } + + private func isGeneric(name: String, generics: [String]) -> Bool { + let name = "(\(name.replacingOccurrences(of: " ", with: "")))" + let modifiers = "[\\?\\!]*" + return generics.contains(where: { generic in + let wrapped = "([\\(]\(generic)\(modifiers)[\\)\\.])" + let constraint = "([<,]\(generic)\(modifiers)[>,\\.])" + let arrays = "([\\[:]\(generic)\(modifiers)[\\],\\.:])" + let tuples = "([\\(,]\(generic)\(modifiers)[,\\.\\)])" + let closures = "((\\-\\>)\(generic)\(modifiers)[,\\.\\)])" + let pattern = "\(wrapped)|\(constraint)|\(arrays)|\(tuples)|\(closures)" + guard let regex = try? NSRegularExpression(pattern: pattern) else { return false } + return regex.firstMatch(in: name, options: [], range: NSRange(location: 0, length: (name as NSString).length)) != nil + }) + } + + func replacingSelf(unwrap: Bool = false) -> String { + guard isSelfTypeRecursive() else { + return unwrap ? self.unwrapped : "\(type)" + } + + if isSelfType { + let optionality: String = { + if type.isImplicitlyUnwrappedOptional { + return "!" + } else if type.isOptional { + return "?" + } else { + return "" + } + }() + return unwrap ? Current.selfType : Current.selfType + optionality + } else if let tuple = type.tuple { + let inner = tuple.elements.map({ TypeWrapper($0.typeName).replacingSelf() }).joined(separator: ",") + let value = "(\(inner))" + return value + } else if let array = type.array { + let value = "[\(TypeWrapper(array.elementTypeName).replacingSelf())]" + return value + } else if let dictionary = type.dictionary { + let value = "[" + + "\(TypeWrapper(dictionary.valueTypeName).replacingSelf())" + + ":" + + "\(TypeWrapper(dictionary.keyTypeName).replacingSelf())" + + "]" + return value + } else if let closure = type.closure { + let returnType = TypeWrapper(closure.actualReturnTypeName).replacingSelf() + let inner = closure.parameters + .map { TypeWrapper($0.typeName).replacingSelf() } + .joined(separator: ",") + let throwing = closure.throws ? "throws " : "" + let value = "(\(inner)) \(throwing)-> \(returnType)" + return value + } else { + return (unwrap ? self.unwrapped : "\(type)") + } + } + + func replacingSelfRespectingVariadic() -> String { + return "\(vPref)\(replacingSelf())\(vSuff)" + } +} +func replacingSelf(_ value: String) -> String { + return value + // TODO: proper regex here + // default < case > + .replacingOccurrences(of: "", with: "<\(Current.selfType)>") + .replacingOccurrences(of: "", with: " \(Current.selfType)>") + .replacingOccurrences(of: ",Self>", with: ",\(Current.selfType)>") + // (Self) -> Case + .replacingOccurrences(of: "(Self)", with: "(\(Current.selfType))") + .replacingOccurrences(of: "(Self ", with: "(\(Current.selfType) ") + .replacingOccurrences(of: "(Self.", with: "(\(Current.selfType).") + .replacingOccurrences(of: "(Self,", with: "(\(Current.selfType),") + .replacingOccurrences(of: "(Self?", with: "(\(Current.selfType)?") + .replacingOccurrences(of: " Self)", with: " \(Current.selfType))") + .replacingOccurrences(of: ",Self)", with: ",\(Current.selfType))") + // literals + .replacingOccurrences(of: "[Self]", with: "[\(Current.selfType)]") + // right + .replacingOccurrences(of: "[Self ", with: "[\(Current.selfType) ") + .replacingOccurrences(of: "[Self.", with: "[\(Current.selfType).") + .replacingOccurrences(of: "[Self,", with: "[\(Current.selfType),") + .replacingOccurrences(of: "[Self:", with: "[\(Current.selfType):") + .replacingOccurrences(of: "[Self?", with: "[\(Current.selfType)?") + // left + .replacingOccurrences(of: " Self]", with: " \(Current.selfType)]") + .replacingOccurrences(of: ",Self]", with: ",\(Current.selfType)]") + .replacingOccurrences(of: ":Self]", with: ":\(Current.selfType)]") + // unknown + .replacingOccurrences(of: " Self ", with: " \(Current.selfType) ") + .replacingOccurrences(of: " Self.", with: " \(Current.selfType).") + .replacingOccurrences(of: " Self,", with: " \(Current.selfType),") + .replacingOccurrences(of: " Self:", with: " \(Current.selfType):") + .replacingOccurrences(of: " Self?", with: " \(Current.selfType)?") + .replacingOccurrences(of: ",Self ", with: ",\(Current.selfType) ") + .replacingOccurrences(of: ",Self,", with: ",\(Current.selfType),") + .replacingOccurrences(of: ",Self?", with: ",\(Current.selfType)?") +} + +class MethodWrapper { + private var noStubDefinedMessage: String { + let methodName = method.name.condenseWhitespace() + .replacingOccurrences(of: "( ", with: "(") + .replacingOccurrences(of: " )", with: ")") + return "Stub return value not specified for \(methodName). Use given" + } + private static var registered: [String: Int] = [:] + private static var suffixes: [String: Int] = [:] + private static var suffixesWithoutReturnType: [String: Int] = [:] + + let method: SourceryRuntime.Method + var accessModifier: String { + guard !method.isStatic else { return "public static" } + guard !returnsGenericConstrainedToSelf else { return "public" } + guard !parametersContainsSelf else { return "public" } + return Current.accessModifier + } + var hasAvailability: Bool { method.attributes["available"]?.isEmpty == false } + var isAsync: Bool { + self.method.annotations["async"] != nil + } + + private var registrationName: String { + var rawName = (method.isStatic ? "sm*\(method.selectorName)" : "m*\(method.selectorName)") + .replacingOccurrences(of: "_", with: "") + .replacingOccurrences(of: "(", with: "__") + .replacingOccurrences(of: ")", with: "") + + var parametersNames = method.parameters.map { "\($0.name)" } + + while let range = rawName.range(of: ":"), let name = parametersNames.first { + parametersNames.removeFirst() + rawName.replaceSubrange(range, with: "_\(name)") + } + + let trimSet = CharacterSet(charactersIn: "_") + + return rawName + .replacingOccurrences(of: ":", with: "") + .replacingOccurrences(of: "m*", with: "m_") + .replacingOccurrences(of: "___", with: "__").trimmingCharacters(in: trimSet) + } + private var uniqueName: String { + var rawName = (method.isStatic ? "sm_\(method.selectorName)" : "m_\(method.selectorName)") + var parametersNames = method.parameters.map { "\($0.name)_of_\($0.typeName.name)" } + + while let range = rawName.range(of: ":"), let name = parametersNames.first { + parametersNames.removeFirst() + rawName.replaceSubrange(range, with: "_\(name)") + } + + return rawName.trimmingCharacters(in: CharacterSet(charactersIn: "_")) + } + private var uniqueNameWithReturnType: String { + let returnTypeRaw = "\(method.returnTypeName)" + var returnTypeStripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + returnTypeStripped = returnTypeStripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + return "\(uniqueName)->\(returnTypeStripped)" + } + private var nameSuffix: String { + guard let count = MethodWrapper.registered[registrationName] else { return "" } + guard count > 1 else { return "" } + guard let index = MethodWrapper.suffixes[uniqueNameWithReturnType] else { return "" } + return "_\(index)" + } + private var methodAttributes: String { + return Helpers.extractAttributes(from: self.method.attributes, filterOutStartingWith: ["mutating", "@inlinable"]) + } + private var methodAttributesNonObjc: String { + return Helpers.extractAttributes(from: self.method.attributes, filterOutStartingWith: ["mutating", "@inlinable", "@objc"]) + } + + var prototype: String { + return "\(registrationName)\(nameSuffix)".replacingOccurrences(of: "`", with: "") + } + var parameters: [ParameterWrapper] { + return filteredParameters.map { ParameterWrapper($0, self.getVariadicParametersNames()) } + } + var filteredParameters: [MethodParameter] { + return method.parameters.filter { $0.name != "" } + } + var functionPrototype: String { + let throwing: String = { + if method.throws { + return "throws " + } else if method.rethrows { + return "rethrows " + } else { + return "" + } + }() + + let staticModifier: String = "\(accessModifier) " + let params = replacingSelf(parametersForStubSignature()) + var attributes = self.methodAttributes + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t" + var asyncModifier = self.isAsync ? "async " : "" + + if method.isInitializer { + return "\(attributes)public required \(method.name) \(asyncModifier)\(throwing)" + } else if method.returnTypeName.isVoid { + let wherePartIfNeeded: String = { + if method.returnTypeName.name.hasPrefix("Void") { + let range = method.returnTypeName.name.range(of: "Void")! + return "\(method.returnTypeName.name[range.upperBound...])" + } else { + return !method.returnTypeName.name.isEmpty ? "\(method.returnTypeName.name) " : "" + } + }() + return "\(attributes)\(staticModifier)func \(method.shortName)\(params) \(asyncModifier)\(throwing)\(wherePartIfNeeded)" + } else if returnsGenericConstrainedToSelf { + return "\(attributes)\(staticModifier)func \(method.shortName)\(params) \(asyncModifier)\(throwing)-> \(returnTypeReplacingSelf) " + } else { + return "\(attributes)\(staticModifier)func \(method.shortName)\(params) \(asyncModifier)\(throwing)-> \(method.returnTypeName.name) " + } + } + var invocation: String { + guard !method.isInitializer else { return "" } + if filteredParameters.isEmpty { + return "addInvocation(.\(prototype))" + } else { + return "addInvocation(.\(prototype)(\(parametersForMethodCall())))" + } + } + var givenValue: String { + guard !method.isInitializer else { return "" } + guard method.throws || !method.returnTypeName.isVoid else { return "" } + + let methodType = filteredParameters.isEmpty ? ".\(prototype)" : ".\(prototype)(\(parametersForMethodCall()))" + let returnType: String = returnsSelf ? "__Self__" : "\(TypeWrapper(method.returnTypeName).stripped)" + + if method.returnTypeName.isVoid { + return """ + \n\t\tdo { + \t\t _ = try methodReturnValue(\(methodType)).casted() as Void + \t\t}\(" ") + """ + } else { + let defaultValue = method.returnTypeName.isOptional ? " = nil" : "" + return """ + \n\t\tvar __value: \(returnType)\(defaultValue) + \t\tdo { + \t\t __value = try methodReturnValue(\(methodType)).casted() + \t\t}\(" ") + """ + } + } + var throwValue: String { + guard !method.isInitializer else { return "" } + guard method.throws || !method.returnTypeName.isVoid else { return "" } + let safeFailure = method.isStatic ? "" : "\t\t\tonFatalFailure(\"\(noStubDefinedMessage)\")\n" + // For Void and Returning optionals - we allow not stubbed case to happen, as we are still able to return + let noStubHandling = method.returnTypeName.isVoid || method.returnTypeName.isOptional ? "\t\t\t// do nothing" : "\(safeFailure)\t\t\tFailure(\"\(noStubDefinedMessage)\")" + guard method.throws else { + return """ + catch { + \(noStubHandling) + \t\t} + """ + } + + return """ + catch MockError.notStubed { + \(noStubHandling) + \t\t} catch { + \t\t throw error + \t\t} + """ + } + var returnValue: String { + guard !method.isInitializer else { return "" } + guard !method.returnTypeName.isVoid else { return "" } + + return "\n\t\treturn __value" + } + var equalCase: String { + guard !method.isInitializer else { return "" } + + if filteredParameters.isEmpty { + return "case (.\(prototype), .\(prototype)):" + } else { + let lhsParams = filteredParameters.map { "let lhs\($0.name.capitalized)" }.joined(separator: ", ") + let rhsParams = filteredParameters.map { "let rhs\($0.name.capitalized)" }.joined(separator: ", ") + return "case (.\(prototype)(\(lhsParams)), .\(prototype)(\(rhsParams))):" + } + } + func equalCases() -> String { + var results = self.equalCase + + guard !parameters.isEmpty else { + results += " return .match" + return results + } + + results += "\n\t\t\t\tvar results: [Matcher.ParameterComparisonResult] = []\n" + results += parameters.map { "\t\t\t\t\($0.comparatorResult())" }.joined(separator: "\n") + results += "\n\t\t\t\treturn Matcher.ComparisonResult(results)" + return results + } + var intValueCase: String { + if filteredParameters.isEmpty { + return "case .\(prototype): return 0" + } else { + let params = filteredParameters.enumerated().map { offset, _ in + return "p\(offset)" + } + let definitions = params.joined(separator: ", ") + let paramsSum = params.map({ "\($0).intValue" }).joined(separator: " + ") + return "case let .\(prototype)(\(definitions)): return \(paramsSum)" + } + } + var assertionName: String { + return "case .\(prototype): return \".\(method.selectorName)\(method.parameters.isEmpty ? "()" : "")\"" + } + + var returnsSelf: Bool { + guard !returnsGenericConstrainedToSelf else { return true } + return !method.returnTypeName.isVoid && TypeWrapper(method.returnTypeName).isSelfType + } + var returnsGenericConstrainedToSelf: Bool { + let defaultReturnType = "\(method.returnTypeName.name) " + return defaultReturnType != returnTypeReplacingSelf + } + var returnTypeReplacingSelf: String { + return replacingSelf("\(method.returnTypeName.name) ") + } + var parametersContainsSelf: Bool { + return replacingSelf(parametersForStubSignature()) != parametersForStubSignature() + } + + var replaceSelf: String { + return Current.selfType + } + + init(_ method: SourceryRuntime.Method) { + self.method = method + } + + public static func clear() -> String { + MethodWrapper.registered = [:] + MethodWrapper.suffixes = [:] + MethodWrapper.suffixesWithoutReturnType = [:] + return "" + } + + func register() { + MethodWrapper.register(registrationName,uniqueName,uniqueNameWithReturnType) + } + + static func register(_ name: String, _ uniqueName: String, _ uniqueNameWithReturnType: String) { + if let count = MethodWrapper.registered[name] { + MethodWrapper.registered[name] = count + 1 + MethodWrapper.suffixes[uniqueNameWithReturnType] = count + 1 + } else { + MethodWrapper.registered[name] = 1 + MethodWrapper.suffixes[uniqueNameWithReturnType] = 1 + } + + if let count = MethodWrapper.suffixesWithoutReturnType[uniqueName] { + MethodWrapper.suffixesWithoutReturnType[uniqueName] = count + 1 + } else { + MethodWrapper.suffixesWithoutReturnType[uniqueName] = 1 + } + } + + func returnTypeMatters() -> Bool { + let count = MethodWrapper.suffixesWithoutReturnType[uniqueName] ?? 0 + return count > 1 + } + + func wrappedInMethodType() -> Bool { + return !method.isInitializer + } + + func returningParameter(_ multiple: Bool, _ front: Bool) -> String { + guard returnTypeMatters() else { return "" } + let returning: String = "returning: \(returnTypeStripped(method, type: true))" + guard multiple else { return returning } + + return front ? ", \(returning)" : "\(returning), " + } + + // Stub + func stubBody() -> String { + let body: String = { + if method.isInitializer || !returnsSelf { + return invocation + performCall() + givenValue + throwValue + returnValue + } else { + return wrappedStubPrefix() + + "\t\t" + invocation + + performCall() + + givenValue + + throwValue + + returnValue + + wrappedStubPostfix() + } + }() + return replacingSelf(body) + } + + func wrappedStubPrefix() -> String { + guard !method.isInitializer, returnsSelf else { + return "" + } + + let throwing: String = { + if method.throws { + return "throws " + } else if method.rethrows { + return "rethrows " + } else { + return "" + } + }() + + return "func _wrapped<__Self__>() \(throwing)-> __Self__ {\n" + } + + func wrappedStubPostfix() -> String { + guard !method.isInitializer, returnsSelf else { + return "" + } + + let throwing: String = (method.throws || method.rethrows) ? "try ": "" + + return "\n\t\t}" + + "\n\t\treturn \(throwing)_wrapped()" + } + + // Method Type + func methodTypeDeclarationWithParameters() -> String { + if filteredParameters.isEmpty { + return "case \(prototype)" + } else { + return "case \(prototype)(\(parametersForMethodTypeDeclaration(availability: hasAvailability)))" + } + } + + // Given + func containsEmptyArgumentLabels() -> Bool { + return parameters.contains(where: { $0.parameter.argumentLabel == nil }) + } + + func givenReturnTypeString() -> String { + let returnTypeString: String = { + guard !returnsGenericConstrainedToSelf else { return returnTypeReplacingSelf } + guard !returnsSelf else { return replaceSelf } + return TypeWrapper(method.returnTypeName).stripped + }() + return returnTypeString + } + + func givenConstructorName(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + let (annotation, _, _) = methodInfo() + let clauseConstraints = whereClauseExpression() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(method.shortName)(willReturn: \(returnTypeString)...) -> \(prefix)MethodStub" + clauseConstraints + } else { + return "\(annotation)public static func \(method.shortName)(\(parametersForProxySignature()), willReturn: \(returnTypeString)...) -> \(prefix)MethodStub" + clauseConstraints + } + } + + func givenConstructorNameThrows(prefix: String = "") -> String { + let (annotation, _, _) = methodInfo() + let clauseConstraints = whereClauseExpression() + + let genericsArray = getGenericsConstraints(getGenericsAmongParameters(), filterSingle: false) + let generics = genericsArray.isEmpty ? "" : "<\(genericsArray.joined(separator: ", "))>" + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(method.callName)\(generics)(willThrow: Error...) -> \(prefix)MethodStub" + clauseConstraints + } else { + return "\(annotation)public static func \(method.callName)\(generics)(\(parametersForProxySignature()), willThrow: Error...) -> \(prefix)MethodStub" + clauseConstraints + } + } + + func givenConstructor(prefix: String = "") -> String { + if filteredParameters.isEmpty { + return "return \(prefix)Given(method: .\(prototype), products: willReturn.map({ StubProduct.return($0 as Any) }))" + } else { + return "return \(prefix)Given(method: .\(prototype)(\(parametersForProxyInit())), products: willReturn.map({ StubProduct.return($0 as Any) }))" + } + } + + func givenConstructorThrows(prefix: String = "") -> String { + if filteredParameters.isEmpty { + return "return \(prefix)Given(method: .\(prototype), products: willThrow.map({ StubProduct.throw($0) }))" + } else { + return "return \(prefix)Given(method: .\(prototype)(\(parametersForProxyInit())), products: willThrow.map({ StubProduct.throw($0) }))" + } + } + + // Given willProduce + func givenProduceConstructorName(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + let (annotation, _, _) = methodInfo() + let produceClosure = "(Stubber<\(returnTypeString)>) -> Void" + let clauseConstraints = whereClauseExpression() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(method.shortName)(willProduce: \(produceClosure)) -> \(prefix)MethodStub" + clauseConstraints + } else { + return "\(annotation)public static func \(method.shortName)(\(parametersForProxySignature()), willProduce: \(produceClosure)) -> \(prefix)MethodStub" + clauseConstraints + } + } + + func givenProduceConstructorNameThrows(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + let (annotation, _, _) = methodInfo() + let produceClosure = "(StubberThrows<\(returnTypeString)>) -> Void" + let clauseConstraints = whereClauseExpression() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(method.shortName)(willProduce: \(produceClosure)) -> \(prefix)MethodStub" + clauseConstraints + } else { + return "\(annotation)public static func \(method.shortName)(\(parametersForProxySignature()), willProduce: \(produceClosure)) -> \(prefix)MethodStub" + clauseConstraints + } + } + + func givenProduceConstructor(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + return """ + let willReturn: [\(returnTypeString)] = [] + \t\t\tlet given: \(prefix)Given = { \(givenConstructor(prefix: prefix)) }() + \t\t\tlet stubber = given.stub(for: (\(returnTypeString)).self) + \t\t\twillProduce(stubber) + \t\t\treturn given + """ + } + + func givenProduceConstructorThrows(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + return """ + let willThrow: [Error] = [] + \t\t\tlet given: \(prefix)Given = { \(givenConstructorThrows(prefix: prefix)) }() + \t\t\tlet stubber = given.stubThrows(for: (\(returnTypeString)).self) + \t\t\twillProduce(stubber) + \t\t\treturn given + """ + } + + // Verify + func verificationProxyConstructorName(prefix: String = "") -> String { + let (annotation, methodName, genericConstrains) = methodInfo() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(methodName)(\(returningParameter(false,true))) -> \(prefix)Verify\(genericConstrains)" + } else { + return "\(annotation)public static func \(methodName)(\(parametersForProxySignature())\(returningParameter(true,true))) -> \(prefix)Verify\(genericConstrains)" + } + } + + func verificationProxyConstructor(prefix: String = "") -> String { + if filteredParameters.isEmpty { + return "return \(prefix)Verify(method: .\(prototype))" + } else { + return "return \(prefix)Verify(method: .\(prototype)(\(parametersForProxyInit())))" + } + } + + // Perform + func performProxyConstructorName(prefix: String = "") -> String { + let body: String = { + let (annotation, methodName, genericConstrains) = methodInfo() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(methodName)(\(returningParameter(true,false))perform: @escaping \(performProxyClosureType())) -> \(prefix)Perform\(genericConstrains)" + } else { + return "\(annotation)public static func \(methodName)(\(parametersForProxySignature()), \(returningParameter(true,false))perform: @escaping \(performProxyClosureType())) -> \(prefix)Perform\(genericConstrains)" + } + }() + return replacingSelf(body) + } + + func performProxyConstructor(prefix: String = "") -> String { + if filteredParameters.isEmpty { + return "return \(prefix)Perform(method: .\(prototype), performs: perform)" + } else { + return "return \(prefix)Perform(method: .\(prototype)(\(parametersForProxyInit())), performs: perform)" + } + } + + func performProxyClosureType() -> String { + if filteredParameters.isEmpty { + return "() -> Void" + } else { + let parameters = self.parameters + .map { "\($0.justPerformType)" } + .joined(separator: ", ") + return "(\(parameters)) -> Void" + } + } + + func performProxyClosureCall() -> String { + if filteredParameters.isEmpty { + return "perform?()" + } else { + let parameters = filteredParameters + .map { p in + let wrapped = ParameterWrapper(p, self.getVariadicParametersNames()) + let isAutolosure = wrapped.justType.hasPrefix("@autoclosure") + return "\(p.inout ? "&" : "")`\(p.name)`\(isAutolosure ? "()" : "")" + } + .joined(separator: ", ") + return "perform?(\(parameters))" + } + } + + func performCall() -> String { + guard !method.isInitializer else { return "" } + let type = performProxyClosureType() + var proxy = filteredParameters.isEmpty ? "\(prototype)" : "\(prototype)(\(parametersForMethodCall()))" + + let cast = "let perform = methodPerformValue(.\(proxy)) as? \(type)" + let call = performProxyClosureCall() + + return "\n\t\t\(cast)\n\t\t\(call)" + } + + // Helpers + private func parametersForMethodCall() -> String { + let generics = getGenericsWithoutConstraints() + return parameters.map { $0.wrappedForCalls(generics, hasAvailability) }.joined(separator: ", ") + } + + private func parametersForMethodTypeDeclaration(availability: Bool) -> String { + let generics = getGenericsWithoutConstraints() + return parameters.map { param in + if param.isGeneric(generics) { return param.genericType } + if availability { return param.typeErasedType } + return replacingSelf(param.nestedType) + }.joined(separator: ", ") + } + + private func parametersForProxySignature() -> String { + return parameters.map { p in + return "\(p.labelAndName()): \(replacingSelf(p.nestedType))" + }.joined(separator: ", ") + } + + private func parametersForStubSignature() -> String { + func replacing(first: String, in full: String, with other: String) -> String { + guard let range = full.range(of: first) else { return full } + return full.replacingCharacters(in: range, with: other) + } + let prefix = method.shortName + let full = method.name + let range = full.range(of: prefix)! + var unrefined = "\(full[range.upperBound...])" + parameters.map { p -> (String,String) in + return ("\(p.type)","\(p.justType)") + }.forEach { + unrefined = replacing(first: $0, in: unrefined, with: $1) + } + return unrefined + } + + private func parametersForProxyInit() -> String { + let generics = getGenericsWithoutConstraints() + return parameters.map { "\($0.wrappedForProxy(generics, hasAvailability))" }.joined(separator: ", ") + } + + private func isGeneric() -> Bool { + return method.shortName.contains("<") && method.shortName.contains(">") + } + + private func getVariadicParametersNames() -> [String] { + let pattern = "[\\(|,]( *[_|\\w]* )? *(\\w+) *\\: *(.+?\\.\\.\\.)" + let str = method.name + let range = NSRange(location: 0, length: (str as NSString).length) + + guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } + + var result: [String] = regex + .matches(in: str, options: [], range: range) + .compactMap { match -> String? in + guard let nameRange = Range(match.range(at: 2), in: str) else { return nil } + return String(str[nameRange]) + } + return result + } + + /// Returns list of generics used in method signature, without their constraints (like [T,U,V]) + /// + /// - Returns: Array of strings, where each strings represent generic name + private func getGenericsWithoutConstraints() -> [String] { + let name = method.shortName + guard let start = name.index(of: "<"), let end = name.index(of: ">") else { return [] } + + var genPart = name[start...end] + genPart.removeFirst() + genPart.removeLast() + + let parts = genPart.replacingOccurrences(of: " ", with: "").split(separator: ",").map(String.init) + return parts.map { stripGenPart(part: $0) } + } + + /// Returns list of generic constraintes from method signature. Does only contain stuff between '<' and '>' + /// + /// - Returns: Array of strings, like ["T: Codable", "U: Whatever"] + private func getGenericsConstraints(_ generics: [String], filterSingle: Bool = true) -> [String] { + let name = method.shortName + guard let start = name.index(of: "<"), let end = name.index(of: ">") else { return [] } + + var genPart = name[start...end] + genPart.removeFirst() + genPart.removeLast() + + let parts = genPart.replacingOccurrences(of: " ", with: "").split(separator: ",").map(String.init) + return parts.filter { + let components = $0.components(separatedBy: ":") + return (components.count == 2 || !filterSingle) && generics.contains(components[0]) + } + } + + private func getGenericsAmongParameters() -> [String] { + return getGenericsWithoutConstraints().filter { + for param in self.parameters { + if param.isGeneric([$0]) { return true } + } + return false + } + } + + private func wrapGenerics(_ generics: [String]) -> String { + guard !generics.isEmpty else { return "" } + return "<\(generics.joined(separator:","))>" + } + + private func stripGenPart(part: String) -> String { + return part.split(separator: ":").map(String.init).first! + } + + private func returnTypeStripped(_ method: SourceryRuntime.Method, type: Bool = false) -> String { + let returnTypeRaw = "\(method.returnTypeName)" + var stripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + stripped = stripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + guard type else { return stripped } + return "(\(stripped)).Type" + } + + private func whereClauseConstraints() -> [String] { + let returnTypeRaw = method.returnTypeName.name + guard let range = returnTypeRaw.range(of: "where") else { return [] } + var whereClause = returnTypeRaw + whereClause.removeSubrange(...(range.upperBound)) + return whereClause + .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + .components(separatedBy: ",") + } + + private func whereClauseExpression() -> String { + let constraints = whereClauseConstraints() + if constraints.isEmpty { + return "" + } + return " where " + constraints.joined(separator: ", ") + } + + private func methodInfo() -> (annotation: String, methodName: String, genericConstrains: String) { + let generics = getGenericsAmongParameters() + let methodName = returnTypeMatters() ? method.shortName : "\(method.callName)\(wrapGenerics(generics))" + let constraints: String = { + let constraints: [String] + if returnTypeMatters() { + constraints = whereClauseConstraints() + } else { + constraints = getGenericsConstraints(generics) + } + guard !constraints.isEmpty else { return "" } + + return " where \(constraints.joined(separator: ", "))" + }() + var attributes = self.methodAttributesNonObjc + attributes = attributes.condenseWhitespace() + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t\t" + return (attributes, methodName, constraints) + } +} + +extension String { + func condenseWhitespace() -> String { + let components = self.components(separatedBy: .whitespacesAndNewlines) + return components.filter { !$0.isEmpty }.joined(separator: " ") + } +} +class SubscriptWrapper { + let wrapped: SourceryRuntime.Subscript + var readonly: Bool { return !wrapped.isMutable } + var wrappedParameters: [ParameterWrapper] { return wrapped.parameters.map { ParameterWrapper($0) } } + var casesCount: Int { return readonly ? 1 : 2 } + var nestedType: String { return "\(TypeWrapper(wrapped.returnTypeName).nestedParameter)" } + let associatedTypes: [String]? + let genericTypesList: [String] + let genericTypesModifier: String? + let whereClause: String + var hasAvailability: Bool { wrapped.attributes["available"]?.isEmpty == false } + + private var methodAttributes: String { + return Helpers.extractAttributes(from: self.wrapped.attributes, filterOutStartingWith: ["mutating", "@inlinable"]) + } + private var methodAttributesNonObjc: String { + return Helpers.extractAttributes(from: self.wrapped.attributes, filterOutStartingWith: ["mutating", "@inlinable", "@objc"]) + } + + private let noStubDefinedMessage = "Stub return value not specified for subscript. Use given first." + + private static var registered: [String: Int] = [:] + private static var namesWithoutReturnType: [String: Int] = [:] + private static var suffixes: [String: Int] = [:] + public static func clear() -> String { + SubscriptWrapper.registered = [:] + SubscriptWrapper.suffixes = [:] + namesWithoutReturnType = [:] + return "" + } + static func register(_ name: String, _ uniqueName: String) { + let count = SubscriptWrapper.registered[name] ?? 0 + SubscriptWrapper.registered[name] = count + 1 + SubscriptWrapper.suffixes[uniqueName] = count + 1 + } + static func register(short name: String) { + let count = SubscriptWrapper.namesWithoutReturnType[name] ?? 0 + SubscriptWrapper.namesWithoutReturnType[name] = count + 1 + } + + func register() { + SubscriptWrapper.register(registrationName("get"),uniqueName) + SubscriptWrapper.register(short: shortName) + guard !readonly else { return } + SubscriptWrapper.register(registrationName("set"),uniqueName) + } + + init(_ wrapped: SourceryRuntime.Subscript) { + self.wrapped = wrapped + associatedTypes = Helpers.extractAssociatedTypes(from: wrapped) + genericTypesList = Helpers.extractGenericsList(associatedTypes) + whereClause = Helpers.extractWhereClause(from: wrapped) ?? "" + if let types = associatedTypes { + genericTypesModifier = "<\(types.joined(separator: ","))>" + } else { + genericTypesModifier = nil + } + } + + func registrationName(_ accessor: String) -> String { + return "subscript_\(accessor)_\(wrappedParameters.map({ $0.sanitizedForEnumCaseName() }).joined(separator: "_"))" + } + var shortName: String { return "public subscript\(genericTypesModifier ?? " ")(\(wrappedParameters.map({ $0.asMethodArgument() }).joined(separator: ", ")))" } + var uniqueName: String { return "\(shortName) -> \(wrapped.returnTypeName)\(self.whereClause)" } + + private func nameSuffix(_ accessor: String) -> String { + guard let count = SubscriptWrapper.registered[registrationName(accessor)] else { return "" } + guard count > 1 else { return "" } + guard let index = SubscriptWrapper.suffixes[uniqueName] else { return "" } + return "_\(index)" + } + + // call + func subscriptCall() -> String { + let get = "\n\t\tget {\(getter())\n\t\t}" + let set = readonly ? "" : "\n\t\tset {\(setter())\n\t\t}" + var attributes = self.methodAttributesNonObjc + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t" + return "\(attributes)\(uniqueName) {\(get)\(set)\n\t}" + } + private func getter() -> String { + let method = ".\(subscriptCasePrefix("get"))(\(parametersForMethodCall()))" + let optionalReturnWorkaround = "\(wrapped.returnTypeName)".hasSuffix("?") + let noStubDefined = (optionalReturnWorkaround || wrapped.returnTypeName.isOptional) ? "return nil" : "onFatalFailure(\"\(noStubDefinedMessage)\"); Failure(\"noStubDefinedMessage\")" + return + "\n\t\t\taddInvocation(\(method))" + + "\n\t\t\tdo {" + + "\n\t\t\t\treturn try methodReturnValue(\(method)).casted()" + + "\n\t\t\t} catch {" + + "\n\t\t\t\t\(noStubDefined)" + + "\n\t\t\t}" + } + private func setter() -> String { + let method = ".\(subscriptCasePrefix("set"))(\(parametersForMethodCall(set: true)))" + return "\n\t\t\taddInvocation(\(method))" + } + + var assertionName: String { + return readonly ? assertionName("get") : "\(assertionName("get"))\n\t\t\t\(assertionName("set"))" + } + private func assertionName(_ accessor: String) -> String { + return "case .\(subscriptCasePrefix(accessor)): return " + + "\"[\(accessor)] `subscript`\(genericTypesModifier ?? "")[\(parametersForAssertionName())]\"" + } + + // method type + func subscriptCasePrefix(_ accessor: String) -> String { + return "\(registrationName(accessor))\(nameSuffix(accessor))" + } + func subscriptCaseName(_ accessor: String, availability: Bool = false) -> String { + return "\(subscriptCasePrefix(accessor))(\(parametersForMethodTypeDeclaration(availability: availability, set: accessor == "set")))" + } + func subscriptCases() -> String { + if readonly { + return "case \(subscriptCaseName("get", availability: hasAvailability))" + } else { + return "case \(subscriptCaseName("get", availability: hasAvailability))\n\t\tcase \(subscriptCaseName("set", availability: hasAvailability))" + } + } + func equalCase(_ accessor: String) -> String { + var lhsParams = wrapped.parameters.map { "lhs\($0.name.capitalized)" }.joined(separator: ", ") + var rhsParams = wrapped.parameters.map { "rhs\($0.name.capitalized)" }.joined(separator: ", ") + var comparators = "\t\t\t\tvar results: [Matcher.ParameterComparisonResult] = []\n" + comparators += wrappedParameters.map { "\t\t\t\t\($0.comparatorResult())" }.joined(separator: "\n") + + if accessor == "set" { + lhsParams += ", lhsDidSet" + rhsParams += ", rhsDidSet" + comparators += "\n\t\t\t\tresults.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDidSet, rhs: rhsDidSet, with: matcher), lhsDidSet, rhsDidSet, \"newValue\"))" + } + + comparators += "\n\t\t\t\treturn Matcher.ComparisonResult(results)" + + // comparatorResult() + return "case (let .\(subscriptCasePrefix(accessor))(\(lhsParams)), let .\(subscriptCasePrefix(accessor))(\(rhsParams))):\n" + comparators + } + func equalCases() -> String { + return readonly ? equalCase("get") : "\(equalCase("get"))\n\t\t\t\(equalCase("set"))" + } + func intValueCase() -> String { + return readonly ? intValueCase("get") : "\(intValueCase("get"))\n\t\t\t\(intValueCase("set"))" + } + func intValueCase(_ accessor: String) -> String { + let params = wrappedParameters.enumerated().map { offset, _ in + return "p\(offset)" + } + let definitions = params.joined(separator: ", ") + (accessor == "set" ? ", _" : "") + let paramsSum = params.map({ "\($0).intValue" }).joined(separator: " + ") + return "case let .\(subscriptCasePrefix(accessor))(\(definitions)): return \(paramsSum)" + } + + // Given + func givenConstructorName() -> String { + let returnTypeString = returnsSelf ? replaceSelf : TypeWrapper(wrapped.returnTypeName).stripped + var attributes = self.methodAttributesNonObjc + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t\t" + return "\(attributes)public static func `subscript`\(genericTypesModifier ?? "")(\(parametersForProxySignature()), willReturn: \(returnTypeString)...) -> SubscriptStub" + } + func givenConstructor() -> String { + return "return Given(method: .\(subscriptCasePrefix("get"))(\(parametersForProxyInit())), products: willReturn.map({ StubProduct.return($0 as Any) }))" + } + + // Verify + func verifyConstructorName(set: Bool = false) -> String { + let returnTypeString = returnsSelf ? replaceSelf : nestedType + let returning = set ? "" : returningParameter(true, true) + var attributes = self.methodAttributesNonObjc + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t\t" + return "\(attributes)public static func `subscript`\(genericTypesModifier ?? "")(\(parametersForProxySignature())\(returning)\(set ? ", set newValue: \(returnTypeString)" : "")) -> Verify" + } + func verifyConstructor(set: Bool = false) -> String { + return "return Verify(method: .\(subscriptCasePrefix(set ? "set" : "get"))(\(parametersForProxyInit(set: set))))" + } + + // Generics + private func getGenerics() -> [String] { + return genericTypesList + } + + // Helpers + private var returnsSelf: Bool { return TypeWrapper(wrapped.returnTypeName).isSelfType } + private var replaceSelf: String { return Current.selfType } + private func returnTypeStripped(type: Bool = false) -> String { + let returnTypeRaw = "\(wrapped.returnTypeName)" + var stripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + stripped = stripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + guard type else { return stripped } + return "(\(stripped)).Type" + } + private func returnTypeMatters() -> Bool { + let count = SubscriptWrapper.namesWithoutReturnType[shortName] ?? 0 + return count > 1 + } + + // params + private func returningParameter(_ multiple: Bool, _ front: Bool) -> String { + guard returnTypeMatters() else { return "" } + let returning: String = "returning: \(returnTypeStripped(type: true))" + guard multiple else { return returning } + return front ? ", \(returning)" : "\(returning), " + } + private func parametersForMethodTypeDeclaration(availability: Bool = false, set: Bool = false) -> String { + let generics: [String] = getGenerics() + let params = wrappedParameters.map { param in + if param.isGeneric(generics) { return param.genericType } + if availability { return param.typeErasedType } + return param.nestedType + }.joined(separator: ", ") + guard set else { return params } + let newValue = TypeWrapper(wrapped.returnTypeName).isGeneric(generics) ? "Parameter" : nestedType + return "\(params), \(newValue)" + } + private func parametersForProxyInit(set: Bool = false) -> String { + let generics = getGenerics() + let newValue = TypeWrapper(wrapped.returnTypeName).isGeneric(generics) ? "newValue.wrapAsGeneric()" : "newValue" + return wrappedParameters.map { "\($0.wrappedForProxy(generics, hasAvailability))" }.joined(separator: ", ") + (set ? ", \(newValue)" : "") + } + private func parametersForProxySignature(set: Bool = false) -> String { + return wrappedParameters.map { "\($0.labelAndName()): \($0.nestedType)" }.joined(separator: ", ") + (set ? ", set newValue: \(nestedType)" : "") + } + private func parametersForAssertionName() -> String { + return wrappedParameters.map { "\($0.labelAndName())" }.joined(separator: ", ") + } + private func parametersForMethodCall(set: Bool = false) -> String { + let generics = getGenerics() + let params = wrappedParameters.map { $0.wrappedForCalls(generics, hasAvailability) }.joined(separator: ", ") + let postfix = TypeWrapper(wrapped.returnTypeName).isGeneric(generics) ? ".wrapAsGeneric()" : "" + return !set ? params : "\(params), \(nestedType).value(newValue)\(postfix)" + } +} +class VariableWrapper { + let variable: SourceryRuntime.Variable + let scope: String + var readonly: Bool { return variable.writeAccess.isEmpty } + var privatePrototypeName: String { return "__p_\(variable.name)".replacingOccurrences(of: "`", with: "") } + var casesCount: Int { return readonly ? 1 : 2 } + + var accessModifier: String { + // TODO: Fix access levels for SwiftyPrototype + // guard variable.type?.accessLevel != "internal" else { return "" } + return "public " + } + var attributes: String { + let value = Helpers.extractAttributes(from: self.variable.attributes) + return value.isEmpty ? "\(accessModifier)" : "\(value)\n\t\t\(accessModifier)" + } + var noStubDefinedMessage: String { return "\(scope) - stub value for \(variable.name) was not defined" } + + var getter: String { + let staticModifier = variable.isStatic ? "\(scope)." : "" + let returnValue = variable.isOptional ? "optionalGivenGetterValue(.\(propertyCaseGetName), \"\(noStubDefinedMessage)\")" : "givenGetterValue(.\(propertyCaseGetName), \"\(noStubDefinedMessage)\")" + return "\n\t\tget {\t\(staticModifier)invocations.append(.\(propertyCaseGetName)); return \(staticModifier)\(privatePrototypeName) ?? \(returnValue) }" + } + var setter: String { + let staticModifier = variable.isStatic ? "\(scope)." : "" + if readonly { + return "" + } else { + return "\n\t\tset {\t\(staticModifier)invocations.append(.\(propertyCaseSetName)(.value(newValue))); \(variable.isStatic ? "\(scope)." : "")\(privatePrototypeName) = newValue }" + } + } + var prototype: String { + let staticModifier = variable.isStatic ? "static " : "" + + return "\(attributes)\(staticModifier)var \(variable.name): \(variable.typeName.name) {" + + "\(getter)" + + "\(setter)" + + "\n\t}" + } + var assertionName: String { + var result = "case .\(propertyCaseGetName): return \"[get] .\(variable.name)\"" + if !readonly { + result += "\n\t\t\tcase .\(propertyCaseSetName): return \"[set] .\(variable.name)\"" + } + return result + } + + var privatePrototype: String { + let staticModifier = variable.isStatic ? "static " : "" + var typeName = "\(variable.typeName.unwrappedTypeName)" + let isWrappedInBrackets = typeName.hasPrefix("(") && typeName.hasSuffix(")") + if !isWrappedInBrackets { + typeName = "(\(typeName))" + } + return "private \(staticModifier)var \(privatePrototypeName): \(typeName)?" + } + var nestedType: String { return "\(TypeWrapper(variable.typeName).nestedParameter)" } + + init(_ variable: SourceryRuntime.Variable, scope: String) { + self.variable = variable + self.scope = scope + } + + func compareCases() -> String { + var result = propertyCaseGetCompare() + if !readonly { + result += "\n\t\t\t\(propertyCaseSetCompare())" + } + return result + } + + func propertyGet() -> String { + let staticModifier = variable.isStatic ? "Static" : "" + return "public static var \(variable.name): \(staticModifier)Verify { return \(staticModifier)Verify(method: .\(propertyCaseGetName)) }" + } + + func propertySet() -> String { + let staticModifier = variable.isStatic ? "Static" : "" + return "public static func \(variable.name)(set newValue: \(nestedType)) -> \(staticModifier)Verify { return \(staticModifier)Verify(method: .\(propertyCaseSetName)(newValue)) }" + } + + var propertyCaseGetName: String { return "p_\(variable.name)_get".replacingOccurrences(of: "`", with: "") } + func propertyCaseGet() -> String { + return "case \(propertyCaseGetName)" + } + func propertyCaseGetCompare() -> String { + return "case (.\(propertyCaseGetName),.\(propertyCaseGetName)): return Matcher.ComparisonResult.match" + } + func propertyCaseGetIntValue() -> String { + return "case .\(propertyCaseGetName): return 0" + } + + var propertyCaseSetName: String { return "p_\(variable.name)_set".replacingOccurrences(of: "`", with: "") } + func propertyCaseSet() -> String { + return "case \(propertyCaseSetName)(\(nestedType))" + } + func propertyCaseSetCompare() -> String { + let lhsName = "left" + let rhsName = "right" + let comaprison = "Matcher.ParameterComparisonResult(\(nestedType).compare(lhs: \(lhsName), rhs: \(rhsName), with: matcher), \(lhsName), \(rhsName), \"newValue\")" + let result = "Matcher.ComparisonResult([\(comaprison)])" + return "case (.\(propertyCaseSetName)(let left),.\(propertyCaseSetName)(let right)): return \(result)" + } + func propertyCaseSetIntValue() -> String { + return "case .\(propertyCaseSetName)(let newValue): return newValue.intValue" + } + + // Given + func givenConstructorName(prefix: String = "") -> String { + return "\(attributes)static func \(variable.name)(getter defaultValue: \(TypeWrapper(variable.typeName).stripped)...) -> \(prefix)PropertyStub" + } + + func givenConstructor(prefix: String = "") -> String { + return "return \(prefix)Given(method: .\(propertyCaseGetName), products: defaultValue.map({ StubProduct.return($0 as Any) }))" + } +} +_%> +<%# ================================================== SETUP -%><%_ -%> +<%_ var all = types.all + all += types.protocols.map { $0 } + all += types.protocolCompositions.map { $0 } + var mockedCount = 0 +-%> + +<%_ for type in all { -%><%_ -%> +<%_ let autoMockable: Bool = type.inheritedTypes.contains("AutoMockable") || type.annotations["AutoMockable"] != nil + let protocolToDecorate = types.protocols.first(where: { $0.name == (type.annotations["mock"] as? String) }) + let inlineMockable = protocolToDecorate != nil + guard let aProtocol = autoMockable ? type : protocolToDecorate else { continue } + mockedCount += 1 + + let associatedTypes: [String]? = Helpers.extractAssociatedTypes(from: aProtocol) + let attributes: String = Helpers.extractAttributes(from: type.attributes) + let typeAliases: [String] = Helpers.extractTypealiases(from: aProtocol) + let genericTypesModifier: String = Helpers.extractGenericTypesModifier(associatedTypes) + let genericTypesConstraints: String = Helpers.extractGenericTypesConstraints(associatedTypes) + let allSubscripts = aProtocol.allSubscripts + let allVariables = uniques(variables: aProtocol.allVariables.filter({ !$0.isStatic })) + let containsVariables = !allVariables.isEmpty + let allStaticVariables = uniques(variables: aProtocol.allVariables.filter({ $0.isStatic })) + let containsStaticVariables = !allStaticVariables.isEmpty + let allMethods = uniques(methods: aProtocol.allMethods.filter({ !$0.isStatic || $0.isInitializer })) + let selfConstrained = allMethods.map(wrapMethod).contains(where: { $0.returnsGenericConstrainedToSelf || $0.parametersContainsSelf }) + let accessModifier: String = selfConstrained ? "public final" : "open" + Current.accessModifier = accessModifier // TODO: Temporary workaround for access modifiers + let inheritFromNSObject = type.annotations["ObjcProtocol"] != nil || attributes.contains("@objc") + let allMethodsForMethodType = uniquesWithoutGenericConstraints(methods: aProtocol.allMethods.filter({ !$0.isStatic })) + let allStaticMethods = uniques(methods: aProtocol.allMethods.filter({ $0.isStatic && !$0.isInitializer })) + let allStaticMethodsForMethodType = uniquesWithoutGenericConstraints(methods: aProtocol.allMethods.filter({ $0.isStatic })) + let conformsToStaticMock = !allStaticMethods.isEmpty || !allStaticVariables.isEmpty + let conformsToMock = !allMethods.isEmpty || !allVariables.isEmpty -%><%_ -%><%_ -%> +<%_ if autoMockable { -%> +// MARK: - <%= type.name %> +<%= attributes %> +<%= accessModifier %> class <%= type.name %><%= mockTypeName %><%= genericTypesModifier %>:<%= inheritFromNSObject ? " NSObject," : "" %> <%= type.name %>, Mock<%= conformsToStaticMock ? ", StaticMock" : "" %><%= genericTypesConstraints %> { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + +<%_ } else { -%> +// sourcery:inline:auto:<%= type.name %>.autoMocked +<%_ } -%> +<%# ================================================== MAIN CLASS -%><%_ -%> + <%# ================================================== MOCK INTERNALS -%><%_ -%> + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + <%_ for typeAlias in typeAliases { -%> + public typealias <%= typeAlias %> + <%_ } %> <%_ -%> + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + <%_ -%> + <%# ================================================== STATIC MOCK INTERNALS -%><%_ -%> + <%_ if conformsToStaticMock { -%> + static var matcher: Matcher = Matcher.default + static var stubbingPolicy: StubbingPolicy = .wrap + static var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + static private var queue = DispatchQueue(label: "com.swiftymocky.invocations.static", qos: .userInteractive) + static private var invocations: [StaticMethodType] = [] + static private var methodReturnValues: [StaticGiven] = [] + static private var methodPerformValues: [StaticPerform] = [] + public typealias StaticPropertyStub = StaticGiven + public typealias StaticMethodStub = StaticGiven + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public static func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + <%_ } -%> + + <%# ================================================== VARIABLES -%><%_ -%> + <%_ for variable in allVariables { -%> + <%_ if autoMockable { -%> + <%= stubProperty(variable,"\(type.name)\(mockTypeName)") %> + <%_ } else { %> + <%= stubProperty(variable,"\(type.name)") %> + <%_ } %> + <%_ } %> <%_ -%> + + <%# ================================================== STATIC VARIABLES -%><%_ -%> + <%_ for variable in allStaticVariables { -%> + <%_ if autoMockable { -%> + <%= stubProperty(variable,"\(type.name)\(mockTypeName)") %> + <%_ } else { %> + <%= stubProperty(variable,"\(type.name)") %> + <%_ } %> + <%_ } %> <%_ -%> + + <%# ================================================== METHOD REGISTRATIONS -%><%_ -%> + <%_ MethodWrapper.clear() -%> + <%_ SubscriptWrapper.clear() -%> + <%_ if autoMockable { -%> + <%_ Current.selfType = "\(type.name)\(mockTypeName)\(genericTypesModifier)" -%> + <%_ } else { %> + <%_ Current.selfType = "\(type.name)\(mockTypeName)\(genericTypesModifier)" -%> + <%_ } %> + <%_ let wrappedSubscripts = allSubscripts.map(wrapSubscript) -%> + <%_ let wrappedMethods = allMethods.map(wrapMethod).filter({ $0.wrappedInMethodType() }) -%> + <%_ let wrappedVariables = allVariables.map(justWrap) -%> + <%_ let wrappedMethodsForMethodType = allMethodsForMethodType.map(wrapMethod).filter({ $0.wrappedInMethodType() }) -%> + <%_ let wrappedInitializers = allMethods.map(wrapMethod).filter({ $0.method.isInitializer }) -%> + <%_ let wrappedStaticMethods = allStaticMethods.map(wrapMethod).filter({ $0.wrappedInMethodType() }) -%> + <%_ let wrappedStaticVariables = allStaticVariables.map(justWrap) -%> + <%_ let wrappedStaticMethodsForMethodType = allStaticMethodsForMethodType.map(wrapMethod).filter({ $0.wrappedInMethodType() }) -%> + <%_ for variable in allVariables { propertyRegister(variable) } -%> + <%_ for variable in allStaticVariables { propertyRegister(variable) } -%> + <%_ for method in wrappedMethods { method.register() } -%> + <%_ for wrapped in wrappedSubscripts { wrapped.register() } -%> + <%_ for method in wrappedStaticMethods { method.register() } -%><%_ -%> + <%_ let variableCasesCount: Int = wrappedVariables.reduce(0) { return $0 + $1.casesCount } -%><%_ -%> + <%_ let subscriptsCasesCount: Int = wrappedSubscripts.reduce(0) { return $0 + $1.casesCount } -%><%_ -%> + <%_ let staticVariableCasesCount: Int = wrappedStaticVariables.reduce(0) { return $0 + $1.casesCount } -%><%_ -%> + + <%# ================================================== STATIC STUBS -%><%_ -%> + <%_ for method in wrappedStaticMethods { -%> + <%= method.functionPrototype _%> { + <%= method.stubBody() _%> + } + + <%_ } %><%_ -%> + <%_ -%> + <%# ================================================== INITIALIZERS -%><%_ -%> + <%_ for method in wrappedInitializers { -%> + <%= method.functionPrototype _%> { } + + <%_ } -%><%_ -%> + <%_ -%><%_ -%> + <%# ================================================== STUBS -%><%_ -%> + <%_ for method in wrappedMethods { -%> + <%= method.functionPrototype _%> { + <%= method.stubBody() _%> + } + + <%_ } -%> + <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.subscriptCall() _%> + + <%_ } -%> + <%# ================================================== STATIC METHOD TYPE -%><%_ -%> + <%_ if conformsToStaticMock { -%> + fileprivate enum StaticMethodType { + <%_ for method in wrappedStaticMethodsForMethodType { -%> + <%= method.methodTypeDeclarationWithParameters() _%> + <%_ } %> <%_ for variable in allStaticVariables { -%> + <%= propertyMethodTypes(variable) %> + <%_ } %> <%_ %> + <%_ -%> + static func compareParameters(lhs: StaticMethodType, rhs: StaticMethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { <%_ for method in wrappedStaticMethodsForMethodType { %> + <%= method.equalCases() %> + <%_ } %> <%_ for variable in wrappedStaticVariables { -%> + <%= variable.compareCases() %> + <%_ } %> <%_ -%> <%_ if wrappedStaticMethods.count + staticVariableCasesCount > 1 { -%> + default: return .none + <%_ } -%> + } + } + <%_ %> + func intValue() -> Int { + switch self { <%_ for method in wrappedStaticMethodsForMethodType { %> + <%= method.intValueCase -%><% } %> + <%_ for variable in allStaticVariables { -%> + <%= propertyMethodTypesIntValue(variable) %> + <%_ } %> <%_ -%> + } + } + func assertionName() -> String { + switch self { <%_ for method in wrappedStaticMethodsForMethodType { %> + <%= method.assertionName -%><% } %> + <%_ for variable in wrappedStaticVariables { -%> + <%= variable.assertionName %> + <%_ } %> + } + } + } + + open class StaticGiven: StubbedMethod { + fileprivate var method: StaticMethodType + + private init(method: StaticMethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + <%_ for variable in allStaticVariables { -%> + <%= wrapProperty(variable).givenConstructorName(prefix: "Static") -%> { + <%= wrapProperty(variable).givenConstructor(prefix: "Static") _%> + } + <%_ } %> <%_ %> + <%_ for method in wrappedStaticMethodsForMethodType.filter({ !$0.method.returnTypeName.isVoid && !$0.method.isInitializer }) { -%> + <%= method.givenConstructorName(prefix: "Static") -%> { + <%= method.givenConstructor(prefix: "Static") _%> + } + <%_ } -%> + <%_ for method in wrappedStaticMethodsForMethodType.filter({ !$0.method.throws && !$0.method.rethrows && !$0.method.returnTypeName.isVoid && !$0.method.isInitializer }) { -%> + <%= method.givenProduceConstructorName(prefix: "Static") -%> { + <%= method.givenProduceConstructor(prefix: "Static") _%> + } + <%_ } -%> + <%_ for method in wrappedStaticMethodsForMethodType.filter({ ($0.method.throws || $0.method.rethrows) && !$0.method.isInitializer }) { -%> + <%= method.givenConstructorNameThrows(prefix: "Static") -%> { + <%= method.givenConstructorThrows(prefix: "Static") _%> + } + <%= method.givenProduceConstructorNameThrows(prefix: "Static") -%> { + <%= method.givenProduceConstructorThrows(prefix: "Static") _%> + } + <%_ } %> <%_ -%> + } + + public struct StaticVerify { + fileprivate var method: StaticMethodType + + <%_ for method in wrappedStaticMethodsForMethodType { -%> + <%= method.verificationProxyConstructorName(prefix: "Static") -%> { <%= method.verificationProxyConstructor(prefix: "Static") _%> } + <%_ } %> <%_ -%> + <%_ for variable in allStaticVariables { -%> + <%= propertyTypes(variable) %> + <%_ } %> <%_ -%> + } + + public struct StaticPerform { + fileprivate var method: StaticMethodType + var performs: Any + + <%_ for method in wrappedStaticMethodsForMethodType { -%> + <%= method.performProxyConstructorName(prefix: "Static") -%> { + <%= method.performProxyConstructor(prefix: "Static") _%> + } + <%_ } %> <%_ -%> + } + + <% } -%> + <%# ================================================== METHOD TYPE -%><%_ -%> + <%_ if !wrappedMethods.isEmpty || !allVariables.isEmpty || !allSubscripts.isEmpty { -%> + + fileprivate enum MethodType { + <%_ for method in wrappedMethodsForMethodType { -%> + <%= method.methodTypeDeclarationWithParameters() _%> + <%_ } -%> <%_ for variable in allVariables { -%> + <%= propertyMethodTypes(variable) %> + <%_ } %> <%_ %> <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.subscriptCases() _%> + <%_ } %> <%_ %> + <%_ -%> + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { <%_ for method in wrappedMethodsForMethodType { %> + <%= method.equalCases() %> + <%_ } %> <%_ for variable in wrappedVariables { -%> + <%= variable.compareCases() %> + <%_ } %> <%_ -%> <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.equalCases() %> + <%_ } %> <%_ if wrappedMethods.count + variableCasesCount + subscriptsCasesCount > 1 { -%> + default: return .none + <%_ } -%> + } + } + <%_ %> + func intValue() -> Int { + switch self { <%_ for method in wrappedMethodsForMethodType { %> + <%= method.intValueCase -%><% } %> + <%_ for variable in allVariables { -%> + <%= propertyMethodTypesIntValue(variable) %> + <%_ } %> <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.intValueCase() %> + <%_ } -%> + } + } + func assertionName() -> String { + switch self { <%_ for method in wrappedMethodsForMethodType { %> + <%= method.assertionName -%><% } %> + <%_ for variable in wrappedVariables { -%> + <%= variable.assertionName %> + <%_ } %> <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.assertionName %> + <%_ } -%> + } + } + } + <%_ } else { %> + fileprivate struct MethodType { + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { return .match } + func intValue() -> Int { return 0 } + func assertionName() -> String { return "" } + } + <%_ } -%><%_ -%> + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + <%_ for variable in allVariables { -%> + <%= wrapProperty(variable).givenConstructorName() -%> { + <%= wrapProperty(variable).givenConstructor() _%> + } + <%_ } %> <%_ %> + <%_ for method in wrappedMethodsForMethodType.filter({ !$0.method.returnTypeName.isVoid && !$0.method.isInitializer }) { -%> + <%= method.givenConstructorName() -%> { + <%= method.givenConstructor() _%> + } + <%_ } -%> + <%_ for method in wrappedMethodsForMethodType.filter({ !$0.method.throws && !$0.method.rethrows && !$0.method.returnTypeName.isVoid && !$0.method.isInitializer }) { -%> + <%= method.givenProduceConstructorName() -%> { + <%= method.givenProduceConstructor() _%> + } + <%_ } -%> + <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.givenConstructorName() -%> { + <%= wrapped.givenConstructor() _%> + } + <%_ } -%> + <%_ for method in wrappedMethodsForMethodType.filter({ ($0.method.throws || $0.method.rethrows) && !$0.method.isInitializer }) { -%> + <%= method.givenConstructorNameThrows() -%> { + <%= method.givenConstructorThrows() _%> + } + <%= method.givenProduceConstructorNameThrows() -%> { + <%= method.givenProduceConstructorThrows() _%> + } + <%_ } %> <%_ -%> + } + + public struct Verify { + fileprivate var method: MethodType + + <%_ for method in wrappedMethodsForMethodType { -%> + <%= method.verificationProxyConstructorName() -%> { <%= method.verificationProxyConstructor() _%> } + <%_ } %> <%_ -%> + <%_ for variable in allVariables { -%> + <%= propertyTypes(variable) %> + <%_ } %> <%_ -%> + <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.verifyConstructorName() -%> { <%= wrapped.verifyConstructor() _%> } + <%_ if !wrapped.readonly { -%> + <%= wrapped.verifyConstructorName(set: true) -%> { <%= wrapped.verifyConstructor(set: true) _%> } + <%_ } -%> + <%_ } %> <%_ -%> + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + <%_ for method in wrappedMethodsForMethodType { -%> + <%= method.performProxyConstructorName() -%> { + <%= method.performProxyConstructor() _%> + } + <%_ } %> <%_ -%> + } + + <%# ================================================== MOCK METHODS -%><%_ -%> + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } + <%# ================================================== STATIC MOCK METHODS -%><%_ -%> + <%_ if conformsToStaticMock { -%> + + static public func given(_ method: StaticGiven) { + methodReturnValues.append(method) + } + + static public func perform(_ method: StaticPerform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + static public func verify(_ method: StaticVerify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return StaticMethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + static private func addInvocation(_ call: StaticMethodType) { + self.queue.sync { invocations.append(call) } + } + static private func methodReturnValue(_ method: StaticMethodType) throws -> StubProduct { + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && StaticMethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + static private func methodPerformValue(_ method: StaticMethodType) -> Any? { + let matched = methodPerformValues.reversed().first { StaticMethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + static private func matchingCalls(_ method: StaticMethodType, file: StaticString?, line: UInt?) -> [StaticMethodType] { + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return invocations.filter { StaticMethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + static private func matchingCalls(_ method: StaticVerify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + static private func givenGetterValue(_ method: StaticMethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + Failure(message) + } + } + static private func optionalGivenGetterValue(_ method: StaticMethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + <%_ } -%> +<%_ if autoMockable { -%> +} + +<%_ } else { -%> +// sourcery:end +<%_ } -%> +<% } -%> +<%_ if mockedCount == 0 { -%> +// SwiftyMocky: no AutoMockable found. +// Please define and inherit from AutoMockable, or annotate protocols to be mocked +<%_ } -%> diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 8aecb5c84..072473af5 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -12,11 +12,14 @@ 0218196528F734FA00202564 /* Discussion.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0218196328F734FA00202564 /* Discussion.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0219C67728F4347600D64452 /* Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0219C67628F4347600D64452 /* Course.framework */; }; 0219C67828F4347600D64452 /* Course.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0219C67628F4347600D64452 /* Course.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 024E69202AEFC3FB00FA0B59 /* MainScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024E691F2AEFC3FB00FA0B59 /* MainScreenViewModel.swift */; }; 025AD4AC2A6FB95C00AB8FA7 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */; }; 025DE1A428DB4DAE0053E0F4 /* Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 025DE1A328DB4DAE0053E0F4 /* Profile.framework */; }; 025DE1A528DB4DAE0053E0F4 /* Profile.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 025DE1A328DB4DAE0053E0F4 /* Profile.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 027DB33028D8A063002B6862 /* Dashboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 027DB32F28D8A063002B6862 /* Dashboard.framework */; }; 027DB33128D8A063002B6862 /* Dashboard.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 027DB32F28D8A063002B6862 /* Dashboard.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 028A37362ADFF404008CA604 /* WhatsNew.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 028A37352ADFF404008CA604 /* WhatsNew.framework */; }; + 028A37372ADFF404008CA604 /* WhatsNew.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 028A37352ADFF404008CA604 /* WhatsNew.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0293A2032A6FCA590090A336 /* CorePersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0293A2022A6FCA590090A336 /* CorePersistence.swift */; }; 0293A2052A6FCD430090A336 /* CoursePersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0293A2042A6FCD430090A336 /* CoursePersistence.swift */; }; 0293A2072A6FCDA30090A336 /* DiscoveryPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0293A2062A6FCDA30090A336 /* DiscoveryPersistence.swift */; }; @@ -26,7 +29,6 @@ 02ED50D829A66007008341CD /* languages.json in Resources */ = {isa = PBXBuildFile; fileRef = 02ED50DA29A66007008341CD /* languages.json */; }; 02F175312A4DA95B0019CD70 /* MainScreenAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175302A4DA95B0019CD70 /* MainScreenAnalytics.swift */; }; 071009C928D1DB3F00344290 /* ScreenAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 071009C828D1DB3F00344290 /* ScreenAssembly.swift */; }; - 0727876D28D23312002E9142 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727876C28D23312002E9142 /* Environment.swift */; }; 0727878E28D347C7002E9142 /* MainScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727878D28D347C7002E9142 /* MainScreenView.swift */; }; 072787B128D34D83002E9142 /* Discovery.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072787B028D34D83002E9142 /* Discovery.framework */; }; 072787B228D34D83002E9142 /* Discovery.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 072787B028D34D83002E9142 /* Discovery.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -43,6 +45,9 @@ 07D5DA3E28D075AB00752FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */; }; 07D5DA4128D075AB00752FD9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3F28D075AB00752FD9 /* LaunchScreen.storyboard */; }; 95C140F3BDF778364986E83B /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F138C15C3A2515F8F94DAA8B /* Pods_App_OpenEdX.framework */; }; + BA3042792B1F7147009B64B7 /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = BA3042782B1F7147009B64B7 /* MSAL */; }; + E0D6E6A32B1626B10089F9C9 /* Theme.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0D6E6A22B1626B10089F9C9 /* Theme.framework */; }; + E0D6E6A42B1626D60089F9C9 /* Theme.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E0D6E6A22B1626B10089F9C9 /* Theme.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -52,6 +57,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + E0D6E6A42B1626D60089F9C9 /* Theme.framework in Embed Frameworks */, 072787B228D34D83002E9142 /* Discovery.framework in Embed Frameworks */, 0218196528F734FA00202564 /* Discussion.framework in Embed Frameworks */, 07A7D79028F5C9060000BE81 /* Core.framework in Embed Frameworks */, @@ -59,6 +65,7 @@ 0219C67828F4347600D64452 /* Course.framework in Embed Frameworks */, 025DE1A528DB4DAE0053E0F4 /* Profile.framework in Embed Frameworks */, 027DB33128D8A063002B6862 /* Dashboard.framework in Embed Frameworks */, + 028A37372ADFF404008CA604 /* WhatsNew.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -70,11 +77,13 @@ 0218196328F734FA00202564 /* Discussion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discussion.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0219C67628F4347600D64452 /* Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02450ABD29C35FF20094E2D0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 024E691F2AEFC3FB00FA0B59 /* MainScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenViewModel.swift; sourceTree = ""; }; 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; 025C77A028E463E900B3DFA3 /* CourseOutline.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CourseOutline.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 025DE1A328DB4DAE0053E0F4 /* Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 025EF2F7297177F300B838AB /* OpenEdX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenEdX.entitlements; sourceTree = ""; }; 027DB32F28D8A063002B6862 /* Dashboard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Dashboard.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 028A37352ADFF404008CA604 /* WhatsNew.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WhatsNew.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0293A2022A6FCA590090A336 /* CorePersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePersistence.swift; sourceTree = ""; }; 0293A2042A6FCD430090A336 /* CoursePersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursePersistence.swift; sourceTree = ""; }; 0293A2062A6FCDA30090A336 /* DiscoveryPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPersistence.swift; sourceTree = ""; }; @@ -88,7 +97,6 @@ 02ED50DB29A6600B008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = uk; path = uk.lproj/languages.json; sourceTree = ""; }; 02F175302A4DA95B0019CD70 /* MainScreenAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenAnalytics.swift; sourceTree = ""; }; 071009C828D1DB3F00344290 /* ScreenAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenAssembly.swift; sourceTree = ""; }; - 0727876C28D23312002E9142 /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 0727878D28D347C7002E9142 /* MainScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenView.swift; sourceTree = ""; }; 072787B028D34D83002E9142 /* Discovery.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discovery.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0770DE1228D07845006D8A5D /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -109,6 +117,7 @@ A24D6A8E1BC4DF46AD68904C /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; A89AD827F52CF6A6B903606E /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; BB08ACD2CCA33D6DDDDD31B4 /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; + E0D6E6A22B1626B10089F9C9 /* Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F138C15C3A2515F8F94DAA8B /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -117,9 +126,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E0D6E6A32B1626B10089F9C9 /* Theme.framework in Frameworks */, 07A7D78F28F5C9060000BE81 /* Core.framework in Frameworks */, + 028A37362ADFF404008CA604 /* WhatsNew.framework in Frameworks */, 025DE1A428DB4DAE0053E0F4 /* Profile.framework in Frameworks */, 0770DE4B28D0A462006D8A5D /* Authorization.framework in Frameworks */, + BA3042792B1F7147009B64B7 /* MSAL in Frameworks */, 072787B128D34D83002E9142 /* Discovery.framework in Frameworks */, 0218196428F734FA00202564 /* Discussion.framework in Frameworks */, 0219C67728F4347600D64452 /* Course.framework in Frameworks */, @@ -148,6 +160,7 @@ isa = PBXGroup; children = ( 0727878D28D347C7002E9142 /* MainScreenView.swift */, + 024E691F2AEFC3FB00FA0B59 /* MainScreenViewModel.swift */, ); path = View; sourceTree = ""; @@ -190,7 +203,6 @@ 0298DF2F2A4EF7230023A257 /* AnalyticsManager.swift */, 02F175302A4DA95B0019CD70 /* MainScreenAnalytics.swift */, 0293A2012A6FC9E30090A336 /* Data */, - 0727876C28D23312002E9142 /* Environment.swift */, 0727878C28D347B2002E9142 /* View */, 0770DE1A28D084BC006D8A5D /* DI */, 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */, @@ -206,6 +218,8 @@ 4E6FB43543890E90BB88D64D /* Frameworks */ = { isa = PBXGroup; children = ( + E0D6E6A22B1626B10089F9C9 /* Theme.framework */, + 028A37352ADFF404008CA604 /* WhatsNew.framework */, 0218196328F734FA00202564 /* Discussion.framework */, 07A7D78E28F5C9060000BE81 /* Core.framework */, 0219C67628F4347600D64452 /* Course.framework */, @@ -247,6 +261,7 @@ 07D5DA2E28D075AA00752FD9 /* Frameworks */, 07D5DA2F28D075AA00752FD9 /* Resources */, 0770DE1528D07845006D8A5D /* Embed Frameworks */, + DB97C0542B002EF00035C36F /* Process Config */, 02F175442A4E3B320019CD70 /* FirebaseCrashlytics */, ); buildRules = ( @@ -254,6 +269,9 @@ dependencies = ( ); name = OpenEdX; + packageProductDependencies = ( + BA3042782B1F7147009B64B7 /* MSAL */, + ); productName = OpenEdX; productReference = 07D5DA3128D075AA00752FD9 /* OpenEdX.app */; productType = "com.apple.product-type.application"; @@ -283,6 +301,9 @@ uk, ); mainGroup = 07D5DA2828D075AA00752FD9; + packageReferences = ( + BA3042772B1F7147009B64B7 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */, + ); productRefGroup = 07D5DA3228D075AA00752FD9 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -324,7 +345,7 @@ ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; - shellScript = "case $CONFIGURATION in\n \"DebugDev\" | \"ReleaseDev\" )\n googleAppID=$(grep -A 4 'case .debugDev, .releaseDev:' ${PROJECT_DIR}/${TARGET_NAME}/Environment.swift | grep 'googleAppID:' | awk -F'\"' '{print $2}')\n ;;\n \"DebugStage\" | \"ReleaseStage\" )\n googleAppID=$(grep -A 4 'case .debugStage, .releaseStage:' ${PROJECT_DIR}/${TARGET_NAME}/Environment.swift | grep 'googleAppID:' | awk -F'\"' '{print $2}')\n ;;\n \"DebugProd\" | \"RelesaseProd\" )\n googleAppID=$(grep -A 4 'case .debugProd, .releaseProd:' ${PROJECT_DIR}/${TARGET_NAME}/Environment.swift | grep 'googleAppID:' | awk -F'\"' '{print $2}')\n ;;\n *)\n echo \"Unknown configuration\"\n ;;\nesac\n\nif [ -z \"$googleAppID\" ]\nthen\n echo \"GoogleAppID is empty. The FirebaseCrashlytics script will be skipped.\"\nelse\n \"${PODS_ROOT}/FirebaseCrashlytics/run\" --app-id \"$googleAppID\" -p ios ${DWARF_DSYM_FOLDER_PATH}\nfi\n"; + shellScript = "plistPath=\"${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}/GoogleService-Info.plist\"\n\ngoogleAppID=$(/usr/libexec/PlistBuddy -c \"Print :GOOGLE_APP_ID\" \"$plistPath\")\n\nif [ -z \"$googleAppID\" ]\nthen\n echo \"GoogleAppID is empty. The FirebaseCrashlytics script will be skipped.\"\nelse\n \"${PODS_ROOT}/FirebaseCrashlytics/run\" --app-id \"$googleAppID\" -p ios \"${DWARF_DSYM_FOLDER_PATH}\"\nfi\n"; }; 0770DE2328D08647006D8A5D /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; @@ -366,6 +387,25 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + DB97C0542B002EF00035C36F /* Process Config */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Process Config"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/bin/bash\n\nVENV_PATH=\"${SRCROOT}/venv\"\n\n/usr/bin/python3 -m venv \"$VENV_PATH\"\n\nsource \"$VENV_PATH/bin/activate\"\n\npip install --upgrade pip\npip install PyYAML\n\nscheme_mapping='{\n \"prod\": [\"ReleaseProd\", \"DebugProd\"],\n \"stage\": [\"ReleaseStage\", \"DebugStage\"],\n \"dev\": [\"ReleaseDev\", \"DebugDev\"]\n}'\n\npython config_script/process_config.py \"$CONFIGURATION\" \"$scheme_mapping\"\n\ndeactivate\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -384,8 +424,8 @@ 0293A2032A6FCA590090A336 /* CorePersistence.swift in Sources */, 0770DE1E28D084E8006D8A5D /* AppAssembly.swift in Sources */, 025AD4AC2A6FB95C00AB8FA7 /* DatabaseManager.swift in Sources */, + 024E69202AEFC3FB00FA0B59 /* MainScreenViewModel.swift in Sources */, 0770DE2028D0858A006D8A5D /* Router.swift in Sources */, - 0727876D28D23312002E9142 /* Environment.swift in Sources */, 0293A2092A6FCDE50090A336 /* DashboardPersistence.swift in Sources */, 0770DE1728D080A1006D8A5D /* RouteController.swift in Sources */, 071009C928D1DB3F00344290 /* ScreenAssembly.swift in Sources */, @@ -509,14 +549,13 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.stage; PRODUCT_NAME = "$(TARGET_NAME) Stage"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -598,14 +637,13 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.stage; PRODUCT_NAME = "$(TARGET_NAME) Stage"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -693,14 +731,13 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.dev; PRODUCT_NAME = "$(TARGET_NAME) Dev"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -782,14 +819,13 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.dev; PRODUCT_NAME = "$(TARGET_NAME) Dev"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -931,14 +967,13 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -966,14 +1001,13 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1015,6 +1049,25 @@ defaultConfigurationName = ReleaseProd; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + BA3042772B1F7147009B64B7 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/AzureAD/microsoft-authentication-library-for-objc"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.19; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + BA3042782B1F7147009B64B7 /* MSAL */ = { + isa = XCSwiftPackageProductDependency; + package = BA3042772B1F7147009B64B7 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */; + productName = MSAL; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 07D5DA2928D075AA00752FD9 /* Project object */; } diff --git a/OpenEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate b/OpenEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index b59556e65..000000000 Binary files a/OpenEdX.xcodeproj/project.xcworkspace/xcuserdata/vchekyrta.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXDev.xcscheme b/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXDev.xcscheme index 3a38de2f5..cd0feecf1 100644 --- a/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXDev.xcscheme +++ b/OpenEdX.xcodeproj/xcshareddata/xcschemes/OpenEdXDev.xcscheme @@ -97,6 +97,26 @@ ReferencedContainer = "container:Profile/Profile.xcodeproj"> + + + + + + + + + + + + + + + + + + @@ -25,6 +28,9 @@ + + diff --git a/OpenEdX/AnalyticsManager.swift b/OpenEdX/AnalyticsManager.swift index 04a11b640..ecb74b036 100644 --- a/OpenEdX/AnalyticsManager.swift +++ b/OpenEdX/AnalyticsManager.swift @@ -27,8 +27,8 @@ class AnalyticsManager: AuthorizationAnalytics, Analytics.setUserID(id) } - public func userLogin(method: LoginMethod) { - logEvent(.userLogin, parameters: [Key.method: method.rawValue]) + public func userLogin(method: AuthMethod) { + logEvent(.userLogin, parameters: [Key.method: method.analyticsValue]) } public func signUpClicked() { @@ -257,6 +257,14 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(.courseOutlineVideosTabClicked, parameters: parameters) } + public func courseOutlineDatesTabClicked(courseId: String, courseName: String) { + let parameters = [ + Key.courseID: courseId, + Key.courseName: courseName + ] + logEvent(.courseOutlineDatesTabClicked, parameters: parameters) + } + public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) { let parameters = [ Key.courseID: courseId, @@ -360,6 +368,7 @@ enum Event: String { case finishVerticalBackToOutlineClicked = "Finish_Vertical_Back_to_outline_Clicked" case courseOutlineCourseTabClicked = "Course_Outline_Course_tab_Clicked" case courseOutlineVideosTabClicked = "Course_Outline_Videos_tab_Clicked" + case courseOutlineDatesTabClicked = "Course_Outline_Dates_tab_Clicked" case courseOutlineDiscussionTabClicked = "Course_Outline_Discussion_tab_Clicked" case courseOutlineHandoutsTabClicked = "Course_Outline_Handouts_tab_Clicked" diff --git a/OpenEdX/AppDelegate.swift b/OpenEdX/AppDelegate.swift index 3c3cad303..adca92809 100644 --- a/OpenEdX/AppDelegate.swift +++ b/OpenEdX/AppDelegate.swift @@ -12,6 +12,10 @@ import FirebaseCore import FirebaseAnalytics import FirebaseCrashlytics import Profile +import GoogleSignIn +import FacebookCore +import MSAL +import Theme @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -21,9 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } var window: UIWindow? - - private var orientationLock: UIInterfaceOrientationMask = .portrait - + private var assembler: Assembler? private var lastForceLogoutTime: TimeInterval = 0 @@ -32,14 +34,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - - if BuildConfiguration.shared.firebaseOptions.apiKey != "" { - FirebaseApp.configure(options: BuildConfiguration.shared.firebaseOptions) - Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true) - } - initDI() + if let config = Container.shared.resolve(ConfigProtocol.self) { + Theme.Shapes.isRoundedCorners = config.theme.isRoundedCorners + if let configuration = config.firebase.firebaseOptions { + FirebaseApp.configure(options: configuration) + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true) + } + if config.facebook.enabled { + ApplicationDelegate.shared.application( + application, + didFinishLaunchingWithOptions: launchOptions + ) + } + } + Theme.Fonts.registerFonts() window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = RouteController() @@ -54,19 +64,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - + func application( - _ application: UIApplication, - supportedInterfaceOrientationsFor window: UIWindow? - ) -> UIInterfaceOrientationMask { - //Allows external windows, such as WebView Player, to work in any orientation - if window == self.window { - return UIDevice.current.userInterfaceIdiom == .phone ? orientationLock : .all - } else { - return UIDevice.current.userInterfaceIdiom == .phone ? .allButUpsideDown : .all + _ app: UIApplication, + open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + if let config = Container.shared.resolve(ConfigProtocol.self) { + if config.facebook.enabled { + ApplicationDelegate.shared.application( + app, + open: url, + sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String, + annotation: options[UIApplication.OpenURLOptionsKey.annotation] + ) + } + + if config.google.enabled { + return GIDSignIn.sharedInstance.handle(url) + } + + if config.microsoft.enabled { + return MSALPublicClientApplication.handleMSALResponse( + url, + sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String + ) + } } + + return false } - + private func initDI() { let navigation = UINavigationController() navigation.modalPresentationStyle = .fullScreen @@ -91,7 +118,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { lastForceLogoutTime = Date().timeIntervalSince1970 Container.shared.resolve(CoreStorage.self)?.clear() - Container.shared.resolve(DownloadManagerProtocol.self)?.deleteAllFiles() + Task { + await Container.shared.resolve(DownloadManagerProtocol.self)?.deleteAllFiles() + } Container.shared.resolve(CoreDataHandlerProtocol.self)?.clear() window?.rootViewController = RouteController() } diff --git a/OpenEdX/Base.lproj/LaunchScreen.storyboard b/OpenEdX/Base.lproj/LaunchScreen.storyboard index 7bc7cbbed..5cb3986ad 100644 --- a/OpenEdX/Base.lproj/LaunchScreen.storyboard +++ b/OpenEdX/Base.lproj/LaunchScreen.storyboard @@ -1,9 +1,9 @@ - + - + @@ -26,7 +26,7 @@ - + @@ -39,8 +39,8 @@ - - + + diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index edc74bd1f..8015abff3 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -15,6 +15,7 @@ import Course import Discussion import Authorization import Profile +import WhatsNew // swiftlint:disable function_body_length class AppAssembly: Assembly { @@ -112,12 +113,18 @@ class AppAssembly: Assembly { r.resolve(Router.self)! }.inObjectScope(.container) - container.register(Config.self) { _ in - Config(baseURL: BuildConfiguration.shared.baseURL, oAuthClientId: BuildConfiguration.shared.clientId) + container.register(WhatsNewRouter.self) { r in + r.resolve(Router.self)! }.inObjectScope(.container) - container.register(CSSInjector.self) { _ in - CSSInjector(baseURL: BuildConfiguration.shared.baseURL) + container.register(ConfigProtocol.self) { _ in + Config() + }.inObjectScope(.container) + + container.register(CSSInjector.self) { r in + CSSInjector( + config: r.resolve(ConfigProtocol.self)! + ) }.inObjectScope(.container) container.register(KeychainSwift.self) { _ in @@ -139,6 +146,14 @@ class AppAssembly: Assembly { r.resolve(AppStorage.self)! }.inObjectScope(.container) + container.register(WhatsNewStorage.self) { r in + r.resolve(AppStorage.self)! + }.inObjectScope(.container) + + container.register(CourseStorage.self) { r in + r.resolve(AppStorage.self)! + }.inObjectScope(.container) + container.register(ProfileStorage.self) { r in r.resolve(AppStorage.self)! }.inObjectScope(.container) diff --git a/OpenEdX/DI/NetworkAssembly.swift b/OpenEdX/DI/NetworkAssembly.swift index 1f036a860..83537fb29 100644 --- a/OpenEdX/DI/NetworkAssembly.swift +++ b/OpenEdX/DI/NetworkAssembly.swift @@ -13,7 +13,7 @@ import Swinject class NetworkAssembly: Assembly { func assemble(container: Container) { container.register(RequestInterceptor.self) { r in - RequestInterceptor(config: r.resolve(Config.self)!, storage: r.resolve(CoreStorage.self)!) + RequestInterceptor(config: r.resolve(ConfigProtocol.self)!, storage: r.resolve(CoreStorage.self)!) }.inObjectScope(.container) container.register(Alamofire.Session.self) { r in @@ -38,7 +38,7 @@ class NetworkAssembly: Assembly { }.inObjectScope(.container) container.register(API.self) {r in - API(session: r.resolve(Alamofire.Session.self)!, config: r.resolve(Config.self)!) + API(session: r.resolve(Alamofire.Session.self)!, config: r.resolve(ConfigProtocol.self)!) }.inObjectScope(.container) } } diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 1dd9ddddb..9c61fc679 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -24,7 +24,7 @@ class ScreenAssembly: Assembly { AuthRepository( api: r.resolve(API.self)!, appStorage: r.resolve(CoreStorage.self)!, - config: r.resolve(Config.self)! + config: r.resolve(ConfigProtocol.self)! ) } container.register(AuthInteractorProtocol.self) { r in @@ -33,23 +33,42 @@ class ScreenAssembly: Assembly { ) } + // MARK: MainScreenView + container.register(MainScreenViewModel.self) { r, sourceScreen in + MainScreenViewModel( + analytics: r.resolve(MainScreenAnalytics.self)!, + config: r.resolve(ConfigProtocol.self)!, + profileInteractor: r.resolve(ProfileInteractorProtocol.self)!, + sourceScreen: sourceScreen + ) + } + // MARK: Startup screen + container.register(StartupViewModel.self) { r in + StartupViewModel( + router: r.resolve(AuthorizationRouter.self)! + ) + } + // MARK: SignIn - container.register(SignInViewModel.self) { r in + container.register(SignInViewModel.self) { r, sourceScreen in SignInViewModel( interactor: r.resolve(AuthInteractorProtocol.self)!, router: r.resolve(AuthorizationRouter.self)!, + config: r.resolve(ConfigProtocol.self)!, analytics: r.resolve(AuthorizationAnalytics.self)!, - validator: r.resolve(Validator.self)! + validator: r.resolve(Validator.self)!, + sourceScreen: sourceScreen ) } - container.register(SignUpViewModel.self) { r in + container.register(SignUpViewModel.self) { r, sourceScreen in SignUpViewModel( interactor: r.resolve(AuthInteractorProtocol.self)!, router: r.resolve(AuthorizationRouter.self)!, analytics: r.resolve(AuthorizationAnalytics.self)!, - config: r.resolve(Config.self)!, + config: r.resolve(ConfigProtocol.self)!, cssInjector: r.resolve(CSSInjector.self)!, - validator: r.resolve(Validator.self)! + validator: r.resolve(Validator.self)!, + sourceScreen: sourceScreen ) } container.register(ResetPasswordViewModel.self) { r in @@ -70,7 +89,7 @@ class ScreenAssembly: Assembly { DiscoveryRepository( api: r.resolve(API.self)!, appStorage: r.resolve(CoreStorage.self)!, - config: r.resolve(Config.self)!, + config: r.resolve(ConfigProtocol.self)!, persistence: r.resolve(DiscoveryPersistenceProtocol.self)! ) } @@ -81,9 +100,35 @@ class ScreenAssembly: Assembly { } container.register(DiscoveryViewModel.self) { r in DiscoveryViewModel( + router: r.resolve(DiscoveryRouter.self)!, + config: r.resolve(ConfigProtocol.self)!, + interactor: r.resolve(DiscoveryInteractorProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + analytics: r.resolve(DiscoveryAnalytics.self)!, + storage: r.resolve(CoreStorage.self)! + ) + } + + container.register(DiscoveryWebviewViewModel.self) { r, sourceScreen in + DiscoveryWebviewViewModel( + router: r.resolve(DiscoveryRouter.self)!, + config: r.resolve(ConfigProtocol.self)!, + interactor: r.resolve(DiscoveryInteractorProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + analytics: r.resolve(DiscoveryAnalytics.self)!, + storage: r.resolve(CoreStorage.self)!, + sourceScreen: sourceScreen + ) + } + + container.register(ProgramWebviewViewModel.self) { r in + ProgramWebviewViewModel( + router: r.resolve(DiscoveryRouter.self)!, + config: r.resolve(ConfigProtocol.self)!, interactor: r.resolve(DiscoveryInteractorProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, - analytics: r.resolve(DiscoveryAnalytics.self)! + analytics: r.resolve(DiscoveryAnalytics.self)!, + authInteractor: r.resolve(AuthInteractorProtocol.self)! ) } @@ -106,7 +151,7 @@ class ScreenAssembly: Assembly { DashboardRepository( api: r.resolve(API.self)!, storage: r.resolve(CoreStorage.self)!, - config: r.resolve(Config.self)!, + config: r.resolve(ConfigProtocol.self)!, persistence: r.resolve(DashboardPersistenceProtocol.self)! ) } @@ -131,27 +176,27 @@ class ScreenAssembly: Assembly { storage: r.resolve(AppStorage.self)!, coreDataHandler: r.resolve(CoreDataHandlerProtocol.self)!, downloadManager: r.resolve(DownloadManagerProtocol.self)!, - config: r.resolve(Config.self)! + config: r.resolve(ConfigProtocol.self)! ) } - container.register(ProfileInteractor.self) { r in + container.register(ProfileInteractorProtocol.self) { r in ProfileInteractor( repository: r.resolve(ProfileRepositoryProtocol.self)! ) } container.register(ProfileViewModel.self) { r in ProfileViewModel( - interactor: r.resolve(ProfileInteractor.self)!, + interactor: r.resolve(ProfileInteractorProtocol.self)!, router: r.resolve(ProfileRouter.self)!, analytics: r.resolve(ProfileAnalytics.self)!, - config: r.resolve(Config.self)!, + config: r.resolve(ConfigProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)! ) } container.register(EditProfileViewModel.self) { r, userModel in EditProfileViewModel( userModel: userModel, - interactor: r.resolve(ProfileInteractor.self)!, + interactor: r.resolve(ProfileInteractorProtocol.self)!, router: r.resolve(ProfileRouter.self)!, analytics: r.resolve(ProfileAnalytics.self)! @@ -160,14 +205,14 @@ class ScreenAssembly: Assembly { container.register(SettingsViewModel.self) { r in SettingsViewModel( - interactor: r.resolve(ProfileInteractor.self)!, + interactor: r.resolve(ProfileInteractorProtocol.self)!, router: r.resolve(ProfileRouter.self)! ) } container.register(DeleteAccountViewModel.self) { r in DeleteAccountViewModel( - interactor: r.resolve(ProfileInteractor.self)!, + interactor: r.resolve(ProfileInteractorProtocol.self)!, router: r.resolve(ProfileRouter.self)!, connectivity: r.resolve(ConnectivityProtocol.self)! ) @@ -181,8 +226,8 @@ class ScreenAssembly: Assembly { container.register(CourseRepositoryProtocol.self) { r in CourseRepository( api: r.resolve(API.self)!, - appStorage: r.resolve(CoreStorage.self)!, - config: r.resolve(Config.self)!, + coreStorage: r.resolve(CoreStorage.self)!, + config: r.resolve(ConfigProtocol.self)!, persistence: r.resolve(CoursePersistenceProtocol.self)! ) } @@ -193,12 +238,13 @@ class ScreenAssembly: Assembly { } container.register(CourseDetailsViewModel.self) { r in CourseDetailsViewModel( - interactor: r.resolve(CourseInteractorProtocol.self)!, - router: r.resolve(CourseRouter.self)!, - analytics: r.resolve(CourseAnalytics.self)!, - config: r.resolve(Config.self)!, + interactor: r.resolve(DiscoveryInteractorProtocol.self)!, + router: r.resolve(DiscoveryRouter.self)!, + analytics: r.resolve(DiscoveryAnalytics.self)!, + config: r.resolve(ConfigProtocol.self)!, cssInjector: r.resolve(CSSInjector.self)!, - connectivity: r.resolve(ConnectivityProtocol.self)! + connectivity: r.resolve(ConnectivityProtocol.self)!, + storage: r.resolve(CoreStorage.self)! ) } @@ -211,9 +257,10 @@ class ScreenAssembly: Assembly { authInteractor: r.resolve(AuthInteractorProtocol.self)!, router: r.resolve(CourseRouter.self)!, analytics: r.resolve(CourseAnalytics.self)!, - config: r.resolve(Config.self)!, + config: r.resolve(ConfigProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, manager: r.resolve(DownloadManagerProtocol.self)!, + storage: r.resolve(CourseStorage.self)!, isActive: isActive, courseStart: courseStart, courseEnd: courseEnd, @@ -246,16 +293,18 @@ class ScreenAssembly: Assembly { sequentialIndex: sequentialIndex, verticalIndex: verticalIndex, interactor: r.resolve(CourseInteractorProtocol.self)!, + config: r.resolve(ConfigProtocol.self)!, router: r.resolve(CourseRouter.self)!, analytics: r.resolve(CourseAnalytics.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, + storage: r.resolve(CourseStorage.self)!, manager: r.resolve(DownloadManagerProtocol.self)! ) } container.register(WebUnitViewModel.self) { r in WebUnitViewModel(authInteractor: r.resolve(AuthInteractorProtocol.self)!, - config: r.resolve(Config.self)!) + config: r.resolve(ConfigProtocol.self)!) } container.register( @@ -269,6 +318,7 @@ class ScreenAssembly: Assembly { playerStateSubject: playerStateSubject, interactor: r.resolve(CourseInteractorProtocol.self)!, router: r.resolve(CourseRouter.self)!, + appStorage: r.resolve(CoreStorage.self)!, connectivity: r.resolve(ConnectivityProtocol.self)! ) } @@ -283,7 +333,8 @@ class ScreenAssembly: Assembly { languages: languages, playerStateSubject: playerStateSubject, interactor: r.resolve(CourseInteractorProtocol.self)!, - router: r.resolve(CourseRouter.self)!, + router: r.resolve(CourseRouter.self)!, + appStorage: r.resolve(CoreStorage.self)!, connectivity: r.resolve(ConnectivityProtocol.self)! ) } @@ -298,12 +349,21 @@ class ScreenAssembly: Assembly { ) } + container.register(CourseDatesViewModel.self) { r, courseID in + CourseDatesViewModel( + interactor: r.resolve(CourseInteractorProtocol.self)!, + router: r.resolve(CourseRouter.self)!, + cssInjector: r.resolve(CSSInjector.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + courseID: courseID) + } + // MARK: Discussion container.register(DiscussionRepositoryProtocol.self) { r in DiscussionRepository( api: r.resolve(API.self)!, appStorage: r.resolve(CoreStorage.self)!, - config: r.resolve(Config.self)!, + config: r.resolve(ConfigProtocol.self)!, router: r.resolve(DiscussionRouter.self)! ) } @@ -320,7 +380,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, analytics: r.resolve(DiscussionAnalytics.self)!, - config: r.resolve(Config.self)! + config: r.resolve(ConfigProtocol.self)! ) } @@ -337,7 +397,7 @@ class ScreenAssembly: Assembly { PostsViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, - config: r.resolve(Config.self)! + config: r.resolve(ConfigProtocol.self)! ) } @@ -345,7 +405,7 @@ class ScreenAssembly: Assembly { ThreadViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, - config: r.resolve(Config.self)!, + config: r.resolve(ConfigProtocol.self)!, postStateSubject: subject ) } @@ -354,7 +414,7 @@ class ScreenAssembly: Assembly { ResponsesViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, - config: r.resolve(Config.self)!, + config: r.resolve(ConfigProtocol.self)!, threadStateSubject: subject ) } @@ -363,7 +423,7 @@ class ScreenAssembly: Assembly { CreateNewThreadViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, - config: r.resolve(Config.self)! + config: r.resolve(ConfigProtocol.self)! ) } } diff --git a/OpenEdX/Data/AppStorage.swift b/OpenEdX/Data/AppStorage.swift index 99144be00..ff17e4128 100644 --- a/OpenEdX/Data/AppStorage.swift +++ b/OpenEdX/Data/AppStorage.swift @@ -9,8 +9,10 @@ import Foundation import KeychainSwift import Core import Profile +import WhatsNew +import Course -public class AppStorage: CoreStorage, ProfileStorage { +public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseStorage { private let keychain: KeychainSwift private let userDefaults: UserDefaults @@ -32,6 +34,35 @@ public class AppStorage: CoreStorage, ProfileStorage { } } } + + public var reviewLastShownVersion: String? { + get { + return userDefaults.string(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + } else { + userDefaults.removeObject(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + } + } + } + + public var lastReviewDate: Date? { + get { + guard let dateString = userDefaults.string(forKey: KEY_REVIEW_LAST_REVIEW_DATE) else { + return nil + } + return Date(iso8601: dateString) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue.dateToString(style: .iso8601), forKey: KEY_REVIEW_LAST_REVIEW_DATE) + } else { + userDefaults.removeObject(forKey: KEY_REVIEW_LAST_REVIEW_DATE) + } + } + } public var refreshToken: String? { get { @@ -46,6 +77,32 @@ public class AppStorage: CoreStorage, ProfileStorage { } } + public var appleSignFullName: String? { + get { + return keychain.get(KEY_APPLE_SIGN_FULLNAME) + } + set(newValue) { + if let newValue { + keychain.set(newValue, forKey: KEY_APPLE_SIGN_FULLNAME) + } else { + keychain.delete(KEY_APPLE_SIGN_FULLNAME) + } + } + } + + public var appleSignEmail: String? { + get { + return keychain.get(KEY_APPLE_SIGN_EMAIL) + } + set(newValue) { + if let newValue { + keychain.set(newValue, forKey: KEY_APPLE_SIGN_EMAIL) + } else { + keychain.delete(KEY_APPLE_SIGN_EMAIL) + } + } + } + public var cookiesDate: String? { get { return userDefaults.string(forKey: KEY_COOKIES_DATE) @@ -58,6 +115,19 @@ public class AppStorage: CoreStorage, ProfileStorage { } } } + + public var whatsNewVersion: String? { + get { + return userDefaults.string(forKey: KEY_WHATSNEW_VERSION) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_WHATSNEW_VERSION) + } else { + userDefaults.removeObject(forKey: KEY_WHATSNEW_VERSION) + } + } + } public var userProfile: DataLayer.UserProfile? { get { @@ -81,7 +151,7 @@ public class AppStorage: CoreStorage, ProfileStorage { public var userSettings: UserSettings? { get { guard let userSettings = userDefaults.data(forKey: KEY_SETTINGS) else { - let defaultSettings = UserSettings(wifiOnly: true, downloadQuality: .auto) + let defaultSettings = UserSettings(wifiOnly: true, streamingQuality: .auto, downloadQuality: .auto) let encoder = JSONEncoder() if let encoded = try? encoder.encode(defaultSettings) { userDefaults.set(encoded, forKey: KEY_SETTINGS) @@ -121,6 +191,19 @@ public class AppStorage: CoreStorage, ProfileStorage { } } + public var allowedDownloadLargeFile: Bool? { + get { + return userDefaults.bool(forKey: KEY_ALLOWED_DOWNLOAD_LARGE_FILE) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_ALLOWED_DOWNLOAD_LARGE_FILE) + } else { + userDefaults.removeObject(forKey: KEY_ALLOWED_DOWNLOAD_LARGE_FILE) + } + } + } + public func clear() { accessToken = nil refreshToken = nil @@ -134,4 +217,10 @@ public class AppStorage: CoreStorage, ProfileStorage { private let KEY_USER_PROFILE = "userProfile" private let KEY_USER = "refreshToken" private let KEY_SETTINGS = "userSettings" + private let KEY_REVIEW_LAST_SHOWN_VERSION = "reviewLastShownVersion" + private let KEY_REVIEW_LAST_REVIEW_DATE = "lastReviewDate" + private let KEY_WHATSNEW_VERSION = "whatsNewVersion" + private let KEY_APPLE_SIGN_FULLNAME = "appleSignFullName" + private let KEY_APPLE_SIGN_EMAIL = "appleSignEmail" + private let KEY_ALLOWED_DOWNLOAD_LARGE_FILE = "allowedDownloadLargeFile" } diff --git a/OpenEdX/Data/CorePersistence.swift b/OpenEdX/Data/CorePersistence.swift index 75b136eb5..1070011dd 100644 --- a/OpenEdX/Data/CorePersistence.swift +++ b/OpenEdX/Data/CorePersistence.swift @@ -11,59 +11,68 @@ import CoreData import Combine public class CorePersistence: CorePersistenceProtocol { - + private var context: NSManagedObjectContext - + public init(context: NSManagedObjectContext) { self.context = context } - + public func publisher() -> AnyPublisher { let notification = NSManagedObjectContext.didChangeObjectsNotification return NotificationCenter.default.publisher(for: notification, object: context) .compactMap({ notification in guard let userInfo = notification.userInfo else { return nil } - + if let inserts = userInfo[NSInsertedObjectsKey] as? Set, inserts.count > 0 { return inserts.count } - + if let updates = userInfo[NSUpdatedObjectsKey] as? Set, updates.count > 0 { return updates.count } - + if let deletes = userInfo[NSDeletedObjectsKey] as? Set, deletes.count > 0 { return deletes.count } - + return nil }) .eraseToAnyPublisher() } - - public func getAllDownloadData() -> [DownloadData] { - let request = CDDownloadData.fetchRequest() - guard let downloadData = try? context.fetch(request) else { return [] } - return downloadData.map { - DownloadData( - id: $0.id ?? "", - courseId: $0.courseId ?? "", - url: $0.url ?? "", - fileName: $0.fileName ?? "", - progress: $0.progress, - resumeData: $0.resumeData, - state: DownloadState(rawValue: $0.state ?? "") ?? .waiting, - type: DownloadType(rawValue: $0.type ?? "") ?? .video - ) + + public func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void) { + context.performAndWait { + let request = CDDownloadData.fetchRequest() + guard let downloadData = try? context.fetch(request) else { + completion([]) + return + } + let downloads = downloadData.map { + DownloadDataTask( + id: $0.id ?? "", + courseId: $0.courseId ?? "", + url: $0.url ?? "", + fileName: $0.fileName ?? "", + displayName: $0.displayName ?? "", + progress: $0.progress, + resumeData: $0.resumeData, + state: DownloadState(rawValue: $0.state ?? "") ?? .waiting, + type: DownloadType(rawValue: $0.type ?? "") ?? .video, + fileSize: Int($0.fileSize) + ) + } + completion(downloads) } } - - public func addToDownloadQueue(blocks: [CourseBlock]) { + + public func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) { for block in blocks { let request = CDDownloadData.fetchRequest() request.predicate = NSPredicate(format: "id = %@", block.id) guard (try? context.fetch(request).first) == nil else { continue } - guard let url = block.videoUrl, + guard let video = block.encodedVideo?.video(downloadQuality: downloadQuality), + let url = video.url, let fileExtension = URL(string: url)?.pathExtension else { continue } let fileName = "\(block.id).\(fileExtension)" @@ -74,96 +83,137 @@ public class CorePersistence: CorePersistenceProtocol { newDownloadData.courseId = block.courseId newDownloadData.url = url newDownloadData.fileName = fileName + newDownloadData.displayName = block.displayName newDownloadData.progress = .zero newDownloadData.resumeData = nil newDownloadData.state = DownloadState.waiting.rawValue newDownloadData.type = DownloadType.video.rawValue + newDownloadData.fileSize = Int32(video.fileSize ?? 0) } } } - - public func getNextBlockForDownloading() -> DownloadData? { + + public func getNextBlockForDownloading() -> DownloadDataTask? { let request = CDDownloadData.fetchRequest() request.predicate = NSPredicate(format: "state != %@", DownloadState.finished.rawValue) request.fetchLimit = 1 guard let data = try? context.fetch(request).first else { return nil } - return DownloadData( + return DownloadDataTask( id: data.id ?? "", courseId: data.courseId ?? "", url: data.url ?? "", fileName: data.fileName ?? "", + displayName: data.displayName ?? "", progress: data.progress, resumeData: data.resumeData, state: DownloadState(rawValue: data.state ?? "") ?? .waiting, - type: DownloadType(rawValue: data.type ?? "" ) ?? .video + type: DownloadType(rawValue: data.type ?? "" ) ?? .video, + fileSize: Int(data.fileSize) ) } - - public func getDownloadsForCourse(_ courseId: String) -> [DownloadData] { - let request = CDDownloadData.fetchRequest() - request.predicate = NSPredicate(format: "courseId = %@", courseId) - guard let downloadData = try? context.fetch(request) else { return [] } - return downloadData.map { - DownloadData( - id: $0.id ?? "", - courseId: $0.courseId ?? "", - url: $0.url ?? "", - fileName: $0.fileName ?? "", - progress: $0.progress, - resumeData: $0.resumeData, - state: DownloadState(rawValue: $0.state ?? "") ?? .waiting, - type: DownloadType(rawValue: $0.type ?? "") ?? .video + + public func getDownloadDataTasksForCourse(_ courseId: String, completion: @escaping ([DownloadDataTask]) -> Void) { + context.performAndWait { + let request = CDDownloadData.fetchRequest() + request.predicate = NSPredicate(format: "courseId = %@", courseId) + guard let downloadData = try? context.fetch(request) else { + completion([]) + return + } + let downloads = downloadData.map { + DownloadDataTask( + id: $0.id ?? "", + courseId: $0.courseId ?? "", + url: $0.url ?? "", + fileName: $0.fileName ?? "", + displayName: $0.displayName ?? "", + progress: $0.progress, + resumeData: $0.resumeData, + state: DownloadState(rawValue: $0.state ?? "") ?? .waiting, + type: DownloadType(rawValue: $0.type ?? "") ?? .video, + fileSize: Int($0.fileSize) + ) + } + completion(downloads) + } + } + + public func downloadDataTask(for blockId: String, completion: @escaping (DownloadDataTask?) -> Void) { + context.performAndWait { + let request = CDDownloadData.fetchRequest() + request.predicate = NSPredicate(format: "id = %@", blockId) + guard let downloadData = try? context.fetch(request).first else { + completion(nil) + return + } + let data = DownloadDataTask( + id: downloadData.id ?? "", + courseId: downloadData.courseId ?? "", + url: downloadData.url ?? "", + fileName: downloadData.fileName ?? "", + displayName: downloadData.displayName ?? "", + progress: downloadData.progress, + resumeData: downloadData.resumeData, + state: DownloadState(rawValue: downloadData.state ?? "") ?? .waiting, + type: DownloadType(rawValue: downloadData.type ?? "" ) ?? .video, + fileSize: Int(downloadData.fileSize) ) + completion(data) } } - - public func downloadData(by blockId: String) -> DownloadData? { + + public func downloadDataTask(for blockId: String) -> DownloadDataTask? { let request = CDDownloadData.fetchRequest() request.predicate = NSPredicate(format: "id = %@", blockId) guard let downloadData = try? context.fetch(request).first else { return nil } - return DownloadData( + return DownloadDataTask( id: downloadData.id ?? "", courseId: downloadData.courseId ?? "", url: downloadData.url ?? "", fileName: downloadData.fileName ?? "", + displayName: downloadData.displayName ?? "", progress: downloadData.progress, resumeData: downloadData.resumeData, - state: DownloadState(rawValue: downloadData.state ?? "") ?? .paused, - type: DownloadType(rawValue: downloadData.type ?? "" ) ?? .video + state: DownloadState(rawValue: downloadData.state ?? "") ?? .waiting, + type: DownloadType(rawValue: downloadData.type ?? "" ) ?? .video, + fileSize: Int(downloadData.fileSize) ) } - + public func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { context.performAndWait { let request = CDDownloadData.fetchRequest() request.predicate = NSPredicate(format: "id = %@", id) guard let downloadData = try? context.fetch(request).first else { return } downloadData.state = state.rawValue + if state == .finished { downloadData.progress = 1 } downloadData.resumeData = resumeData do { try context.save() } catch { - print("⛔️⛔️⛔️⛔️⛔️", error) + debugLog("⛔️⛔️⛔️⛔️⛔️", error) } } } - - public func deleteDownloadData(id: String) throws { - let request = CDDownloadData.fetchRequest() - request.predicate = NSPredicate(format: "id = %@", id) - do { - let records = try context.fetch(request) - for record in records { - context.delete(record) - try context.save() - print("File erased successfully") + + public func deleteDownloadDataTask(id: String) throws { + context.performAndWait { + let request = CDDownloadData.fetchRequest() + request.predicate = NSPredicate(format: "id = %@", id) + do { + let records = try context.fetch(request) + for record in records { + context.delete(record) + try context.save() + debugLog("File erased successfully") + } + } catch { + debugLog("Error fetching records: \(error.localizedDescription)") } - } catch { - print("Error fetching records: \(error.localizedDescription)") } } - public func saveDownloadData(data: DownloadData) { + public func saveDownloadDataTask(data: DownloadDataTask) { context.performAndWait { let newDownloadData = CDDownloadData(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump @@ -174,11 +224,12 @@ public class CorePersistence: CorePersistenceProtocol { newDownloadData.fileName = data.fileName newDownloadData.resumeData = data.resumeData newDownloadData.state = data.state.rawValue - + newDownloadData.fileSize = Int32(data.fileSize) + do { try context.save() } catch { - print("⛔️⛔️⛔️⛔️⛔️", error) + debugLog("⛔️⛔️⛔️⛔️⛔️", error) } } } diff --git a/OpenEdX/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift index cd61e5e6e..90d9470e6 100644 --- a/OpenEdX/Data/CoursePersistence.swift +++ b/OpenEdX/Data/CoursePersistence.swift @@ -18,47 +18,6 @@ public class CoursePersistence: CoursePersistenceProtocol { self.context = context } - public func loadCourseDetails(courseID: String) throws -> CourseDetails { - let request = CDCourseDetails.fetchRequest() - request.predicate = NSPredicate(format: "courseID = %@", courseID) - guard let courseDetails = try? context.fetch(request).first else { throw NoCachedDataError() } - return CourseDetails(courseID: courseDetails.courseID ?? "", - org: courseDetails.org ?? "", - courseTitle: courseDetails.courseTitle ?? "", - courseDescription: courseDetails.courseDescription ?? "", - courseStart: courseDetails.courseStart, - courseEnd: courseDetails.courseEnd, - enrollmentStart: courseDetails.enrollmentStart, - enrollmentEnd: courseDetails.enrollmentEnd, - isEnrolled: courseDetails.isEnrolled, - overviewHTML: courseDetails.overviewHTML ?? "", - courseBannerURL: courseDetails.courseBannerURL ?? "", - courseVideoURL: nil) - } - - public func saveCourseDetails(course: CourseDetails) { - context.performAndWait { - let newCourseDetails = CDCourseDetails(context: self.context) - newCourseDetails.courseID = course.courseID - newCourseDetails.org = course.org - newCourseDetails.courseTitle = course.courseTitle - newCourseDetails.courseDescription = course.courseDescription - newCourseDetails.courseStart = course.courseStart - newCourseDetails.courseEnd = course.courseEnd - newCourseDetails.enrollmentStart = course.enrollmentStart - newCourseDetails.enrollmentEnd = course.enrollmentEnd - newCourseDetails.isEnrolled = course.isEnrolled - newCourseDetails.overviewHTML = course.overviewHTML - newCourseDetails.courseBannerURL = course.courseBannerURL - - do { - try context.save() - } catch { - print("⛔️⛔️⛔️⛔️⛔️", error) - } - } - } - public func loadEnrollments() throws -> [CourseItem] { let result = try? context.fetch(CDCourseItem.fetchRequest()) .map { @@ -116,24 +75,50 @@ public class CoursePersistence: CoursePersistenceProtocol { let requestBlocks = CDCourseBlock.fetchRequest() requestBlocks.predicate = NSPredicate(format: "courseID = %@", courseID) - + let blocks = try? context.fetch(requestBlocks).map { let userViewData = DataLayer.CourseDetailUserViewData( transcripts: nil, encodedVideo: DataLayer.CourseDetailEncodedVideoData( - youTube: DataLayer.CourseDetailYouTubeData(url: $0.youTubeUrl), - fallback: DataLayer.CourseDetailYouTubeData(url: $0.fallbackUrl) - ), topicID: "") - return DataLayer.CourseBlock(blockId: $0.blockId ?? "", - id: $0.id ?? "", - graded: $0.graded, - completion: $0.completion, - studentUrl: $0.studentUrl ?? "", - type: $0.type ?? "", - displayName: $0.displayName ?? "", - descendants: $0.descendants, - allSources: $0.allSources, - userViewData: userViewData) + youTube: DataLayer.EncodedVideoData( + url: $0.youTube?.url, + fileSize: Int($0.youTube?.fileSize ?? 0) + ), + fallback: DataLayer.EncodedVideoData( + url: $0.fallback?.url, + fileSize: Int($0.fallback?.fileSize ?? 0) + ), + desktopMP4: DataLayer.EncodedVideoData( + url: $0.desktopMP4?.url, + fileSize: Int($0.desktopMP4?.fileSize ?? 0) + ), + mobileHigh: DataLayer.EncodedVideoData( + url: $0.mobileHigh?.url, + fileSize: Int($0.mobileHigh?.fileSize ?? 0) + ), + mobileLow: DataLayer.EncodedVideoData( + url: $0.mobileLow?.url, + fileSize: Int($0.mobileLow?.fileSize ?? 0) + ), + hls: DataLayer.EncodedVideoData( + url: $0.hls?.url, + fileSize: Int($0.hls?.fileSize ?? 0) + ) + ), + topicID: "" + ) + return DataLayer.CourseBlock( + blockId: $0.blockId ?? "", + id: $0.id ?? "", + graded: $0.graded, + completion: $0.completion, + studentUrl: $0.studentUrl ?? "", + type: $0.type ?? "", + displayName: $0.displayName ?? "", + descendants: $0.descendants, + allSources: $0.allSources, + userViewData: userViewData + ) } let dictionary = blocks?.reduce(into: [:]) { result, block in @@ -176,10 +161,56 @@ public class CoursePersistence: CoursePersistenceProtocol { courseDetail.id = block.id courseDetail.studentUrl = block.studentUrl courseDetail.type = block.type - courseDetail.youTubeUrl = block.userViewData?.encodedVideo?.youTube?.url - courseDetail.fallbackUrl = block.userViewData?.encodedVideo?.fallback?.url courseDetail.completion = block.completion ?? 0 - + + if block.userViewData?.encodedVideo?.youTube != nil { + let youTube = CDCourseBlockVideo(context: self.context) + youTube.url = block.userViewData?.encodedVideo?.youTube?.url + youTube.fileSize = Int32(block.userViewData?.encodedVideo?.youTube?.fileSize ?? 0) + youTube.streamPriority = Int32(block.userViewData?.encodedVideo?.youTube?.streamPriority ?? 0) + courseDetail.youTube = youTube + } + + if block.userViewData?.encodedVideo?.fallback != nil { + let fallback = CDCourseBlockVideo(context: self.context) + fallback.url = block.userViewData?.encodedVideo?.fallback?.url + fallback.fileSize = Int32(block.userViewData?.encodedVideo?.fallback?.fileSize ?? 0) + fallback.streamPriority = Int32(block.userViewData?.encodedVideo?.fallback?.streamPriority ?? 0) + courseDetail.fallback = fallback + } + + if block.userViewData?.encodedVideo?.desktopMP4 != nil { + let desktopMP4 = CDCourseBlockVideo(context: self.context) + desktopMP4.url = block.userViewData?.encodedVideo?.desktopMP4?.url + desktopMP4.fileSize = Int32(block.userViewData?.encodedVideo?.desktopMP4?.fileSize ?? 0) + desktopMP4.streamPriority = Int32(block.userViewData?.encodedVideo?.desktopMP4?.streamPriority ?? 0) + courseDetail.desktopMP4 = desktopMP4 + } + + if block.userViewData?.encodedVideo?.mobileHigh != nil { + let mobileHigh = CDCourseBlockVideo(context: self.context) + mobileHigh.url = block.userViewData?.encodedVideo?.mobileHigh?.url + mobileHigh.fileSize = Int32(block.userViewData?.encodedVideo?.mobileHigh?.fileSize ?? 0) + mobileHigh.streamPriority = Int32(block.userViewData?.encodedVideo?.mobileHigh?.streamPriority ?? 0) + courseDetail.mobileHigh = mobileHigh + } + + if block.userViewData?.encodedVideo?.mobileLow != nil { + let mobileLow = CDCourseBlockVideo(context: self.context) + mobileLow.url = block.userViewData?.encodedVideo?.mobileLow?.url + mobileLow.fileSize = Int32(block.userViewData?.encodedVideo?.mobileLow?.fileSize ?? 0) + mobileLow.streamPriority = Int32(block.userViewData?.encodedVideo?.mobileLow?.streamPriority ?? 0) + courseDetail.mobileLow = mobileLow + } + + if block.userViewData?.encodedVideo?.hls != nil { + let hls = CDCourseBlockVideo(context: self.context) + hls.url = block.userViewData?.encodedVideo?.hls?.url + hls.fileSize = Int32(block.userViewData?.encodedVideo?.hls?.fileSize ?? 0) + hls.streamPriority = Int32(block.userViewData?.encodedVideo?.hls?.streamPriority ?? 0) + courseDetail.hls = hls + } + do { try context.save() } catch { @@ -215,4 +246,12 @@ public class CoursePersistence: CoursePersistenceProtocol { } return nil } + + public func saveCourseDates(courseID: String, courseDates: CourseDates) { + + } + + public func loadCourseDates(courseID: String) throws -> CourseDates { + throw NoCachedDataError() + } } diff --git a/OpenEdX/Data/DiscoveryPersistence.swift b/OpenEdX/Data/DiscoveryPersistence.swift index 7547d0c37..189264f41 100644 --- a/OpenEdX/Data/DiscoveryPersistence.swift +++ b/OpenEdX/Data/DiscoveryPersistence.swift @@ -66,4 +66,47 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { } } } + + public func loadCourseDetails(courseID: String) throws -> CourseDetails { + let request = CDCourseDetails.fetchRequest() + request.predicate = NSPredicate(format: "courseID = %@", courseID) + guard let courseDetails = try? context.fetch(request).first else { throw NoCachedDataError() } + return CourseDetails( + courseID: courseDetails.courseID ?? "", + org: courseDetails.org ?? "", + courseTitle: courseDetails.courseTitle ?? "", + courseDescription: courseDetails.courseDescription ?? "", + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, + isEnrolled: courseDetails.isEnrolled, + overviewHTML: courseDetails.overviewHTML ?? "", + courseBannerURL: courseDetails.courseBannerURL ?? "", + courseVideoURL: nil + ) + } + + public func saveCourseDetails(course: CourseDetails) { + context.performAndWait { + let newCourseDetails = CDCourseDetails(context: self.context) + newCourseDetails.courseID = course.courseID + newCourseDetails.org = course.org + newCourseDetails.courseTitle = course.courseTitle + newCourseDetails.courseDescription = course.courseDescription + newCourseDetails.courseStart = course.courseStart + newCourseDetails.courseEnd = course.courseEnd + newCourseDetails.enrollmentStart = course.enrollmentStart + newCourseDetails.enrollmentEnd = course.enrollmentEnd + newCourseDetails.isEnrolled = course.isEnrolled + newCourseDetails.overviewHTML = course.overviewHTML + newCourseDetails.courseBannerURL = course.courseBannerURL + + do { + try context.save() + } catch { + print("⛔️⛔️⛔️⛔️⛔️", error) + } + } + } } diff --git a/OpenEdX/Environment.swift b/OpenEdX/Environment.swift deleted file mode 100644 index e89c0bb88..000000000 --- a/OpenEdX/Environment.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// Environment.swift -// OpenEdX -// -// Created by Vladimir Chekyrta on 14.09.2022. -// - -import Foundation -import Core -import FirebaseCore - -enum `Environment`: String { - case debugDev = "DebugDev" - case releaseDev = "ReleaseDev" - - case debugStage = "DebugStage" - case releaseStage = "ReleaseStage" - - case debugProd = "DebugProd" - case releaseProd = "ReleaseProd" -} - -class BuildConfiguration { - static let shared = BuildConfiguration() - - var environment: Environment - - var baseURL: String { - switch environment { - case .debugDev, .releaseDev: - return "https://example-dev.com" - case .debugStage, .releaseStage: - return "https://example-stage.com" - case .debugProd, .releaseProd: - return "https://example.com" - } - } - - var clientId: String { - switch environment { - case .debugDev, .releaseDev: - return "DEV_CLIENT_ID" - case .debugStage, .releaseStage: - return "STAGE_CLIENT_ID" - case .debugProd, .releaseProd: - return "PROD_CLIENT_ID" - } - } - - var firebaseOptions: FirebaseOptions { - switch environment { - case .debugDev, .releaseDev: - let firebaseOptions = FirebaseOptions(googleAppID: "", - gcmSenderID: "") - firebaseOptions.apiKey = "" - firebaseOptions.projectID = "" - firebaseOptions.bundleID = "" - firebaseOptions.clientID = "" - firebaseOptions.storageBucket = "" - - return firebaseOptions - case .debugStage, .releaseStage: - let firebaseOptions = FirebaseOptions(googleAppID: "", - gcmSenderID: "") - firebaseOptions.apiKey = "" - firebaseOptions.projectID = "" - firebaseOptions.bundleID = "" - firebaseOptions.clientID = "" - firebaseOptions.storageBucket = "" - - return firebaseOptions - case .debugProd, .releaseProd: - let firebaseOptions = FirebaseOptions(googleAppID: "", - gcmSenderID: "") - firebaseOptions.apiKey = "" - firebaseOptions.projectID = "" - firebaseOptions.bundleID = "" - firebaseOptions.clientID = "" - firebaseOptions.storageBucket = "" - - return firebaseOptions - } - } - - init() { - let currentConfiguration = Bundle.main.object(forInfoDictionaryKey: "Configuration") as! String - environment = Environment(rawValue: currentConfiguration)! - } -} diff --git a/OpenEdX/Info.plist b/OpenEdX/Info.plist index b94522839..b74f3967c 100644 --- a/OpenEdX/Info.plist +++ b/OpenEdX/Info.plist @@ -10,6 +10,21 @@ ITSAppUsesNonExemptEncryption + LSApplicationQueriesSchemes + + googlegmail + readdle-spark + airmail + ms-outlook + ymail + fastmail + protonmail + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIAppFonts UIViewControllerBasedStatusBarAppearance diff --git a/OpenEdX/MainScreenAnalytics.swift b/OpenEdX/MainScreenAnalytics.swift index 39dd9e484..fc7bdb38c 100644 --- a/OpenEdX/MainScreenAnalytics.swift +++ b/OpenEdX/MainScreenAnalytics.swift @@ -14,3 +14,12 @@ public protocol MainScreenAnalytics { func mainProgramsTabClicked() func mainProfileTabClicked() } + +#if DEBUG +public class MainScreenAnalyticsMock: MainScreenAnalytics { + public func mainDiscoveryTabClicked() {} + public func mainDashboardTabClicked() {} + public func mainProgramsTabClicked() {} + public func mainProfileTabClicked() {} +} +#endif diff --git a/OpenEdX/OpenEdX.entitlements b/OpenEdX/OpenEdX.entitlements index 74c85d2ed..80b5221de 100644 --- a/OpenEdX/OpenEdX.entitlements +++ b/OpenEdX/OpenEdX.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.developer.applesignin + + Default + - \ No newline at end of file + diff --git a/OpenEdX/RouteController.swift b/OpenEdX/RouteController.swift index 1aa036dc1..70bae693c 100644 --- a/OpenEdX/RouteController.swift +++ b/OpenEdX/RouteController.swift @@ -9,6 +9,8 @@ import UIKit import SwiftUI import Core import Authorization +import WhatsNew +import Swinject class RouteController: UIViewController { @@ -30,26 +32,60 @@ class RouteController: UIViewController { if let user = appStorage.user, appStorage.accessToken != nil { analytics.setUserID("\(user.id)") DispatchQueue.main.async { - self.showMainScreen() + self.showMainOrWhatsNewScreen() } } else { DispatchQueue.main.async { - self.showAuthorization() + self.showStartupScreen() } } } - private func showAuthorization() { - let controller = UIHostingController( - rootView: SignInView(viewModel: diContainer.resolve(SignInViewModel.self)!) - ) - navigation.viewControllers = [controller] - present(navigation, animated: false) + private func showStartupScreen() { + if let config = Container.shared.resolve(ConfigProtocol.self), config.features.startupScreenEnabled { + let controller = UIHostingController( + rootView: StartupView(viewModel: diContainer.resolve(StartupViewModel.self)!)) + navigation.viewControllers = [controller] + present(navigation, animated: false) + } else { + let controller = UIHostingController( + rootView: SignInView( + viewModel: diContainer.resolve( + SignInViewModel.self, + argument: LogistrationSourceScreen.default + )! + ) + ) + navigation.viewControllers = [controller] + present(navigation, animated: false) + } } - private func showMainScreen() { - let controller = UIHostingController(rootView: MainScreenView()) - navigation.viewControllers = [controller] + private func showMainOrWhatsNewScreen() { + var storage = Container.shared.resolve(WhatsNewStorage.self)! + let config = Container.shared.resolve(ConfigProtocol.self)! + + let viewModel = WhatsNewViewModel(storage: storage) + let shouldShowWhatsNew = viewModel.shouldShowWhatsNew() + + if shouldShowWhatsNew && config.features.whatNewEnabled { + if let jsonVersion = viewModel.getVersion() { + storage.whatsNewVersion = jsonVersion + } + let whatsNewView = WhatsNewView( + router: Container.shared.resolve(WhatsNewRouter.self)!, + viewModel: viewModel + ) + let controller = UIHostingController(rootView: whatsNewView) + navigation.viewControllers = [controller] + } else { + let viewModel = Container.shared.resolve( + MainScreenViewModel.self, + argument: LogistrationSourceScreen.default + )! + let controller = UIHostingController(rootView: MainScreenView(viewModel: viewModel)) + navigation.viewControllers = [controller] + } present(navigation, animated: false) } } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 9be51948e..adc46d1f6 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -16,9 +16,11 @@ import Discussion import Discovery import Dashboard import Profile +import WhatsNew import Combine public class Router: AuthorizationRouter, + WhatsNewRouter, DiscoveryRouter, ProfileRouter, DashboardRouter, @@ -56,16 +58,72 @@ public class Router: AuthorizationRouter, navigationController.setViewControllers(viewControllers, animated: true) } - public func showMainScreen() { + public func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { showToolBar() - let controller = UIHostingController(rootView: MainScreenView()) - navigationController.setViewControllers([controller], animated: true) + var storage = Container.shared.resolve(WhatsNewStorage.self)! + let config = Container.shared.resolve(ConfigProtocol.self)! + + let viewModel = WhatsNewViewModel(storage: storage, sourceScreen: sourceScreen) + let whatsNew = WhatsNewView(router: Container.shared.resolve(WhatsNewRouter.self)!, viewModel: viewModel) + let shouldShowWhatsNew = viewModel.shouldShowWhatsNew() + + if shouldShowWhatsNew && config.features.whatNewEnabled { + if let jsonVersion = viewModel.getVersion() { + storage.whatsNewVersion = jsonVersion + } + let controller = UIHostingController(rootView: whatsNew) + navigationController.viewControllers = [controller] + navigationController.setViewControllers([controller], animated: true) + } else { + let viewModel = Container.shared.resolve( + MainScreenViewModel.self, + argument: sourceScreen + )! + + let controller = UIHostingController(rootView: MainScreenView(viewModel: viewModel)) + navigationController.viewControllers = [controller] + navigationController.setViewControllers([controller], animated: true) + } + } + + public func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + guard let viewModel = Container.shared.resolve( + SignInViewModel.self, + argument: sourceScreen + ) else { return } + + let view = SignInView(viewModel: viewModel) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) } - public func showLoginScreen() { - let view = SignInView(viewModel: Container.shared.resolve(SignInViewModel.self)!) - let controller = UIHostingController(rootView: view) - navigationController.setViewControllers([controller], animated: false) + public func showStartupScreen() { + if let config = Container.shared.resolve(ConfigProtocol.self), config.features.startupScreenEnabled { + let view = StartupView(viewModel: Container.shared.resolve(StartupViewModel.self)!) + let controller = UIHostingController(rootView: view) + navigationController.setViewControllers([controller], animated: true) + } else { + let view = SignInView( + viewModel: Container.shared.resolve( + SignInViewModel.self, + argument: LogistrationSourceScreen.default + )! + ) + let controller = UIHostingController(rootView: view) + navigationController.setViewControllers([controller], animated: false) + } + } + + public func presentAppReview() { + let config = Container.shared.resolve(ConfigProtocol.self)! + let storage = Container.shared.resolve(CoreStorage.self)! + let vm = AppReviewViewModel(config: config, storage: storage) + if vm.shouldShowRatingView() { + presentView( + transitionStyle: .crossDissolve, + view: AppReviewView(viewModel: vm) + ) + } } public func presentAlert( @@ -121,12 +179,21 @@ public class Router: AuthorizationRouter, navigationController.present(view, animated: true) } - public func showRegisterScreen() { - let view = SignUpView(viewModel: Container.shared.resolve(SignUpViewModel.self)!) + public func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + guard let viewModel = Container.shared.resolve( + SignUpViewModel.self, + argument: sourceScreen + ), let authAnalytics = Container.shared.resolve( + AuthorizationAnalytics.self + ) else { return } + + let view = SignUpView(viewModel: viewModel) let controller = UIHostingController(rootView: view) navigationController.pushViewController(controller, animated: true) + + authAnalytics.signUpClicked() } - + public func showForgotPasswordScreen() { let view = ResetPasswordView(viewModel: Container.shared.resolve(ResetPasswordViewModel.self)!) let controller = UIHostingController(rootView: view) @@ -143,14 +210,74 @@ public class Router: AuthorizationRouter, navigationController.pushViewController(controller, animated: true) } - public func showDiscoverySearch() { + public func showWebDiscoveryDetails( + pathID: String, + discoveryType: DiscoveryWebviewType, + sourceScreen: LogistrationSourceScreen + ) { + let view = DiscoveryWebview( + viewModel: Container.shared.resolve( + DiscoveryWebviewViewModel.self, + argument: sourceScreen)!, + router: Container.shared.resolve(DiscoveryRouter.self)!, + discoveryType: discoveryType, + pathID: pathID + ) + + DispatchQueue.main.async { [weak self] in + let controller = UIHostingController(rootView: view) + self?.navigationController.pushViewController(controller, animated: true) + } + } + + public func showWebProgramDetails( + pathID: String, + viewType: ProgramViewType + ) { + let view = ProgramWebviewView( + viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)!, + viewType: viewType, + pathID: pathID + ) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + + public func showDiscoverySearch(searchQuery: String? = nil) { let viewModel = Container.shared.resolve(SearchViewModel.self)! - let view = SearchView(viewModel: viewModel) + let view = SearchView(viewModel: viewModel, searchQuery: searchQuery) let controller = UIHostingController(rootView: view) navigationController.pushFade(viewController: controller) } + public func showDiscoveryScreen(searchQuery: String? = nil, sourceScreen: LogistrationSourceScreen) { + let config = Container.shared.resolve(ConfigProtocol.self) + if config?.discovery.type == .native { + let view = DiscoveryView( + viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)!, + searchQuery: searchQuery, + sourceScreen: sourceScreen + ) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } else if config?.discovery.type == .webview { + let view = DiscoveryWebview( + viewModel: Container.shared.resolve( + DiscoveryWebviewViewModel.self, + argument: sourceScreen + )!, + router: Container.shared.resolve(DiscoveryRouter.self)!, + searchQuery: searchQuery + ) + + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + } + public func showDiscussionsSearch(courseID: String) { let viewModel = Container.shared.resolve(DiscussionSearchTopicsViewModel.self, argument: courseID)! let view = DiscussionSearchTopicsView(viewModel: viewModel) @@ -247,11 +374,43 @@ public class Router: AuthorizationRouter, sequentialIndex, verticalIndex )! - let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName) + + let config = Container.shared.resolve(ConfigProtocol.self) + let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false + + let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName, isDropdownActive: isDropdownActive) let controller = UIHostingController(rootView: view) navigationController.pushViewController(controller, animated: true) } + public func showCourseComponent( + componentID: String, + courseStructure: CourseStructure) { + courseStructure.childs.enumerated().forEach { chapterIndex, chapter in + chapter.childs.enumerated().forEach { sequentialIndex, sequential in + sequential.childs.enumerated().forEach { verticalIndex, vertical in + vertical.childs.forEach { block in + if block.id == componentID { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.showCourseUnit( + courseName: courseStructure.displayName, + blockId: block.blockId, + courseID: courseStructure.id, + sectionName: sequential.displayName, + verticalIndex: verticalIndex, + chapters: courseStructure.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex) + } + return + } + } + } + } + } + } + public func replaceCourseUnit( courseName: String, blockId: String, @@ -260,7 +419,8 @@ public class Router: AuthorizationRouter, verticalIndex: Int, chapters: [CourseChapter], chapterIndex: Int, - sequentialIndex: Int + sequentialIndex: Int, + animated: Bool ) { let vmVertical = Container.shared.resolve( @@ -288,12 +448,24 @@ public class Router: AuthorizationRouter, sequentialIndex, verticalIndex )! - let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName) + + let config = Container.shared.resolve(ConfigProtocol.self) + let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false + + let view = CourseUnitView(viewModel: viewModel, sectionName: sectionName, isDropdownActive: isDropdownActive) let controllerUnit = UIHostingController(rootView: view) var controllers = navigationController.viewControllers - controllers.removeLast(2) - controllers.append(contentsOf: [controllerVertical, controllerUnit]) - navigationController.setViewControllers(controllers, animated: true) + + if let config = container.resolve(ConfigProtocol.self), + config.uiComponents.courseNestedListEnabled { + controllers.removeLast(1) + controllers.append(contentsOf: [controllerUnit]) + } else { + controllers.removeLast(2) + controllers.append(contentsOf: [controllerVertical, controllerUnit]) + } + + navigationController.setViewControllers(controllers, animated: animated) } public func showThreads(courseID: String, topics: Topics, title: String, type: ThreadType) { @@ -352,6 +524,16 @@ public class Router: AuthorizationRouter, navigationController.pushViewController(controller, animated: true) } + public func showUserDetails(username: String) { + let interactor = container.resolve(ProfileInteractorProtocol.self)! + + let vm = UserProfileViewModel(interactor: interactor, + username: username) + let view = UserProfileView(viewModel: vm) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + public func showEditProfile( userModel: Core.UserProfile, avatar: UIImage?, @@ -379,7 +561,19 @@ public class Router: AuthorizationRouter, let controller = UIHostingController(rootView: view) navigationController.pushViewController(controller, animated: true) } - + + public func showVideoDownloadQualityView( + downloadQuality: DownloadQuality, + didSelect: ((DownloadQuality) -> Void)? + ) { + let view = VideoDownloadQualityView( + downloadQuality: downloadQuality, + didSelect: didSelect + ) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + private func present(transitionStyle: UIModalTransitionStyle, view: ToPresent) { navigationController.present( prepareToPresent(view, transitionStyle: transitionStyle), @@ -396,6 +590,21 @@ public class Router: AuthorizationRouter, navigationController.pushViewController(controller, animated: true) } + public func showUpdateRequiredView(showAccountLink: Bool = true) { + let view = UpdateRequiredView( + router: self, + config: Container.shared.resolve(ConfigProtocol.self)!, + showAccountLink: showAccountLink + ) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: false) + } + + public func showUpdateRecomendedView() { + let view = UpdateRecommendedView(router: self, config: Container.shared.resolve(ConfigProtocol.self)!) + self.presentView(transitionStyle: .crossDissolve, view: view) + } + private func prepareToPresent (_ toPresent: ToPresent, transitionStyle: UIModalTransitionStyle) -> UIViewController { let hosting = UIHostingController(rootView: toPresent) @@ -408,4 +617,14 @@ public class Router: AuthorizationRouter, private func showToolBar() { self.navigationController.setNavigationBarHidden(false, animated: false) } + + public func showWebBrowser(title: String, url: URL) { + let webBrowser = WebBrowser( + url: url.absoluteString, + pageTitle: title, + showProgress: true + ) + let controller = UIHostingController(rootView: webBrowser) + navigationController.pushViewController(controller, animated: true) + } } diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index c27d7ea3f..873350011 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -11,12 +11,16 @@ import Core import Swinject import Dashboard import Profile +import WhatsNew import SwiftUIIntrospect +import Theme struct MainScreenView: View { @State private var selection: MainTab = .discovery @State private var settingsTapped: Bool = false + @State private var disableAllTabs: Bool = false + @State private var updateAvaliable: Bool = false enum MainTab { case discovery @@ -25,9 +29,10 @@ struct MainScreenView: View { case profile } - private let analytics = Container.shared.resolve(MainScreenAnalytics.self)! - - init() { + @ObservedObject private var viewModel: MainScreenViewModel + + init(viewModel: MainScreenViewModel) { + self.viewModel = viewModel UITabBar.appearance().isTranslucent = false UITabBar.appearance().barTintColor = UIColor(Theme.Colors.textInputUnfocusedBackground) UITabBar.appearance().backgroundColor = UIColor(Theme.Colors.textInputUnfocusedBackground) @@ -36,21 +41,43 @@ struct MainScreenView: View { var body: some View { TabView(selection: $selection) { - DiscoveryView( - viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, - router: Container.shared.resolve(DiscoveryRouter.self)! - ) - .tabItem { - CoreAssets.discovery.swiftUIImage.renderingMode(.template) - Text(CoreLocalization.Mainscreen.discovery) + let config = Container.shared.resolve(ConfigProtocol.self) + if config?.discovery.enabled ?? false { + ZStack { + if config?.discovery.type == .native { + DiscoveryView( + viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)!, + sourceScreen: viewModel.sourceScreen + ) + } else if config?.discovery.type == .webview { + DiscoveryWebview( + viewModel: Container.shared.resolve( + DiscoveryWebviewViewModel.self, + argument: viewModel.sourceScreen)!, + router: Container.shared.resolve(DiscoveryRouter.self)! + ) + } + + if updateAvaliable { + UpdateNotificationView(config: viewModel.config) + } + } + .tabItem { + CoreAssets.discovery.swiftUIImage.renderingMode(.template) + Text(CoreLocalization.Mainscreen.discovery) + } + .tag(MainTab.discovery) } - .tag(MainTab.discovery) - VStack { + ZStack { DashboardView( viewModel: Container.shared.resolve(DashboardViewModel.self)!, router: Container.shared.resolve(DashboardRouter.self)! ) + if updateAvaliable { + UpdateNotificationView(config: viewModel.config) + } } .tabItem { CoreAssets.dashboard.swiftUIImage.renderingMode(.template) @@ -58,14 +85,27 @@ struct MainScreenView: View { } .tag(MainTab.dashboard) - VStack { - Text(CoreLocalization.Mainscreen.inDeveloping) - } - .tabItem { - CoreAssets.programs.swiftUIImage.renderingMode(.template) - Text(CoreLocalization.Mainscreen.programs) + if config?.program.enabled ?? false { + ZStack { + if config?.program.type == .webview { + ProgramWebviewView( + viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)! + ) + } else if config?.program.type == .native { + Text(CoreLocalization.Mainscreen.inDeveloping) + } + + if updateAvaliable { + UpdateNotificationView(config: viewModel.config) + } + } + .tabItem { + CoreAssets.programs.swiftUIImage.renderingMode(.template) + Text(CoreLocalization.Mainscreen.programs) + } + .tag(MainTab.programs) } - .tag(MainTab.programs) VStack { ProfileView( @@ -95,18 +135,36 @@ struct MainScreenView: View { } }) } + .onReceive(NotificationCenter.default.publisher(for: .onAppUpgradeAccountSettingsTapped)) { _ in + selection = .profile + disableAllTabs = true + } + .onReceive(NotificationCenter.default.publisher(for: .onNewVersionAvaliable)) { _ in + updateAvaliable = true + } + .onChange(of: selection) { _ in + if disableAllTabs { + selection = .profile + } + } .onChange(of: selection, perform: { selection in switch selection { case .discovery: - analytics.mainDiscoveryTabClicked() + viewModel.trackMainDiscoveryTabClicked() case .dashboard: - analytics.mainDashboardTabClicked() + viewModel.trackMainDashboardTabClicked() case .programs: - analytics.mainProgramsTabClicked() + viewModel.trackMainProgramsTabClicked() case .profile: - analytics.mainProfileTabClicked() + viewModel.trackMainProfileTabClicked() } }) + .onFirstAppear { + Task { + await viewModel.prefetchDataForOffline() + } + } + .accentColor(Theme.Colors.accentColor) } private func titleBar() -> String { @@ -121,10 +179,4 @@ struct MainScreenView: View { return ProfileLocalization.title } } - - struct MainScreenView_Previews: PreviewProvider { - static var previews: some View { - MainScreenView() - } - } } diff --git a/OpenEdX/View/MainScreenViewModel.swift b/OpenEdX/View/MainScreenViewModel.swift new file mode 100644 index 000000000..1ee8beb7e --- /dev/null +++ b/OpenEdX/View/MainScreenViewModel.swift @@ -0,0 +1,50 @@ +// +// MainScreenViewModel.swift +// OpenEdX +// +// Created by  Stepanok Ivan on 30.10.2023. +// + +import Foundation +import Core +import Profile + +class MainScreenViewModel: ObservableObject { + + private let analytics: MainScreenAnalytics + let config: ConfigProtocol + let profileInteractor: ProfileInteractorProtocol + var sourceScreen: LogistrationSourceScreen + + init(analytics: MainScreenAnalytics, + config: ConfigProtocol, + profileInteractor: ProfileInteractorProtocol, + sourceScreen: LogistrationSourceScreen = .default + ) { + self.analytics = analytics + self.config = config + self.profileInteractor = profileInteractor + self.sourceScreen = sourceScreen + } + + func trackMainDiscoveryTabClicked() { + analytics.mainDiscoveryTabClicked() + } + func trackMainDashboardTabClicked() { + analytics.mainDashboardTabClicked() + } + func trackMainProgramsTabClicked() { + analytics.mainProgramsTabClicked() + } + func trackMainProfileTabClicked() { + analytics.mainProfileTabClicked() + } + + @MainActor + func prefetchDataForOffline() async { + if profileInteractor.getMyProfileOffline() == nil { + _ = try? await profileInteractor.getMyProfile() + } + } + +} diff --git a/Podfile b/Podfile index 1d97fa467..291bcb28e 100644 --- a/Podfile +++ b/Podfile @@ -48,6 +48,15 @@ abstract_target "App" do end end + target "WhatsNew" do + project './WhatsNew/WhatsNew.xcodeproj' + workspace './WhatsNew/WhatsNew.xcodeproj' + + target 'WhatsNewTests' do + pod 'SwiftyMocky', :git => 'https://github.com/MakeAWishFoundation/SwiftyMocky.git', :tag => '4.2.0' + end + end + target "Dashboard" do project './Dashboard/Dashboard.xcodeproj' workspace './Dashboard/Dashboard.xcodeproj' @@ -84,4 +93,13 @@ abstract_target "App" do end end + target "Theme" do + project './Theme/Theme.xcodeproj' + workspace './Theme/Theme.xcodeproj' + + target 'ThemeTests' do + pod 'SwiftyMocky', :git => 'https://github.com/MakeAWishFoundation/SwiftyMocky.git', :tag => '4.2.0' + end + end + end diff --git a/Podfile.lock b/Podfile.lock index 5b998cd5e..20a87d7f1 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,7 +1,7 @@ PODS: - - Alamofire (5.7.1) - - FirebaseAnalytics (10.11.0): - - FirebaseAnalytics/AdIdSupport (= 10.11.0) + - Alamofire (5.8.0) + - FirebaseAnalytics (10.15.0): + - FirebaseAnalytics/AdIdSupport (= 10.15.0) - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) @@ -9,24 +9,24 @@ PODS: - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (10.11.0): + - FirebaseAnalytics/AdIdSupport (10.15.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.11.0) + - GoogleAppMeasurement (= 10.15.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseCore (10.11.0): + - FirebaseCore (10.15.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreExtension (10.11.0): + - FirebaseCoreExtension (10.15.0): - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.11.0): + - FirebaseCoreInternal (10.15.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseCrashlytics (10.11.0): + - FirebaseCrashlytics (10.15.0): - FirebaseCore (~> 10.5) - FirebaseInstallations (~> 10.0) - FirebaseSessions (~> 10.5) @@ -34,12 +34,12 @@ PODS: - GoogleUtilities/Environment (~> 7.8) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (~> 2.1) - - FirebaseInstallations (10.11.0): + - FirebaseInstallations (10.15.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseSessions (10.11.0): + - FirebaseSessions (10.15.0): - FirebaseCore (~> 10.5) - FirebaseCoreExtension (~> 10.0) - FirebaseInstallations (~> 10.0) @@ -47,65 +47,65 @@ PODS: - GoogleUtilities/Environment (~> 7.10) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesSwift (~> 2.1) - - GoogleAppMeasurement (10.11.0): - - GoogleAppMeasurement/AdIdSupport (= 10.11.0) + - GoogleAppMeasurement (10.15.0): + - GoogleAppMeasurement/AdIdSupport (= 10.15.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (10.11.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.11.0) + - GoogleAppMeasurement/AdIdSupport (10.15.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.15.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (10.11.0): + - GoogleAppMeasurement/WithoutAdIdSupport (10.15.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleDataTransport (9.2.3): + - GoogleDataTransport (9.2.5): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.11.1): + - GoogleUtilities/AppDelegateSwizzler (7.11.5): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.1): + - GoogleUtilities/Environment (7.11.5): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.1): + - GoogleUtilities/Logger (7.11.5): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.11.1): + - GoogleUtilities/MethodSwizzler (7.11.5): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.11.1): + - GoogleUtilities/Network (7.11.5): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.1)" - - GoogleUtilities/Reachability (7.11.1): + - "GoogleUtilities/NSData+zlib (7.11.5)" + - GoogleUtilities/Reachability (7.11.5): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.1): + - GoogleUtilities/UserDefaults (7.11.5): - GoogleUtilities/Logger - KeychainSwift (20.0.0) - - Kingfisher (7.8.1) + - Kingfisher (7.9.1) - nanopb (2.30909.0): - nanopb/decode (= 2.30909.0) - nanopb/encode (= 2.30909.0) - nanopb/decode (2.30909.0) - nanopb/encode (2.30909.0) - - PromisesObjC (2.2.0) - - PromisesSwift (2.2.0): - - PromisesObjC (= 2.2.0) + - PromisesObjC (2.3.1) + - PromisesSwift (2.3.1): + - PromisesObjC (= 2.3.1) - Sourcery (1.8.0): - Sourcery/CLI-Only (= 1.8.0) - Sourcery/CLI-Only (1.8.0) - SwiftGen (6.6.2) - - SwiftLint (0.52.3) - - SwiftUIIntrospect (0.8.0) + - SwiftLint (0.53.0) + - SwiftUIIntrospect (0.12.0) - SwiftyMocky (4.2.0): - Sourcery (= 1.8.0) - Swinject (2.8.3) @@ -157,29 +157,29 @@ CHECKOUT OPTIONS: :tag: 4.2.0 SPEC CHECKSUMS: - Alamofire: 0123a34370cb170936ae79a8df46cc62b2edeb88 - FirebaseAnalytics: 6c6bf99e8854475bf1fa342028841be8ecd236da - FirebaseCore: 62fd4d549f5e3f3bd52b7998721c5fa0557fb355 - FirebaseCoreExtension: cacdad57fdb60e0b86dcbcac058ec78237946759 - FirebaseCoreInternal: 9e46c82a14a3b3a25be4e1e151ce6d21536b89c0 - FirebaseCrashlytics: 5927efd92f7fb052b0ab1e673d2f0d97274cd442 - FirebaseInstallations: 2a2c6859354cbec0a228a863d4daf6de7c74ced4 - FirebaseSessions: a62ba5c45284adb7714f4126cfbdb32b17c260bd - GoogleAppMeasurement: d3dabccdb336fc0ae44b633c8abaa26559893cd9 - GoogleDataTransport: f0308f5905a745f94fb91fea9c6cbaf3831cb1bd - GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749 + Alamofire: 0e92e751b3e9e66d7982db43919d01f313b8eb91 + FirebaseAnalytics: 47cef43728f81a839cf1306576bdd77ffa2eac7e + FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e + FirebaseCoreExtension: d3f1ea3725fb41f56e8fbfb29eeaff54e7ffb8f6 + FirebaseCoreInternal: 2f4bee5ed00301b5e56da0849268797a2dd31fb4 + FirebaseCrashlytics: a83f26fb922a3fe181eb738fb4dcf0c92bba6455 + FirebaseInstallations: cae95cab0f965ce05b805189de1d4c70b11c76fb + FirebaseSessions: ee59a7811bef4c15f65ef6472f3210faa293f9c8 + GoogleAppMeasurement: 722db6550d1e6d552b08398b69a975ac61039338 + GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 + GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 KeychainSwift: 0ce6a4d13f7228054d1a71bb1b500448fb2ab837 - Kingfisher: 63f677311d36a3473f6b978584f8a3845d023dc5 + Kingfisher: 1d14e9f59cbe19389f591c929000332bf70efd32 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 - PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef - PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959 + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265 Sourcery: 6f5fe49b82b7e02e8c65560cbd52e1be67a1af2e SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c - SwiftLint: 76ec9c62ad369cff2937474cb34c9af3fa270b7b - SwiftUIIntrospect: cde309fef1f6690dd7585100453f1985f3b91c77 + SwiftLint: 5ce4d6a8ff83f1b5fd5ad5dbf30965d35af65e44 + SwiftUIIntrospect: 89f443402f701a9197e9e54e3c2ed00b10c32e6d SwiftyMocky: c5e96e4ff76ec6dbf5a5941aeb039b5a546954a0 Swinject: 893c9a543000ac2f10ee4cbaf0933c6992c935d5 -PODFILE CHECKSUM: 1639b311802f5d36686512914067b7221ff97a64 +PODFILE CHECKSUM: 544edab2f9ecc4ac18973fb8865f1d0613ec8a28 -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.0 diff --git a/Profile/Mockfile b/Profile/Mockfile index dd72a756d..408c90399 100644 --- a/Profile/Mockfile +++ b/Profile/Mockfile @@ -1,5 +1,5 @@ -sourceryCommand: null -sourceryTemplate: null +sourceryCommand: mint run krzysztofzablocki/Sourcery@2.1.2 sourcery +sourceryTemplate: ../MockTemplate.swifttemplate unit.tests.mock: sources: include: diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index 3ba3e0531..f4ebc7e78 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -30,10 +30,13 @@ 02A9A91E2978194A00B55797 /* Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 020F834A28DB4CCD0062FA70 /* Profile.framework */; platformFilter = ios; }; 02A9A92B29781A6300B55797 /* ProfileMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A92A29781A6300B55797 /* ProfileMock.generated.swift */; }; 02B089432A9F832200754BD4 /* ProfileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B089422A9F832200754BD4 /* ProfileStorage.swift */; }; + 02D0FD092AD698380020D752 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D0FD082AD698380020D752 /* UserProfileView.swift */; }; + 02D0FD0B2AD6984D0020D752 /* UserProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D0FD0A2AD6984D0020D752 /* UserProfileViewModel.swift */; }; 02F175352A4DAD030019CD70 /* ProfileAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175342A4DAD030019CD70 /* ProfileAnalytics.swift */; }; 02F3BFE7292539850051930C /* ProfileRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFE6292539850051930C /* ProfileRouter.swift */; }; 0796C8C929B7905300444B05 /* ProfileBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0796C8C829B7905300444B05 /* ProfileBottomSheet.swift */; }; 25B36FF48C1307888A3890DA /* Pods_App_Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEA369C38362C1A91A012F70 /* Pods_App_Profile.framework */; }; + BAD9CA3F2B29BF5C00DE790A /* ProfileSupportInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA3E2B29BF5C00DE790A /* ProfileSupportInfoView.swift */; }; E8264C634DD8AD314ECE8905 /* Pods_App_Profile_ProfileTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C85ADF87135E03275A980E07 /* Pods_App_Profile_ProfileTests.framework */; }; /* End PBXBuildFile section */ @@ -72,6 +75,8 @@ 02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModelTests.swift; sourceTree = ""; }; 02A9A92A29781A6300B55797 /* ProfileMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileMock.generated.swift; sourceTree = ""; }; 02B089422A9F832200754BD4 /* ProfileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStorage.swift; sourceTree = ""; }; + 02D0FD082AD698380020D752 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = ""; }; + 02D0FD0A2AD6984D0020D752 /* UserProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileViewModel.swift; sourceTree = ""; }; 02ED50CE29A64BAD008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02F175342A4DAD030019CD70 /* ProfileAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileAnalytics.swift; sourceTree = ""; }; 02F3BFE6292539850051930C /* ProfileRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRouter.swift; sourceTree = ""; }; @@ -90,6 +95,7 @@ 9D125F82E0EAC4B6C0CE280F /* Pods-App-Profile-ProfileTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile-ProfileTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Profile-ProfileTests/Pods-App-Profile-ProfileTests.releaseprod.xcconfig"; sourceTree = ""; }; A9F98CD65D1F657EB8F9EA59 /* Pods-App-Profile.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.releasedev.xcconfig"; sourceTree = ""; }; B3F05DC21379BD4FE1AFCCF1 /* Pods-App-Profile.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debugprod.xcconfig"; sourceTree = ""; }; + BAD9CA3E2B29BF5C00DE790A /* ProfileSupportInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSupportInfoView.swift; sourceTree = ""; }; BEA369C38362C1A91A012F70 /* Pods_App_Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C85ADF87135E03275A980E07 /* Pods_App_Profile_ProfileTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Profile_ProfileTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F52EFE7DC07BE68B9A302DAF /* Pods-App-Profile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debug.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debug.xcconfig"; sourceTree = ""; }; @@ -131,6 +137,8 @@ 0203DC3D29AE79F80017BD05 /* Profile */ = { isa = PBXGroup; children = ( + BAD9CA402B29D6CD00DE790A /* Subviews */, + 02D0FD072AD695E10020D752 /* UserProfile */, 021D924528DC634300ACC565 /* ProfileView.swift */, 021D925128DC918D00ACC565 /* ProfileViewModel.swift */, ); @@ -287,6 +295,15 @@ path = Data; sourceTree = ""; }; + 02D0FD072AD695E10020D752 /* UserProfile */ = { + isa = PBXGroup; + children = ( + 02D0FD082AD698380020D752 /* UserProfileView.swift */, + 02D0FD0A2AD6984D0020D752 /* UserProfileViewModel.swift */, + ); + path = UserProfile; + sourceTree = ""; + }; 0766DFD3299AD9D800EBEF6A /* Presentation */ = { isa = PBXGroup; children = ( @@ -328,6 +345,14 @@ path = ../Pods; sourceTree = ""; }; + BAD9CA402B29D6CD00DE790A /* Subviews */ = { + isa = PBXGroup; + children = ( + BAD9CA3E2B29BF5C00DE790A /* ProfileSupportInfoView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; C456081FB065DCEDAB8119E4 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -549,8 +574,10 @@ 021D924E28DC88BB00ACC565 /* ProfileRepository.swift in Sources */, 0796C8C929B7905300444B05 /* ProfileBottomSheet.swift in Sources */, 021D924C28DC884A00ACC565 /* ProfileEndpoint.swift in Sources */, + BAD9CA3F2B29BF5C00DE790A /* ProfileSupportInfoView.swift in Sources */, 020306C82932B13F000949EA /* EditProfileView.swift in Sources */, 0262149229AE57A1008BD75A /* DeleteAccountView.swift in Sources */, + 02D0FD092AD698380020D752 /* UserProfileView.swift in Sources */, 0259104629C39CCF004B5A55 /* SettingsViewModel.swift in Sources */, 021D925528DC92F800ACC565 /* ProfileInteractor.swift in Sources */, 029301DA2938948500E99AB8 /* ProfileType.swift in Sources */, @@ -562,6 +589,7 @@ 0259104829C3A5F0004B5A55 /* VideoQualityView.swift in Sources */, 021D924628DC634300ACC565 /* ProfileView.swift in Sources */, 02F3BFE7292539850051930C /* ProfileRouter.swift in Sources */, + 02D0FD0B2AD6984D0020D752 /* UserProfileViewModel.swift in Sources */, 02F175352A4DAD030019CD70 /* ProfileAnalytics.swift in Sources */, 0262149429AE57B1008BD75A /* DeleteAccountViewModel.swift in Sources */, ); diff --git a/Profile/Profile/Data/Network/ProfileEndpoint.swift b/Profile/Profile/Data/Network/ProfileEndpoint.swift index b7fa6bf19..bf3b330ca 100644 --- a/Profile/Profile/Data/Network/ProfileEndpoint.swift +++ b/Profile/Profile/Data/Network/ProfileEndpoint.swift @@ -30,7 +30,7 @@ enum ProfileEndpoint: EndPointType { case .deleteProfilePicture(username: let username): return "/api/user/v1/accounts/\(username)/image" case .deleteAccount: - return "/mobile_api_extensions/user/v1/accounts/deactivate_logout/" + return "/api/user/v1/accounts/deactivate_logout/" } } diff --git a/Profile/Profile/Data/ProfileRepository.swift b/Profile/Profile/Data/ProfileRepository.swift index 9c95c9a7a..83f37215a 100644 --- a/Profile/Profile/Data/ProfileRepository.swift +++ b/Profile/Profile/Data/ProfileRepository.swift @@ -10,8 +10,9 @@ import Core import Alamofire public protocol ProfileRepositoryProtocol { + func getUserProfile(username: String) async throws -> UserProfile func getMyProfile() async throws -> UserProfile - func getMyProfileOffline() throws -> UserProfile + func getMyProfileOffline() -> UserProfile? func logOut() async throws func uploadProfilePicture(pictureData: Data) async throws func deleteProfilePicture() async throws -> Bool @@ -29,14 +30,14 @@ public class ProfileRepository: ProfileRepositoryProtocol { private var storage: CoreStorage & ProfileStorage private let downloadManager: DownloadManagerProtocol private let coreDataHandler: CoreDataHandlerProtocol - private let config: Config + private let config: ConfigProtocol public init( api: API, storage: CoreStorage & ProfileStorage, coreDataHandler: CoreDataHandlerProtocol, downloadManager: DownloadManagerProtocol, - config: Config + config: ConfigProtocol ) { self.api = api self.storage = storage @@ -45,21 +46,23 @@ public class ProfileRepository: ProfileRepositoryProtocol { self.config = config } + public func getUserProfile(username: String) async throws -> UserProfile { + let user = try await api.requestData( + ProfileEndpoint.getUserProfile(username: username) + ).mapResponse(DataLayer.UserProfile.self) + return user.domain + } + public func getMyProfile() async throws -> UserProfile { - let user = - try await api.requestData( + let user = try await api.requestData( ProfileEndpoint.getUserProfile(username: storage.user?.username ?? "") ).mapResponse(DataLayer.UserProfile.self) storage.userProfile = user return user.domain } - public func getMyProfileOffline() throws -> UserProfile { - if let user = storage.userProfile { - return user.domain - } else { - throw NoCachedDataError() - } + public func getMyProfileOffline() -> UserProfile? { + return storage.userProfile?.domain } public func logOut() async throws { @@ -68,7 +71,7 @@ public class ProfileRepository: ProfileRepositoryProtocol { ProfileEndpoint.logOut(refreshToken: refreshToken, clientID: config.oAuthClientId) ) storage.clear() - downloadManager.deleteAllFiles() + await downloadManager.deleteAllFiles() coreDataHandler.clear() } @@ -141,7 +144,7 @@ public class ProfileRepository: ProfileRepositoryProtocol { if let userSettings = storage.userSettings { return userSettings } else { - return UserSettings(wifiOnly: true, downloadQuality: VideoQuality.auto) + return UserSettings(wifiOnly: true, streamingQuality: .auto, downloadQuality: .auto) } } @@ -154,7 +157,19 @@ public class ProfileRepository: ProfileRepositoryProtocol { #if DEBUG // swiftlint:disable all class ProfileRepositoryMock: ProfileRepositoryProtocol { - func getMyProfileOffline() throws -> Core.UserProfile { + + public func getUserProfile(username: String) async throws -> Core.UserProfile { + return Core.UserProfile(avatarUrl: "", + name: "", + username: "", + dateJoined: Date(), + yearOfBirth: 0, + country: "", + shortBiography: "", + isFullProfile: false) + } + + func getMyProfileOffline() -> Core.UserProfile? { return UserProfile( avatarUrl: "", name: "John Lennon", @@ -218,7 +233,7 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol { public func deleteAccount(password: String) async throws -> Bool { return false } public func getSettings() -> UserSettings { - return UserSettings(wifiOnly: true, downloadQuality: .auto) + return UserSettings(wifiOnly: true, streamingQuality: .auto, downloadQuality: .auto) } public func saveSettings(_ settings: UserSettings) {} } diff --git a/Profile/Profile/Domain/ProfileInteractor.swift b/Profile/Profile/Domain/ProfileInteractor.swift index a29d04ad2..18e09aec2 100644 --- a/Profile/Profile/Domain/ProfileInteractor.swift +++ b/Profile/Profile/Domain/ProfileInteractor.swift @@ -11,8 +11,9 @@ import UIKit //sourcery: AutoMockable public protocol ProfileInteractorProtocol { + func getUserProfile(username: String) async throws -> UserProfile func getMyProfile() async throws -> UserProfile - func getMyProfileOffline() throws -> UserProfile + func getMyProfileOffline() -> UserProfile? func logOut() async throws func getSpokenLanguages() -> [PickerFields.Option] func getCountries() -> [PickerFields.Option] @@ -32,12 +33,16 @@ public class ProfileInteractor: ProfileInteractorProtocol { self.repository = repository } + public func getUserProfile(username: String) async throws -> UserProfile { + return try await repository.getUserProfile(username: username) + } + public func getMyProfile() async throws -> UserProfile { return try await repository.getMyProfile() } - public func getMyProfileOffline() throws -> UserProfile { - return try repository.getMyProfileOffline() + public func getMyProfileOffline() -> UserProfile? { + return repository.getMyProfileOffline() } public func logOut() async throws { diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift index 88c5546a2..36db20c35 100644 --- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift +++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme public struct DeleteAccountView: View { @@ -23,8 +24,14 @@ public struct DeleteAccountView: View { ScrollView { VStack { Group { - CoreAssets.deleteAccount.swiftUIImage - .padding(.top, 50) + ZStack { + CoreAssets.bgDelete.swiftUIImage + CoreAssets.deleteChar.swiftUIImage + .foregroundColor(.accentColor) + .offset(y: -31) + CoreAssets.deleteEyes.swiftUIImage + .offset(x: -7, y: -27) + }.padding(.top, 50) Text(ProfileLocalization.DeleteAccount.areYouSure) .foregroundColor(Theme.Colors.textPrimary) + Text(ProfileLocalization.DeleteAccount.wantToDelete) diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountViewModel.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountViewModel.swift index 58dbe5c52..2cece2de0 100644 --- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountViewModel.swift +++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountViewModel.swift @@ -40,14 +40,14 @@ public class DeleteAccountViewModel: ObservableObject { do { if try await interactor.deleteAccount(password: password) { isShowProgress = false - router.showLoginScreen() + router.showLoginScreen(sourceScreen: .default) } else { isShowProgress = false incorrectPassword = true } } catch { isShowProgress = false - if error.asAFError?.responseCode == 403 { + if error.validationError?.statusCode == 403 { incorrectPassword = true } else if let validationError = error.validationError, let value = validationError.data?["error_code"] as? String, diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift index f0a439d48..b26f87c17 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme public struct EditProfileView: View { @@ -48,7 +49,7 @@ public struct EditProfileView: View { Circle().frame(width: 36, height: 36) .foregroundColor(Theme.Colors.accentColor) CoreAssets.addPhoto.swiftUIImage - .foregroundColor(.white) + .foregroundColor(Theme.Colors.white) }.offset(x: 36, y: 50) ) }).disabled(!viewModel.isEditable) @@ -209,7 +210,7 @@ public struct EditProfileView: View { CoreAssets.arrowLeft.swiftUIImage .renderingMode(.template) .foregroundColor(Theme.Colors.accentColor) - }).opacity(viewModel.isChanged ? 1 : 0.3) + }) }) ToolbarItem(placement: .navigationBarTrailing, content: { Button(action: { diff --git a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift index 9a3f09330..fba6c94f8 100644 --- a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift +++ b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import Theme struct ProfileBottomSheet: View { @@ -38,6 +39,8 @@ struct ProfileBottomSheet: View { private var removePhoto: () -> Void @Binding private var showingBottomSheet: Bool + @Environment (\.isHorizontal) private var isHorizontal + init( showingBottomSheet: Binding, openGallery: @escaping () -> Void, @@ -96,7 +99,12 @@ struct ProfileBottomSheet: View { }).padding(.top, 34) }.padding(.horizontal, 24) - }.frame(maxWidth: idiom == .pad ? 330 : .infinity, maxHeight: 290, alignment: .topLeading) + } + .frame(minWidth: 0, + maxWidth: (idiom == .pad || (idiom == .phone && isHorizontal)) + ? 330 + : .infinity, + maxHeight: 290, alignment: .topLeading) .background(Theme.Colors.cardViewBackground) .cornerRadius(8) .padding(.horizontal, 22) @@ -181,7 +189,7 @@ extension ProfileBottomSheet { func textColor() -> Color { switch self { case .gallery: - return .white + return Theme.Colors.white case .remove: return Theme.Colors.alert case .cancel: diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 2d61ed18c..0450da9c8 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -8,226 +8,68 @@ import SwiftUI import Core import Kingfisher +import Theme public struct ProfileView: View { - + @StateObject private var viewModel: ProfileViewModel @Binding var settingsTapped: Bool - + public init(viewModel: ProfileViewModel, settingsTapped: Binding) { self._viewModel = StateObject(wrappedValue: { viewModel }()) self._settingsTapped = settingsTapped } - + public var body: some View { ZStack(alignment: .top) { // MARK: - Page Body - RefreshableScrollViewCompat(action: { - await viewModel.getMyProfile(withProgress: false) - }) { - VStack { - if viewModel.isShowProgress { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - .padding(.horizontal) - } else { - UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar) - .padding(.top, 30) - Text(viewModel.userModel?.name ?? "") - .font(Theme.Fonts.headlineSmall) - .padding(.top, 20) - - Text("@\(viewModel.userModel?.username ?? "")") - .font(Theme.Fonts.labelLarge) - .padding(.top, 4) - .foregroundColor(Theme.Colors.textSecondary) - .padding(.bottom, 10) - - // MARK: - Profile Info - if viewModel.userModel?.yearOfBirth != 0 || viewModel.userModel?.shortBiography != "" { - VStack(alignment: .leading, spacing: 14) { - Text(ProfileLocalization.info) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - - VStack(alignment: .leading, spacing: 16) { - if viewModel.userModel?.yearOfBirth != 0 { - HStack { - Text(ProfileLocalization.Edit.Fields.yearOfBirth) - .foregroundColor(Theme.Colors.textSecondary) - Text(String(viewModel.userModel?.yearOfBirth ?? 0)) - } - } - if let bio = viewModel.userModel?.shortBiography, bio != "" { - HStack(alignment: .top) { - Text(ProfileLocalization.bio + " ") - .foregroundColor(Theme.Colors.textSecondary) - + Text(bio) - } - } - } - .cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - }.padding(.bottom, 16) + RefreshableScrollViewCompat( + action: { + await viewModel.getMyProfile(withProgress: false) + }, + content: content + ) + .accessibilityAction {} + .frameLimit(sizePortrait: 420) + .padding(.top, 8) + .onChange(of: settingsTapped, perform: { _ in + let userModel = viewModel.userModel ?? UserProfile() + viewModel.trackProfileEditClicked() + viewModel.router.showEditProfile( + userModel: userModel, + avatar: viewModel.updatedAvatar, + profileDidEdit: { updatedProfile, updatedImage in + if let updatedProfile { + self.viewModel.userModel = updatedProfile } - - VStack(alignment: .leading, spacing: 14) { - // MARK: - Settings - Text(ProfileLocalization.settings) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - VStack(alignment: .leading, spacing: 27) { - Button(action: { - viewModel.trackProfileVideoSettingsClicked() - viewModel.router.showSettings() - }, label: { - HStack { - Text(ProfileLocalization.settingsVideo) - Spacer() - Image(systemName: "chevron.right") - } - }) - - }.cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - - // MARK: - Support info - Text(ProfileLocalization.supportInfo) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - VStack(alignment: .leading, spacing: 24) { - if let support = viewModel.contactSupport() { - Button(action: { - viewModel.trackEmailSupportClicked() - UIApplication.shared.open(support) - }, label: { - HStack { - Text(ProfileLocalization.contact) - Spacer() - Image(systemName: "chevron.right") - } - }) - .buttonStyle(PlainButtonStyle()) - .foregroundColor(.primary) - Rectangle() - .frame(height: 1) - .foregroundColor(Theme.Colors.textSecondary) - } - - if let tos = viewModel.config.termsOfUse { - Button(action: { - viewModel.trackCookiePolicyClicked() - UIApplication.shared.open(tos) - }, label: { - HStack { - Text(ProfileLocalization.terms) - Spacer() - Image(systemName: "chevron.right") - } - }) - .buttonStyle(PlainButtonStyle()) - .foregroundColor(.primary) - Rectangle() - .frame(height: 1) - .foregroundColor(Theme.Colors.textSecondary) - } - - if let privacy = viewModel.config.privacyPolicy { - Button(action: { - viewModel.trackPrivacyPolicyClicked() - UIApplication.shared.open(privacy) - }, label: { - HStack { - Text(ProfileLocalization.privacy) - Spacer() - Image(systemName: "chevron.right") - } - }) - .buttonStyle(PlainButtonStyle()) - .foregroundColor(.primary) - } - }.cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - - // MARK: - Log out - VStack { - Button(action: { - viewModel.router.presentView(transitionStyle: .crossDissolve) { - AlertView( - alertTitle: ProfileLocalization.LogoutAlert.title, - alertMessage: ProfileLocalization.LogoutAlert.text, - positiveAction: CoreLocalization.Alert.accept, - onCloseTapped: { - viewModel.router.dismiss(animated: true) - }, - okTapped: { - viewModel.router.dismiss(animated: true) - Task { - await viewModel.logOut() - } - }, type: .logOut - ) - } - }, label: { - HStack { - Text(ProfileLocalization.logout) - Spacer() - Image(systemName: "rectangle.portrait.and.arrow.right") - } - }) - - } - .foregroundColor(Theme.Colors.alert) - .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear) - .padding(.top, 24) - .padding(.bottom, 60) + if let updatedImage { + self.viewModel.updatedAvatar = updatedImage } - Spacer() - } - } - }.frameLimit(sizePortrait: 420) - .padding(.top, 8) - .onChange(of: settingsTapped, perform: { _ in - if let userModel = viewModel.userModel { - viewModel.trackProfileEditClicked() - viewModel.router.showEditProfile( - userModel: userModel, - avatar: viewModel.updatedAvatar, - profileDidEdit: { updatedProfile, updatedImage in - if let updatedProfile { - self.viewModel.userModel = updatedProfile - } - if let updatedImage { - self.viewModel.updatedAvatar = updatedImage - } - } - ) } - }) - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - - // MARK: - Offline mode SnackBar - OfflineSnackBarView(connectivity: viewModel.connectivity, - reloadAction: { - await viewModel.getMyProfile(withProgress: false) + ) }) - + .navigationBarHidden(false) + .navigationBarBackButtonHidden(false) + + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getMyProfile(withProgress: false) + } + ) + // MARK: - Error Alert if viewModel.showError { VStack { Spacer() SnackBarView(message: viewModel.errorMessage) } - .padding(.bottom, viewModel.connectivity.isInternetAvaliable - ? 0 : OfflineSnackBarView.height) + .padding( + .bottom, + viewModel.connectivity.isInternetAvaliable + ? 0 : OfflineSnackBarView.height + ) .transition(.move(edge: .bottom)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { @@ -246,6 +88,149 @@ public struct ProfileView: View { .ignoresSafeArea() ) } + + private var progressBar: some View { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } + + private func content() -> some View { + VStack { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } else { + UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar) + .padding(.top, 30) + Text(viewModel.userModel?.name ?? "") + .font(Theme.Fonts.headlineSmall) + .padding(.top, 20) + Text("@\(viewModel.userModel?.username ?? "")") + .font(Theme.Fonts.labelLarge) + .padding(.top, 4) + .foregroundColor(Theme.Colors.textSecondary) + .padding(.bottom, 10) + profileInfo + VStack(alignment: .leading, spacing: 14) { + settings + ProfileSupportInfoView(viewModel: viewModel) + logOutButton + } + Spacer() + } + } + } + + // MARK: - Profile Info + + @ViewBuilder + private var profileInfo: some View { + if viewModel.userModel?.yearOfBirth != 0 || viewModel.userModel?.shortBiography != "" { + VStack(alignment: .leading, spacing: 14) { + Text(ProfileLocalization.info) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + + VStack(alignment: .leading, spacing: 16) { + if viewModel.userModel?.yearOfBirth != 0 { + HStack { + Text(ProfileLocalization.Edit.Fields.yearOfBirth) + .foregroundColor(Theme.Colors.textSecondary) + Text(String(viewModel.userModel?.yearOfBirth ?? 0)) + } + } + if let bio = viewModel.userModel?.shortBiography, bio != "" { + HStack(alignment: .top) { + Text(ProfileLocalization.bio + " ") + .foregroundColor(Theme.Colors.textSecondary) + + Text(bio) + } + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel( + (viewModel.userModel?.yearOfBirth != 0 ? + ProfileLocalization.Edit.Fields.yearOfBirth + String(viewModel.userModel?.yearOfBirth ?? 0) : + "") + + (viewModel.userModel?.shortBiography != nil ? + ProfileLocalization.bio + (viewModel.userModel?.shortBiography ?? "") : + "") + ) + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + }.padding(.bottom, 16) + } + } + + // MARK: - Settings + + @ViewBuilder + private var settings: some View { + Text(ProfileLocalization.settings) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + VStack(alignment: .leading, spacing: 27) { + Button(action: { + viewModel.trackProfileVideoSettingsClicked() + viewModel.router.showSettings() + }, label: { + HStack { + Text(ProfileLocalization.settingsVideo) + Spacer() + Image(systemName: "chevron.right") + } + }) + + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(ProfileLocalization.settingsVideo) + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + } + + // MARK: - Log out + + private var logOutButton: some View { + VStack { + Button(action: { + viewModel.router.presentView(transitionStyle: .crossDissolve) { + AlertView( + alertTitle: ProfileLocalization.LogoutAlert.title, + alertMessage: ProfileLocalization.LogoutAlert.text, + positiveAction: CoreLocalization.Alert.accept, + onCloseTapped: { + viewModel.router.dismiss(animated: true) + }, + okTapped: { + viewModel.router.dismiss(animated: true) + Task { + await viewModel.logOut() + } + }, type: .logOut + ) + } + }, label: { + HStack { + Text(ProfileLocalization.logout) + Spacer() + Image(systemName: "rectangle.portrait.and.arrow.right") + } + }) + .accessibilityElement(children: .ignore) + .accessibilityLabel(ProfileLocalization.logout) + } + .foregroundColor(Theme.Colors.alert) + .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear) + .padding(.top, 24) + .padding(.bottom, 60) + } } #if DEBUG @@ -257,12 +242,12 @@ struct ProfileView_Previews: PreviewProvider { analytics: ProfileAnalyticsMock(), config: ConfigMock(), connectivity: Connectivity()) - + ProfileView(viewModel: vm, settingsTapped: .constant(false)) .preferredColorScheme(.light) .previewDisplayName("DiscoveryView Light") .loadFonts() - + ProfileView(viewModel: vm, settingsTapped: .constant(false)) .preferredColorScheme(.dark) .previewDisplayName("DiscoveryView Dark") @@ -272,10 +257,10 @@ struct ProfileView_Previews: PreviewProvider { #endif struct UserAvatar: View { - + private var url: URL? @Binding private var image: UIImage? - + init(url: String, image: Binding) { if let rightUrl = URL(string: url) { self.url = rightUrl @@ -284,7 +269,7 @@ struct UserAvatar: View { } self._image = image } - + var body: some View { ZStack { Circle() diff --git a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift index 49e5dd254..ae3d9a309 100644 --- a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift @@ -5,12 +5,12 @@ // Created by  Stepanok Ivan on 22.09.2022. // -import Foundation +import Combine import Core import SwiftUI public class ProfileViewModel: ObservableObject { - + @Published public var userModel: UserProfile? @Published public var updatedAvatar: UIImage? @Published private(set) var isShowProgress = false @@ -22,10 +22,21 @@ public class ProfileViewModel: ObservableObject { } } } + + private var cancellables = Set() + + enum VersionState { + case actual + case updateNeeded + case updateRequired + } + @Published var versionState: VersionState = .actual + @Published var currentVersion: String = "" + @Published var latestVersion: String = "" let router: ProfileRouter - let config: Config + let config: ConfigProtocol let connectivity: ConnectivityProtocol private let interactor: ProfileInteractorProtocol @@ -35,7 +46,7 @@ public class ProfileViewModel: ObservableObject { interactor: ProfileInteractorProtocol, router: ProfileRouter, analytics: ProfileAnalytics, - config: Config, + config: ConfigProtocol, connectivity: ConnectivityProtocol ) { self.interactor = interactor @@ -43,6 +54,29 @@ public class ProfileViewModel: ObservableObject { self.analytics = analytics self.config = config self.connectivity = connectivity + generateVersionState() + } + + func openAppStore() { + guard let appStoreURL = URL(string: config.appStoreLink) else { return } + UIApplication.shared.open(appStoreURL) + } + + func generateVersionState() { + guard let info = Bundle.main.infoDictionary else { return } + guard let currentVersion = info["CFBundleShortVersionString"] as? String else { return } + self.currentVersion = currentVersion + NotificationCenter.default.publisher(for: .onActualVersionReceived) + .sink { [weak self] notification in + guard let latestVersion = notification.object as? String else { return } + DispatchQueue.main.async { [weak self] in + self?.latestVersion = latestVersion + + if latestVersion != currentVersion { + self?.versionState = .updateNeeded + } + } + }.store(in: &cancellables) } func contactSupport() -> URL? { @@ -60,39 +94,34 @@ public class ProfileViewModel: ObservableObject { @MainActor func getMyProfile(withProgress: Bool = true) async { - isShowProgress = withProgress do { - if connectivity.isInternetAvaliable { - userModel = try await interactor.getMyProfile() - isShowProgress = false + let userModel = interactor.getMyProfileOffline() + if userModel == nil && connectivity.isInternetAvaliable { + isShowProgress = withProgress } else { - userModel = try interactor.getMyProfileOffline() - isShowProgress = false + self.userModel = userModel } + if connectivity.isInternetAvaliable { + self.userModel = try await interactor.getMyProfile() + } + isShowProgress = false } catch let error { isShowProgress = false - if error.isInternetError || error is NoCachedDataError { + if error.isUpdateRequeiredError { + self.versionState = .updateRequired + } else if error.isInternetError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection } else { errorMessage = CoreLocalization.Error.unknownError } - } } @MainActor func logOut() async { - do { - try await interactor.logOut() - router.showLoginScreen() - analytics.userLogout(force: false) - } catch let error { - if error.isInternetError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } - } + try? await interactor.logOut() + router.showStartupScreen() + analytics.userLogout(force: false) } func trackProfileVideoSettingsClicked() { diff --git a/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift new file mode 100644 index 000000000..81b669355 --- /dev/null +++ b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift @@ -0,0 +1,195 @@ +// +// ProfileSupportInfo.swift +// Profile +// +// Created by Eugene Yatsenko on 13.12.2023. +// + +import SwiftUI +import Theme +import Core + +struct ProfileSupportInfoView: View { + + struct LinkViewModel { + let url: URL + let title: String + } + + @ObservedObject var viewModel: ProfileViewModel + + var body: some View { + Text(ProfileLocalization.supportInfo) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + VStack(alignment: .leading, spacing: 24) { + viewModel.contactSupport().map(supportInfo) + viewModel.config.agreement.tosURL.map(terms) + viewModel.config.agreement.privacyPolicyURL.map(privacy) + viewModel.config.agreement.cookiePolicyURL.map(cookiePolicy) + viewModel.config.agreement.dataSellContentURL.map(dataSellContent) + viewModel.config.faq.map(faq) + version + } + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + } + + private func supportInfo(url: URL) -> some View { + button( + linkViewModel: .init( + url: url, + title: ProfileLocalization.contact + ), + isEmailSupport: true + ) + + } + + private func terms(url: URL) -> some View { + navigationLink( + viewModel: .init( + url: url, + title: ProfileLocalization.terms + ) + ) + } + + private func privacy(url: URL) -> some View { + navigationLink( + viewModel: .init( + url: url, + title: ProfileLocalization.privacy + ) + ) + } + + private func cookiePolicy(url: URL) -> some View { + navigationLink( + viewModel: .init( + url: url, + title: ProfileLocalization.cookiePolicy + ) + ) + } + + private func dataSellContent(url: URL) -> some View { + navigationLink( + viewModel: .init( + url: url, + title: ProfileLocalization.doNotSellInformation + ) + ) + } + + private func faq(url: URL) -> some View { + button( + linkViewModel: .init( + url: url, + title: ProfileLocalization.faqTitle + ) + ) + } + + @ViewBuilder + private func navigationLink(viewModel: LinkViewModel) -> some View { + NavigationLink { + WebBrowser( + url: viewModel.url.absoluteString, + pageTitle: viewModel.title, + showProgress: true + ) + } label: { + HStack { + Text(viewModel.title) + .multilineTextAlignment(.leading) + Spacer() + Image(systemName: "chevron.right") + } + } + .foregroundColor(.primary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(viewModel.title) + Rectangle() + .frame(height: 1) + .foregroundColor(Theme.Colors.textSecondary) + } + + @ViewBuilder + private func button(linkViewModel: LinkViewModel, isEmailSupport: Bool = false) -> some View { + Button { + guard UIApplication.shared.canOpenURL(linkViewModel.url) else { + viewModel.errorMessage = isEmailSupport ? + ProfileLocalization.Error.cannotSendEmail : + CoreLocalization.Error.unknownError + return + } + if isEmailSupport { + viewModel.trackEmailSupportClicked() + } + UIApplication.shared.open(linkViewModel.url) + } label: { + HStack { + Text(linkViewModel.title) + Spacer() + Image(systemName: "chevron.right") + } + } + .foregroundColor(.primary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(linkViewModel.title) + Rectangle() + .frame(height: 1) + .foregroundColor(Theme.Colors.textSecondary) + } + + @ViewBuilder + private var version: some View { + Button(action: { + viewModel.openAppStore() + }, label: { + HStack { + VStack(alignment: .leading, spacing: 0) { + HStack { + if viewModel.versionState == .updateRequired { + CoreAssets.warningFilled.swiftUIImage + .resizable() + .frame(width: 24, height: 24) + } + Text("\(ProfileLocalization.Settings.version) \(viewModel.currentVersion)") + } + switch viewModel.versionState { + case .actual: + HStack { + CoreAssets.checkmark.swiftUIImage + .renderingMode(.template) + .foregroundColor(.green) + Text(ProfileLocalization.Settings.upToDate) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textSecondary) + } + case .updateNeeded: + Text("\(ProfileLocalization.Settings.tapToUpdate) \(viewModel.latestVersion)") + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.accentColor) + case .updateRequired: + Text(ProfileLocalization.Settings.tapToInstall) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.accentColor) + } + } + Spacer() + if viewModel.versionState != .actual { + Image(systemName: "arrow.up.circle") + .resizable() + .frame(width: 24, height: 24) + .foregroundStyle(Theme.Colors.accentColor) + } + + } + }).disabled(viewModel.versionState == .actual) + } + +} diff --git a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift new file mode 100644 index 000000000..870060a77 --- /dev/null +++ b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift @@ -0,0 +1,145 @@ +// +// UserProfileView.swift +// Profile +// +// Created by  Stepanok Ivan on 10.10.2023. +// + +import SwiftUI +import Core +import Kingfisher +import Theme + +public struct UserProfileView: View { + + @ObservedObject private var viewModel: UserProfileViewModel + + public init(viewModel: UserProfileViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + ZStack(alignment: .top) { + Theme.Colors.background + .ignoresSafeArea() + // MARK: - Page Body + RefreshableScrollViewCompat(action: { + await viewModel.getUserProfile(withProgress: false) + }) { + VStack { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } else { + ProfileAvatar(url: viewModel.userModel?.avatarUrl ?? "") + if let name = viewModel.userModel?.name, name != "" { + Text(name) + .font(Theme.Fonts.headlineSmall) + .padding(.top, 20) + } + Text("@\(viewModel.userModel?.username ?? "")") + .font(Theme.Fonts.labelLarge) + .padding(.top, 4) + .foregroundColor(Theme.Colors.textSecondary) + .padding(.bottom, 10) + + // MARK: - Profile Info + if viewModel.userModel?.yearOfBirth != 0 || viewModel.userModel?.shortBiography != "" { + VStack(alignment: .leading, spacing: 14) { + Text(ProfileLocalization.info) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + + VStack(alignment: .leading, spacing: 16) { + if viewModel.userModel?.yearOfBirth != 0 { + HStack { + Text(ProfileLocalization.Edit.Fields.yearOfBirth) + .foregroundColor(Theme.Colors.textSecondary) + Text(String(viewModel.userModel?.yearOfBirth ?? 0)) + } + } + if let bio = viewModel.userModel?.shortBiography, bio != "" { + HStack(alignment: .top) { + Text(ProfileLocalization.bio + " ") + .foregroundColor(Theme.Colors.textSecondary) + + Text(bio) + } + } + } + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + }.padding(.bottom, 16) + } + } + Spacer() + } + }.frameLimit(sizePortrait: 420) + .padding(.top, 8) + .navigationBarHidden(false) + .navigationBarBackButtonHidden(false) + + // MARK: - Error Alert + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + } + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + } + .onFirstAppear { + Task { + await viewModel.getUserProfile() + } + } + } +} + +struct ProfileAvatar: View { + + private var url: URL? + + init(url: String) { + if let rightUrl = URL(string: url) { + self.url = rightUrl + } else { + self.url = nil + } + } + + var body: some View { + ZStack { + Circle() + .foregroundColor(Theme.Colors.avatarStroke) + .frame(width: 104, height: 104) + KFImage(url) + .onFailureImage(CoreAssets.noCourseImage.image) + .resizable() + .scaledToFill() + .frame(width: 100, height: 100) + .cornerRadius(50) + } + } +} + +#if DEBUG +struct UserProfileView_Previews: PreviewProvider { + static var previews: some View { + + let vm = UserProfileViewModel( + interactor: ProfileInteractor.mock, + username: "demo" + ) + + return UserProfileView(viewModel: vm) + } +} +#endif diff --git a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift new file mode 100644 index 000000000..6a723c800 --- /dev/null +++ b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileViewModel.swift @@ -0,0 +1,52 @@ +// +// UserProfileViewModel.swift +// Discussion +// +// Created by  Stepanok Ivan on 10.10.2023. +// + +import Core +import SwiftUI + +public class UserProfileViewModel: ObservableObject { + + @Published public var userModel: UserProfile? + @Published private(set) var isShowProgress = false + @Published var showError: Bool = false + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + private let username: String + + private let interactor: ProfileInteractorProtocol + + public init( + interactor: ProfileInteractorProtocol, + username: String + ) { + self.interactor = interactor + self.username = username + } + + @MainActor + func getUserProfile(withProgress: Bool = true) async { + isShowProgress = withProgress + do { + userModel = try await interactor.getUserProfile(username: username) + isShowProgress = false + } catch let error { + isShowProgress = false + if error.isInternetError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError + } + + } + } +} diff --git a/Profile/Profile/Presentation/ProfileRouter.swift b/Profile/Profile/Presentation/ProfileRouter.swift index 38f0de7e4..d449e2a3b 100644 --- a/Profile/Profile/Presentation/ProfileRouter.swift +++ b/Profile/Profile/Presentation/ProfileRouter.swift @@ -21,7 +21,12 @@ public protocol ProfileRouter: BaseRouter { func showSettings() func showVideoQualityView(viewModel: SettingsViewModel) - + + func showVideoDownloadQualityView( + downloadQuality: DownloadQuality, + didSelect: ((DownloadQuality) -> Void)? + ) + func showDeleteProfileView() } @@ -41,7 +46,12 @@ public class ProfileRouterMock: BaseRouterMock, ProfileRouter { public func showSettings() {} public func showVideoQualityView(viewModel: SettingsViewModel) {} - + + public func showVideoDownloadQualityView( + downloadQuality: DownloadQuality, + didSelect: ((DownloadQuality) -> Void)? + ) {} + public func showDeleteProfileView() {} } diff --git a/Profile/Profile/Presentation/Settings/SettingsView.swift b/Profile/Profile/Presentation/Settings/SettingsView.swift index 1e940716f..5ce74d71a 100644 --- a/Profile/Profile/Presentation/Settings/SettingsView.swift +++ b/Profile/Profile/Presentation/Settings/SettingsView.swift @@ -8,6 +8,7 @@ import SwiftUI import Core import Kingfisher +import Theme public struct SettingsView: View { @@ -41,7 +42,7 @@ public struct SettingsView: View { }.foregroundColor(Theme.Colors.textPrimary) Divider() - // MARK: Download Quality + // MARK: Streaming Quality HStack { Button(action: { viewModel.router.showVideoQualityView(viewModel: viewModel) @@ -55,10 +56,33 @@ public struct SettingsView: View { .frame(width: 10) } Divider() + + // MARK: Download Quality + HStack { + Button { + viewModel.router.showVideoDownloadQualityView( + downloadQuality: viewModel.userSettings.downloadQuality, + didSelect: viewModel.update(downloadQuality:) + ) + } label: { + SettingsCell( + title: CoreLocalization.Settings.videoDownloadQualityTitle, + description: viewModel.userSettings.downloadQuality.settingsDescription + ) + } + // Spacer() + Image(systemName: "chevron.right") + .padding(.trailing, 12) + .frame(width: 10) + } + Divider() } - }.frame(minWidth: 0, - maxWidth: .infinity, - alignment: .topLeading) + } + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading + ) .padding(.horizontal, 24) }.frameLimit(sizePortrait: 420) .padding(.top, 8) diff --git a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift index 99e11ba38..b24eee494 100644 --- a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift +++ b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift @@ -22,16 +22,25 @@ public class SettingsViewModel: ObservableObject { } } - @Published var selectedQuality: VideoQuality { + @Published var selectedQuality: StreamingQuality { willSet { if newValue != selectedQuality { - userSettings.downloadQuality = newValue + userSettings.streamingQuality = newValue interactor.saveSettings(userSettings) } } } - let quality = Array([VideoQuality.auto, VideoQuality.low, VideoQuality.medium, VideoQuality.high].enumerated()) - + + let quality = Array( + [ + StreamingQuality.auto, + StreamingQuality.low, + StreamingQuality.medium, + StreamingQuality.high + ] + .enumerated() + ) + var errorMessage: String? { didSet { withAnimation { @@ -40,8 +49,8 @@ public class SettingsViewModel: ObservableObject { } } - private var userSettings: UserSettings - + @Published private(set) var userSettings: UserSettings + private let interactor: ProfileInteractorProtocol let router: ProfileRouter @@ -49,13 +58,19 @@ public class SettingsViewModel: ObservableObject { self.interactor = interactor self.router = router - self.userSettings = interactor.getSettings() + let userSettings = interactor.getSettings() + self.userSettings = userSettings self.wifiOnly = userSettings.wifiOnly - self.selectedQuality = userSettings.downloadQuality + self.selectedQuality = userSettings.streamingQuality + } + + func update(downloadQuality: DownloadQuality) { + self.userSettings.downloadQuality = downloadQuality + interactor.saveSettings(userSettings) } } -extension VideoQuality { +public extension StreamingQuality { func title() -> String { switch self { diff --git a/Profile/Profile/Presentation/Settings/VideoQualityView.swift b/Profile/Profile/Presentation/Settings/VideoQualityView.swift index a82c67747..e347ee60d 100644 --- a/Profile/Profile/Presentation/Settings/VideoQualityView.swift +++ b/Profile/Profile/Presentation/Settings/VideoQualityView.swift @@ -8,6 +8,7 @@ import SwiftUI import Core import Kingfisher +import Theme public struct VideoQualityView: View { diff --git a/Profile/Profile/SwiftGen/Strings.swift b/Profile/Profile/SwiftGen/Strings.swift index f31fe2bf0..69559d7c8 100644 --- a/Profile/Profile/SwiftGen/Strings.swift +++ b/Profile/Profile/SwiftGen/Strings.swift @@ -14,8 +14,14 @@ public enum ProfileLocalization { public static let bio = ProfileLocalization.tr("Localizable", "BIO", fallback: "Bio:") /// Contact support public static let contact = ProfileLocalization.tr("Localizable", "CONTACT", fallback: "Contact support") + /// Cookie policy + public static let cookiePolicy = ProfileLocalization.tr("Localizable", "COOKIE_POLICY", fallback: "Cookie policy") + /// Do not sell my personal information + public static let doNotSellInformation = ProfileLocalization.tr("Localizable", "DO_NOT_SELL_INFORMATION", fallback: "Do not sell my personal information") /// Edit profile public static let editProfile = ProfileLocalization.tr("Localizable", "EDIT_PROFILE", fallback: "Edit profile") + /// View FAQ + public static let faqTitle = ProfileLocalization.tr("Localizable", "FAQ_TITLE", fallback: "View FAQ") /// full profile public static let fullProfile = ProfileLocalization.tr("Localizable", "FULL_PROFILE", fallback: "full profile") /// Profile info @@ -24,8 +30,8 @@ public enum ProfileLocalization { public static let limitedProfile = ProfileLocalization.tr("Localizable", "LIMITED_PROFILE", fallback: "limited profile") /// Log out public static let logout = ProfileLocalization.tr("Localizable", "LOGOUT", fallback: "Log out") - /// Privacy and policy - public static let privacy = ProfileLocalization.tr("Localizable", "PRIVACY", fallback: "Privacy and policy") + /// Privacy policy + public static let privacy = ProfileLocalization.tr("Localizable", "PRIVACY", fallback: "Privacy policy") /// Settings public static let settings = ProfileLocalization.tr("Localizable", "SETTINGS", fallback: "Settings") /// Video settings @@ -50,8 +56,8 @@ public enum ProfileLocalization { public static let backToProfile = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.BACK_TO_PROFILE", fallback: "Back to profile") /// Yes, delete account public static let comfirm = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.COMFIRM", fallback: "Yes, delete account") - /// To confirm this action you need to enter you account password. - public static let description = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.DESCRIPTION", fallback: "To confirm this action you need to enter you account password.") + /// To confirm this action you need to enter your account password. + public static let description = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.DESCRIPTION", fallback: "To confirm this action you need to enter your account password.") /// The password is incorrect. Please try again. public static let incorrectPassword = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.INCORRECT_PASSWORD", fallback: "The password is incorrect. Please try again.") /// Password @@ -97,6 +103,10 @@ public enum ProfileLocalization { public static let yearOfBirth = ProfileLocalization.tr("Localizable", "EDIT.FIELDS.YEAR_OF_BIRTH", fallback: "Year of birth") } } + public enum Error { + /// Cannot send email. It seems your email client is not set up. + public static let cannotSendEmail = ProfileLocalization.tr("Localizable", "ERROR.CANNOT_SEND_EMAIL", fallback: "Cannot send email. It seems your email client is not set up.") + } public enum LogoutAlert { /// Are you sure you want to log out? public static let text = ProfileLocalization.tr("Localizable", "LOGOUT_ALERT.TEXT", fallback: "Are you sure you want to log out?") @@ -104,8 +114,8 @@ public enum ProfileLocalization { public static let title = ProfileLocalization.tr("Localizable", "LOGOUT_ALERT.TITLE", fallback: "Comfirm log out") } public enum Settings { - /// Smallest video quality - public static let quality360Description = ProfileLocalization.tr("Localizable", "SETTINGS.QUALITY_360_DESCRIPTION", fallback: "Smallest video quality") + /// Lower data usage + public static let quality360Description = ProfileLocalization.tr("Localizable", "SETTINGS.QUALITY_360_DESCRIPTION", fallback: "Lower data usage") /// 360p public static let quality360Title = ProfileLocalization.tr("Localizable", "SETTINGS.QUALITY_360_TITLE", fallback: "360p") /// 540p @@ -118,10 +128,18 @@ public enum ProfileLocalization { public static let qualityAutoDescription = ProfileLocalization.tr("Localizable", "SETTINGS.QUALITY_AUTO_DESCRIPTION", fallback: "Recommended") /// Auto public static let qualityAutoTitle = ProfileLocalization.tr("Localizable", "SETTINGS.QUALITY_AUTO_TITLE", fallback: "Auto") + /// Tap to install required app update + public static let tapToInstall = ProfileLocalization.tr("Localizable", "SETTINGS.TAP_TO_INSTALL", fallback: "Tap to install required app update") + /// Tap to update to version + public static let tapToUpdate = ProfileLocalization.tr("Localizable", "SETTINGS.TAP_TO_UPDATE", fallback: "Tap to update to version") + /// Up-to-date + public static let upToDate = ProfileLocalization.tr("Localizable", "SETTINGS.UP_TO_DATE", fallback: "Up-to-date") + /// Version: + public static let version = ProfileLocalization.tr("Localizable", "SETTINGS.VERSION", fallback: "Version:") /// Auto (Recommended) public static let videoQualityDescription = ProfileLocalization.tr("Localizable", "SETTINGS.VIDEO_QUALITY_DESCRIPTION", fallback: "Auto (Recommended)") - /// Video download quality - public static let videoQualityTitle = ProfileLocalization.tr("Localizable", "SETTINGS.VIDEO_QUALITY_TITLE", fallback: "Video download quality") + /// Video streaming quality + public static let videoQualityTitle = ProfileLocalization.tr("Localizable", "SETTINGS.VIDEO_QUALITY_TITLE", fallback: "Video streaming quality") /// Video settings public static let videoSettingsTitle = ProfileLocalization.tr("Localizable", "SETTINGS.VIDEO_SETTINGS_TITLE", fallback: "Video settings") /// Only download content when wi-fi is turned on diff --git a/Profile/Profile/en.lproj/Localizable.strings b/Profile/Profile/en.lproj/Localizable.strings index 5c0cd4270..8f88084ea 100644 --- a/Profile/Profile/en.lproj/Localizable.strings +++ b/Profile/Profile/en.lproj/Localizable.strings @@ -16,7 +16,11 @@ "SUPPORT_INFO" = "Support info"; "CONTACT" = "Contact support"; "TERMS" = "Terms of use"; -"PRIVACY" = "Privacy and policy"; +"PRIVACY" = "Privacy policy"; +"COOKIE_POLICY" = "Cookie policy"; +"DO_NOT_SELL_INFORMATION" = "Do not sell my personal information"; +"FAQ_TITLE" = "View FAQ"; + "LOGOUT" = "Log out"; "SWITCH_TO" = "Switch to"; "FULL_PROFILE" = "full profile"; @@ -48,7 +52,7 @@ "DELETE_ACCOUNT.TITLE" = "Delete account"; "DELETE_ACCOUNT.ARE_YOU_SURE" = "Are you sure you want to "; "DELETE_ACCOUNT.WANT_TO_DELETE" = "delete your account?"; -"DELETE_ACCOUNT.DESCRIPTION" = "To confirm this action you need to enter you account password."; +"DELETE_ACCOUNT.DESCRIPTION" = "To confirm this action you need to enter your account password."; "DELETE_ACCOUNT.PASSWORD" = "Password"; "DELETE_ACCOUNT.PASSWORD_DESCRIPTION" = "Enter password"; "DELETE_ACCOUNT.COMFIRM" = "Yes, delete account"; @@ -58,13 +62,21 @@ "SETTINGS.VIDEO_SETTINGS_TITLE" = "Video settings"; "SETTINGS.WIFI_TITLE" = "Wi-fi only download"; "SETTINGS.WIFI_DESCRIPTION" = "Only download content when wi-fi is turned on"; -"SETTINGS.VIDEO_QUALITY_TITLE" = "Video download quality"; +"SETTINGS.VIDEO_QUALITY_TITLE" = "Video streaming quality"; "SETTINGS.VIDEO_QUALITY_DESCRIPTION" = "Auto (Recommended)"; "SETTINGS.QUALITY_AUTO_TITLE" = "Auto"; "SETTINGS.QUALITY_AUTO_DESCRIPTION" = "Recommended"; "SETTINGS.QUALITY_360_TITLE" = "360p"; -"SETTINGS.QUALITY_360_DESCRIPTION" = "Smallest video quality"; +"SETTINGS.QUALITY_360_DESCRIPTION" = "Lower data usage"; "SETTINGS.QUALITY_540_TITLE" = "540p"; "SETTINGS.QUALITY_720_TITLE" = "720p"; "SETTINGS.QUALITY_720_DESCRIPTION" = "Best quality"; + + +"SETTINGS.VERSION" = "Version:"; +"SETTINGS.UP_TO_DATE" = "Up-to-date"; +"SETTINGS.TAP_TO_UPDATE" = "Tap to update to version"; +"SETTINGS.TAP_TO_INSTALL" = "Tap to install required app update"; + +"ERROR.CANNOT_SEND_EMAIL" = "Cannot send email. It seems your email client is not set up."; diff --git a/Profile/Profile/uk.lproj/Localizable.strings b/Profile/Profile/uk.lproj/Localizable.strings index 575f9dca8..f0e4d0503 100644 --- a/Profile/Profile/uk.lproj/Localizable.strings +++ b/Profile/Profile/uk.lproj/Localizable.strings @@ -17,6 +17,9 @@ "CONTACT" = "Cлужби підтримки"; "TERMS" = "Умови використання"; "PRIVACY" = "Політика конфіденційності"; +"COOKIE_POLICY" = "Cookie policy"; +"DO_NOT_SELL_INFORMATION" = "Do not sell my personal information"; +"FAQ_TITLE" = "View FAQ"; "LOGOUT" = "Вийти"; "SWITCH_TO" = "Переключити на"; "FULL_PROFILE" = "повний профіль"; @@ -58,13 +61,20 @@ "SETTINGS.VIDEO_SETTINGS_TITLE" = "Налаштування відео"; "SETTINGS.WIFI_TITLE" = "Тільки Wi-fi"; "SETTINGS.WIFI_DESCRIPTION" = "Завантажувати відео, лише коли Wi-Fi увімкнено"; -"SETTINGS.VIDEO_QUALITY_TITLE" = "Якість відео"; +"SETTINGS.VIDEO_QUALITY_TITLE" = "Якість потокового відео"; "SETTINGS.VIDEO_QUALITY_DESCRIPTION" = "Авто (Рекомендовано)"; "SETTINGS.QUALITY_AUTO_TITLE" = "Авто"; "SETTINGS.QUALITY_AUTO_DESCRIPTION" = "Рекомендовано"; "SETTINGS.QUALITY_360_TITLE" = "360p"; -"SETTINGS.QUALITY_360_DESCRIPTION" = "Найменша якість відео"; +"SETTINGS.QUALITY_360_DESCRIPTION" = "економія трафіку"; "SETTINGS.QUALITY_540_TITLE" = "540p"; "SETTINGS.QUALITY_720_TITLE" = "720p"; "SETTINGS.QUALITY_AUTO_DESCRIPTION" = "Найкраща якість"; + +"SETTINGS.VERSION" = "Версія:"; +"SETTINGS.UP_TO_DATE" = "Оновлено"; +"SETTINGS.TAP_TO_UPDATE" = "Клацніть, щоб оновити до версії"; +"SETTINGS.TAP_TO_INSTALL" = "Клацніть, щоб встановити обов'язкове оновлення програми"; + +"ERROR.CANNOT_SEND_EMAIL" = "Cannot send email. It seems your email client is not set up."; diff --git a/Profile/ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift b/Profile/ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift index 8b4576950..ca284bdf8 100644 --- a/Profile/ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift @@ -25,7 +25,7 @@ final class DeleteAccountViewModelTests: XCTestCase { try await viewModel.deleteAccount(password: "123") Verify(interactor, 1, .deleteAccount(password: .any)) - Verify(router, .showLoginScreen()) + Verify(router, .showLoginScreen(sourceScreen: .any)) } func testDeletingAccountWrongPassword() async throws { @@ -39,7 +39,7 @@ final class DeleteAccountViewModelTests: XCTestCase { try await viewModel.deleteAccount(password: "123") Verify(interactor, 1, .deleteAccount(password: .any)) - Verify(router, 0, .showLoginScreen()) + Verify(router, 0, .showLoginScreen(sourceScreen: .any)) XCTAssertTrue(viewModel.incorrectPassword) } @@ -60,7 +60,7 @@ final class DeleteAccountViewModelTests: XCTestCase { try await viewModel.deleteAccount(password: "123") Verify(interactor, 1, .deleteAccount(password: .any)) - Verify(router, 0, .showLoginScreen()) + Verify(router, 0, .showLoginScreen(sourceScreen: .any)) XCTAssertFalse(viewModel.incorrectPassword) XCTAssertTrue(viewModel.showError) @@ -78,7 +78,7 @@ final class DeleteAccountViewModelTests: XCTestCase { try await viewModel.deleteAccount(password: "123") Verify(interactor, 1, .deleteAccount(password: .any)) - Verify(router, 0, .showLoginScreen()) + Verify(router, 0, .showLoginScreen(sourceScreen: .any)) XCTAssertFalse(viewModel.incorrectPassword) XCTAssertTrue(viewModel.showError) @@ -98,7 +98,7 @@ final class DeleteAccountViewModelTests: XCTestCase { try await viewModel.deleteAccount(password: "123") Verify(interactor, 1, .deleteAccount(password: .any)) - Verify(router, 0, .showLoginScreen()) + Verify(router, 0, .showLoginScreen(sourceScreen: .any)) XCTAssertFalse(viewModel.incorrectPassword) XCTAssertTrue(viewModel.showError) diff --git a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift index ddc0f356f..c6888fcaf 100644 --- a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift @@ -14,17 +14,12 @@ import SwiftUI final class ProfileViewModelTests: XCTestCase { - func testGetMyProfileSuccess() async throws { + func testGetUserProfileSuccess() async throws { let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( + + let viewModel = UserProfileViewModel( interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity + username: "Steve" ) let user = UserProfile( @@ -38,12 +33,11 @@ final class ProfileViewModelTests: XCTestCase { isFullProfile: false ) - Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .getMyProfile(willReturn: user)) + Given(interactor, .getUserProfile(username: .value("Steve"), willReturn: user)) - await viewModel.getMyProfile() + await viewModel.getUserProfile() - Verify(interactor, 1, .getMyProfile()) + Verify(interactor, 1, .getUserProfile(username: .value("Steve"))) XCTAssertEqual(viewModel.userModel, user) XCTAssertFalse(viewModel.isShowProgress) @@ -51,7 +45,47 @@ final class ProfileViewModelTests: XCTestCase { XCTAssertNil(viewModel.errorMessage) } - func testGetMyProfileOfflineSuccess() async throws { + func testGetUserProfileNoInternetError() async throws { + let interactor = ProfileInteractorProtocolMock() + + let viewModel = UserProfileViewModel( + interactor: interactor, + username: "Steve" + ) + + let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) + + Given(interactor, .getUserProfile(username: .value("Steve"), willThrow: noInternetError)) + + await viewModel.getUserProfile() + + Verify(interactor, 1, .getUserProfile(username: .value("Steve"))) + + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) + XCTAssertFalse(viewModel.isShowProgress) + XCTAssertTrue(viewModel.showError) + } + + func testGetUserProfileUnknownError() async throws { + let interactor = ProfileInteractorProtocolMock() + + let viewModel = UserProfileViewModel( + interactor: interactor, + username: "Steve" + ) + + Given(interactor, .getUserProfile(username: .value("Steve"), willThrow: NSError())) + + await viewModel.getUserProfile() + + Verify(interactor, 1, .getUserProfile(username: .value("Steve"))) + + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) + XCTAssertFalse(viewModel.isShowProgress) + XCTAssertTrue(viewModel.showError) + } + + func testGetMyProfileSuccess() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() @@ -75,12 +109,13 @@ final class ProfileViewModelTests: XCTestCase { isFullProfile: false ) - Given(connectivity, .isInternetAvaliable(getter: false)) + Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getMyProfileOffline(willReturn: user)) + Given(interactor, .getMyProfile(willReturn: user)) await viewModel.getMyProfile() - Verify(interactor, 1, .getMyProfileOffline()) + Verify(interactor, 1, .getMyProfile()) XCTAssertEqual(viewModel.userModel, user) XCTAssertFalse(viewModel.isShowProgress) @@ -88,7 +123,7 @@ final class ProfileViewModelTests: XCTestCase { XCTAssertNil(viewModel.errorMessage) } - func testGetMyProfileNoInternetError() async throws { + func testGetMyProfileOfflineSuccess() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() @@ -101,21 +136,31 @@ final class ProfileViewModelTests: XCTestCase { connectivity: connectivity ) - let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) - - Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .getMyProfile(willThrow: noInternetError) ) + let user = UserProfile( + avatarUrl: "", + name: "Steve", + username: "Steve", + dateJoined: Date(), + yearOfBirth: 2000, + country: "Ua", + shortBiography: "Bio", + isFullProfile: false + ) + + Given(connectivity, .isInternetAvaliable(getter: false)) + Given(interactor, .getMyProfileOffline(willReturn: user)) await viewModel.getMyProfile() - Verify(interactor, 1, .getMyProfile()) + Verify(interactor, 1, .getMyProfileOffline()) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) + XCTAssertEqual(viewModel.userModel, user) XCTAssertFalse(viewModel.isShowProgress) - XCTAssertTrue(viewModel.showError) + XCTAssertFalse(viewModel.showError) + XCTAssertNil(viewModel.errorMessage) } - func testGetMyProfileNoCacheError() async throws { + func testGetMyProfileNoInternetError() async throws { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() let analytics = ProfileAnalyticsMock() @@ -128,8 +173,22 @@ final class ProfileViewModelTests: XCTestCase { connectivity: connectivity ) + let user = UserProfile( + avatarUrl: "", + name: "Steve", + username: "Steve", + dateJoined: Date(), + yearOfBirth: 2000, + country: "Ua", + shortBiography: "Bio", + isFullProfile: false + ) + + let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) + Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .getMyProfile(willThrow: NoCachedDataError())) + Given(interactor, .getMyProfileOffline(willReturn: user)) + Given(interactor, .getMyProfile(willThrow: noInternetError)) await viewModel.getMyProfile() @@ -179,60 +238,13 @@ final class ProfileViewModelTests: XCTestCase { ) Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .logOut(willProduce: {_ in})) await viewModel.logOut() - Verify(router, .showLoginScreen()) + Verify(router, .showStartupScreen()) XCTAssertFalse(viewModel.showError) } - func testLogOutNoInternetError() async throws { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) - - Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .logOut(willThrow: noInternetError)) - - await viewModel.logOut() - - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) - } - - func testLogOutUnknownError() async throws { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .logOut(willThrow: NSError())) - - await viewModel.logOut() - - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) - } - func testTrackProfileVideoSettingsClicked() { let interactor = ProfileInteractorProtocolMock() let router = ProfileRouterMock() @@ -322,5 +334,4 @@ final class ProfileViewModelTests: XCTestCase { Verify(analytics, 1, .profileEditClicked()) } - } diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index 8ebac614f..047a50d8d 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.8.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func login(externalToken: String, backend: String) throws -> User { + addInvocation(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) + let perform = methodPerformValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) as? (String, String) -> Void + perform?(`externalToken`, `backend`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) return Matcher.ComparisonResult(results) + case (.m_login__externalToken_externalTokenbackend_backend(let lhsExternaltoken, let lhsBackend), .m_login__externalToken_externalTokenbackend_backend(let rhsExternaltoken, let rhsBackend)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsExternaltoken, rhs: rhsExternaltoken, with: matcher), lhsExternaltoken, rhsExternaltoken, "externalToken")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) @@ -490,22 +539,28 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`controllers`) } - open func showMainScreen() { - addInvocation(.m_showMainScreen) - let perform = methodPerformValue(.m_showMainScreen) as? () -> Void - perform?() + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } - open func showLoginScreen() { - addInvocation(.m_showLoginScreen) - let perform = methodPerformValue(.m_showLoginScreen) as? () -> Void + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void perform?() } - open func showRegisterScreen() { - addInvocation(.m_showRegisterScreen) - let perform = methodPerformValue(.m_showRegisterScreen) as? () -> Void - perform?() + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } open func showForgotPasswordScreen() { @@ -514,6 +569,18 @@ open class BaseRouterMock: BaseRouter, Mock { perform?() } + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -545,10 +612,13 @@ open class BaseRouterMock: BaseRouter, Mock { case m_backWithFade case m_dismiss__animated_animated(Parameter) case m_removeLastView__controllers_controllers(Parameter) - case m_showMainScreen - case m_showLoginScreen - case m_showRegisterScreen + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) @@ -578,14 +648,37 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) return Matcher.ComparisonResult(results) - case (.m_showMainScreen, .m_showMainScreen): return .match + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) - case (.m_showLoginScreen, .m_showLoginScreen): return .match + case (.m_showStartupScreen, .m_showStartupScreen): return .match - case (.m_showRegisterScreen, .m_showRegisterScreen): return .match + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -630,10 +723,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return 0 case let .m_dismiss__animated_animated(p0): return p0.intValue case let .m_removeLastView__controllers_controllers(p0): return p0.intValue - case .m_showMainScreen: return 0 - case .m_showLoginScreen: return 0 - case .m_showRegisterScreen: return 0 + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue @@ -647,10 +743,13 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_backWithFade: return ".backWithFade()" case .m_dismiss__animated_animated: return ".dismiss(animated:)" case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" - case .m_showMainScreen: return ".showMainScreen()" - case .m_showLoginScreen: return ".showLoginScreen()" - case .m_showRegisterScreen: return ".showRegisterScreen()" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" @@ -678,10 +777,13 @@ open class BaseRouterMock: BaseRouter, Mock { public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} - public static func showMainScreen() -> Verify { return Verify(method: .m_showMainScreen)} - public static func showLoginScreen() -> Verify { return Verify(method: .m_showLoginScreen)} - public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} @@ -707,18 +809,27 @@ open class BaseRouterMock: BaseRouter, Mock { public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) } - public static func showMainScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showMainScreen, performs: perform) + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) } - public static func showLoginScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showLoginScreen, performs: perform) + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } - public static func showRegisterScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showRegisterScreen, performs: perform) + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showForgotPasswordScreen, performs: perform) } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1042,6 +1153,11 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var currentDownloadTask: DownloadDataTask? { + get { invocations.append(.p_currentDownloadTask_get); return __p_currentDownloadTask ?? optionalGivenGetterValue(.p_currentDownloadTask_get, "DownloadManagerProtocolMock - stub value for currentDownloadTask was not defined") } + } + private var __p_currentDownloadTask: (DownloadDataTask)? + @@ -1060,29 +1176,44 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func eventPublisher() -> AnyPublisher { + addInvocation(.m_eventPublisher) + let perform = methodPerformValue(.m_eventPublisher) as? () -> Void + perform?() + var __value: AnyPublisher do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void - } catch MockError.notStubed { - // do nothing + __value = try methodReturnValue(.m_eventPublisher).casted() } catch { - throw error + onFatalFailure("Stub return value not specified for eventPublisher(). Use given") + Failure("Stub return value not specified for eventPublisher(). Use given") } + return __value } - open func getDownloadsForCourse(_ courseId: String) -> [DownloadData] { - addInvocation(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) - let perform = methodPerformValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + open func getDownloadTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasks) + let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasks(). Use given") + Failure("Stub return value not specified for getDownloadTasks(). Use given") + } + return __value + } + + open func getDownloadTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void perform?(`courseId`) - var __value: [DownloadData] + var __value: [DownloadDataTask] do { - __value = try methodReturnValue(.m_getDownloadsForCourse__courseId(Parameter.value(`courseId`))).casted() + __value = try methodReturnValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))).casted() } catch { - onFatalFailure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") - Failure("Stub return value not specified for getDownloadsForCourse(_ courseId: String). Use given") + onFatalFailure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") } return __value } @@ -1100,12 +1231,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void - perform?() + open func cancelDownloading(task: DownloadDataTask) throws { + addInvocation(.m_cancelDownloading__task_task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_cancelDownloading__task_task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + _ = try methodReturnValue(.m_cancelDownloading__task_task(Parameter.value(`task`))).casted() as Void } catch MockError.notStubed { // do nothing } catch { @@ -1113,10 +1244,17 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } - open func pauseDownloading() { - addInvocation(.m_pauseDownloading) - let perform = methodPerformValue(.m_pauseDownloading) as? () -> Void - perform?() + open func cancelDownloading(courseId: String) throws { + addInvocation(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } } open func deleteFile(blocks: [CourseBlock]) { @@ -1144,28 +1282,72 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + addInvocation(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + Failure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + } + return __value + } + + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + fileprivate enum MethodType { case m_publisher - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_getDownloadsForCourse__courseId(Parameter) + case m_eventPublisher + case m_getDownloadTasks + case m_getDownloadTasksForCourse__courseId(Parameter) case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) - case m_resumeDownloading - case m_pauseDownloading + case m_cancelDownloading__task_task(Parameter) + case m_cancelDownloading__courseId_courseId(Parameter) case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_resumeDownloading + case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { case (.m_publisher, .m_publisher): return .match - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_eventPublisher, .m_eventPublisher): return .match - case (.m_getDownloadsForCourse__courseId(let lhsCourseid), .m_getDownloadsForCourse__courseId(let rhsCourseid)): + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match + + case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) return Matcher.ComparisonResult(results) @@ -1176,9 +1358,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) - case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.m_cancelDownloading__task_task(let lhsTask), .m_cancelDownloading__task_task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "task")) + return Matcher.ComparisonResult(results) - case (.m_pauseDownloading, .m_pauseDownloading): return .match + case (.m_cancelDownloading__courseId_courseId(let lhsCourseid), .m_cancelDownloading__courseId_courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + return Matcher.ComparisonResult(results) case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] @@ -1191,6 +1379,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_resumeDownloading, .m_resumeDownloading): return .match + case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } } @@ -1198,27 +1399,37 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { func intValue() -> Int { switch self { case .m_publisher: return 0 - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case let .m_getDownloadsForCourse__courseId(p0): return p0.intValue + case .m_eventPublisher: return 0 + case .m_getDownloadTasks: return 0 + case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue - case .m_resumeDownloading: return 0 - case .m_pauseDownloading: return 0 + case let .m_cancelDownloading__task_task(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_resumeDownloading: return 0 + case .p_currentDownloadTask_get: return 0 } } func assertionName() -> String { switch self { case .m_publisher: return ".publisher()" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_getDownloadsForCourse__courseId: return ".getDownloadsForCourse(_:)" + case .m_eventPublisher: return ".eventPublisher()" + case .m_getDownloadTasks: return ".getDownloadTasks()" + case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" - case .m_resumeDownloading: return ".resumeDownloading()" - case .m_pauseDownloading: return ".pauseDownloading()" + case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" + case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_resumeDownloading: return ".resumeDownloading()" + case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } } @@ -1231,16 +1442,28 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { super.init(products) } + public static func currentDownloadTask(getter defaultValue: DownloadDataTask?...) -> PropertyStub { + return Given(method: .p_currentDownloadTask_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willReturn: AnyPublisher...) -> MethodStub { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getDownloadsForCourse(_ courseId: Parameter, willReturn: [DownloadData]...) -> MethodStub { - return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func eventPublisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { let willReturn: [AnyPublisher] = [] let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -1248,10 +1471,24 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func getDownloadsForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadData]>) -> Void) -> MethodStub { - let willReturn: [[DownloadData]] = [] - let given: Given = { return Given(method: .m_getDownloadsForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([DownloadData]).self) + public static func eventPublisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func getDownloadTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) willProduce(stubber) return given } @@ -1262,13 +1499,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) willProduce(stubber) return given } @@ -1282,6 +1516,36 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func cancelDownloading(task: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(task: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } public static func resumeDownloading(willThrow: Error...) -> MethodStub { return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) } @@ -1298,14 +1562,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { fileprivate var method: MethodType public static func publisher() -> Verify { return Verify(method: .m_publisher)} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func getDownloadsForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadsForCourse__courseId(`courseId`))} + public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} + public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} - public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} - public static func pauseDownloading() -> Verify { return Verify(method: .m_pauseDownloading)} + public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} + public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } public struct Perform { @@ -1315,20 +1584,23 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func publisher(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_publisher, performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func eventPublisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_eventPublisher, performs: perform) } - public static func getDownloadsForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getDownloadsForCourse__courseId(`courseId`), performs: perform) + public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadTasks, performs: perform) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadTasksForCourse__courseId(`courseId`), performs: perform) } public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, perform: @escaping (String, [CourseBlock]) -> Void) -> Perform { return Perform(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + public static func cancelDownloading(task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__task_task(`task`), performs: perform) } - public static func pauseDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_pauseDownloading, performs: perform) + public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) } public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) @@ -1339,6 +1611,15 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) + } + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) + } } public func given(_ method: Given) { @@ -1738,6 +2019,22 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { + open func getUserProfile(username: String) throws -> UserProfile { + addInvocation(.m_getUserProfile__username_username(Parameter.value(`username`))) + let perform = methodPerformValue(.m_getUserProfile__username_username(Parameter.value(`username`))) as? (String) -> Void + perform?(`username`) + var __value: UserProfile + do { + __value = try methodReturnValue(.m_getUserProfile__username_username(Parameter.value(`username`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getUserProfile(username: String). Use given") + Failure("Stub return value not specified for getUserProfile(username: String). Use given") + } catch { + throw error + } + return __value + } + open func getMyProfile() throws -> UserProfile { addInvocation(.m_getMyProfile) let perform = methodPerformValue(.m_getMyProfile) as? () -> Void @@ -1754,18 +2051,15 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { return __value } - open func getMyProfileOffline() throws -> UserProfile { + open func getMyProfileOffline() -> UserProfile? { addInvocation(.m_getMyProfileOffline) let perform = methodPerformValue(.m_getMyProfileOffline) as? () -> Void perform?() - var __value: UserProfile + var __value: UserProfile? = nil do { __value = try methodReturnValue(.m_getMyProfileOffline).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getMyProfileOffline(). Use given") - Failure("Stub return value not specified for getMyProfileOffline(). Use given") } catch { - throw error + // do nothing } return __value } @@ -1894,6 +2188,7 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { fileprivate enum MethodType { + case m_getUserProfile__username_username(Parameter) case m_getMyProfile case m_getMyProfileOffline case m_logOut @@ -1908,6 +2203,11 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { + case (.m_getUserProfile__username_username(let lhsUsername), .m_getUserProfile__username_username(let rhsUsername)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUsername, rhs: rhsUsername, with: matcher), lhsUsername, rhsUsername, "username")) + return Matcher.ComparisonResult(results) + case (.m_getMyProfile, .m_getMyProfile): return .match case (.m_getMyProfileOffline, .m_getMyProfileOffline): return .match @@ -1947,6 +2247,7 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { func intValue() -> Int { switch self { + case let .m_getUserProfile__username_username(p0): return p0.intValue case .m_getMyProfile: return 0 case .m_getMyProfileOffline: return 0 case .m_logOut: return 0 @@ -1962,6 +2263,7 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { } func assertionName() -> String { switch self { + case .m_getUserProfile__username_username: return ".getUserProfile(username:)" case .m_getMyProfile: return ".getMyProfile()" case .m_getMyProfileOffline: return ".getMyProfileOffline()" case .m_logOut: return ".logOut()" @@ -1986,10 +2288,13 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { } + public static func getUserProfile(username: Parameter, willReturn: UserProfile...) -> MethodStub { + return Given(method: .m_getUserProfile__username_username(`username`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func getMyProfile(willReturn: UserProfile...) -> MethodStub { return Given(method: .m_getMyProfile, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getMyProfileOffline(willReturn: UserProfile...) -> MethodStub { + public static func getMyProfileOffline(willReturn: UserProfile?...) -> MethodStub { return Given(method: .m_getMyProfileOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func getSpokenLanguages(willReturn: [PickerFields.Option]...) -> MethodStub { @@ -2010,6 +2315,13 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { public static func getSettings(willReturn: UserSettings...) -> MethodStub { return Given(method: .m_getSettings, products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func getMyProfileOffline(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [UserProfile?] = [] + let given: Given = { return Given(method: .m_getMyProfileOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (UserProfile?).self) + willProduce(stubber) + return given + } public static func getSpokenLanguages(willProduce: (Stubber<[PickerFields.Option]>) -> Void) -> MethodStub { let willReturn: [[PickerFields.Option]] = [] let given: Given = { return Given(method: .m_getSpokenLanguages, products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -2031,22 +2343,22 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { willProduce(stubber) return given } - public static func getMyProfile(willThrow: Error...) -> MethodStub { - return Given(method: .m_getMyProfile, products: willThrow.map({ StubProduct.throw($0) })) + public static func getUserProfile(username: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getUserProfile__username_username(`username`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func getMyProfile(willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getUserProfile(username: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getMyProfile, products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getUserProfile__username_username(`username`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (UserProfile).self) willProduce(stubber) return given } - public static func getMyProfileOffline(willThrow: Error...) -> MethodStub { - return Given(method: .m_getMyProfileOffline, products: willThrow.map({ StubProduct.throw($0) })) + public static func getMyProfile(willThrow: Error...) -> MethodStub { + return Given(method: .m_getMyProfile, products: willThrow.map({ StubProduct.throw($0) })) } - public static func getMyProfileOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func getMyProfile(willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getMyProfileOffline, products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getMyProfile, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (UserProfile).self) willProduce(stubber) return given @@ -2106,6 +2418,7 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { public struct Verify { fileprivate var method: MethodType + public static func getUserProfile(username: Parameter) -> Verify { return Verify(method: .m_getUserProfile__username_username(`username`))} public static func getMyProfile() -> Verify { return Verify(method: .m_getMyProfile)} public static func getMyProfileOffline() -> Verify { return Verify(method: .m_getMyProfileOffline)} public static func logOut() -> Verify { return Verify(method: .m_logOut)} @@ -2123,6 +2436,9 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { fileprivate var method: MethodType var performs: Any + public static func getUserProfile(username: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getUserProfile__username_username(`username`), performs: perform) + } public static func getMyProfile(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getMyProfile, performs: perform) } @@ -2293,6 +2609,12 @@ open class ProfileRouterMock: ProfileRouter, Mock { perform?(`viewModel`) } + open func showVideoDownloadQualityView(downloadQuality: DownloadQuality, didSelect: ((DownloadQuality) -> Void)?) { + addInvocation(.m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelect(Parameter.value(`downloadQuality`), Parameter<((DownloadQuality) -> Void)?>.value(`didSelect`))) + let perform = methodPerformValue(.m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelect(Parameter.value(`downloadQuality`), Parameter<((DownloadQuality) -> Void)?>.value(`didSelect`))) as? (DownloadQuality, ((DownloadQuality) -> Void)?) -> Void + perform?(`downloadQuality`, `didSelect`) + } + open func showDeleteProfileView() { addInvocation(.m_showDeleteProfileView) let perform = methodPerformValue(.m_showDeleteProfileView) as? () -> Void @@ -2329,22 +2651,28 @@ open class ProfileRouterMock: ProfileRouter, Mock { perform?(`controllers`) } - open func showMainScreen() { - addInvocation(.m_showMainScreen) - let perform = methodPerformValue(.m_showMainScreen) as? () -> Void - perform?() + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } - open func showLoginScreen() { - addInvocation(.m_showLoginScreen) - let perform = methodPerformValue(.m_showLoginScreen) as? () -> Void + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void perform?() } - open func showRegisterScreen() { - addInvocation(.m_showRegisterScreen) - let perform = methodPerformValue(.m_showRegisterScreen) as? () -> Void - perform?() + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) } open func showForgotPasswordScreen() { @@ -2353,6 +2681,18 @@ open class ProfileRouterMock: ProfileRouter, Mock { perform?() } + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -2382,16 +2722,20 @@ open class ProfileRouterMock: ProfileRouter, Mock { case m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit(Parameter, Parameter, Parameter<((UserProfile?, UIImage?)) -> Void>) case m_showSettings case m_showVideoQualityView__viewModel_viewModel(Parameter) + case m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelect(Parameter, Parameter<((DownloadQuality) -> Void)?>) case m_showDeleteProfileView case m_backToRoot__animated_animated(Parameter) case m_back__animated_animated(Parameter) case m_backWithFade case m_dismiss__animated_animated(Parameter) case m_removeLastView__controllers_controllers(Parameter) - case m_showMainScreen - case m_showLoginScreen - case m_showRegisterScreen + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_view(Parameter, Parameter) @@ -2413,6 +2757,12 @@ open class ProfileRouterMock: ProfileRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsViewmodel, rhs: rhsViewmodel, with: matcher), lhsViewmodel, rhsViewmodel, "viewModel")) return Matcher.ComparisonResult(results) + case (.m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelect(let lhsDownloadquality, let lhsDidselect), .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelect(let rhsDownloadquality, let rhsDidselect)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDownloadquality, rhs: rhsDownloadquality, with: matcher), lhsDownloadquality, rhsDownloadquality, "downloadQuality")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDidselect, rhs: rhsDidselect, with: matcher), lhsDidselect, rhsDidselect, "didSelect")) + return Matcher.ComparisonResult(results) + case (.m_showDeleteProfileView, .m_showDeleteProfileView): return .match case (.m_backToRoot__animated_animated(let lhsAnimated), .m_backToRoot__animated_animated(let rhsAnimated)): @@ -2437,14 +2787,37 @@ open class ProfileRouterMock: ProfileRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) return Matcher.ComparisonResult(results) - case (.m_showMainScreen, .m_showMainScreen): return .match + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showStartupScreen, .m_showStartupScreen): return .match - case (.m_showLoginScreen, .m_showLoginScreen): return .match + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) - case (.m_showRegisterScreen, .m_showRegisterScreen): return .match + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -2487,16 +2860,20 @@ open class ProfileRouterMock: ProfileRouter, Mock { case let .m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case .m_showSettings: return 0 case let .m_showVideoQualityView__viewModel_viewModel(p0): return p0.intValue + case let .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelect(p0, p1): return p0.intValue + p1.intValue case .m_showDeleteProfileView: return 0 case let .m_backToRoot__animated_animated(p0): return p0.intValue case let .m_back__animated_animated(p0): return p0.intValue case .m_backWithFade: return 0 case let .m_dismiss__animated_animated(p0): return p0.intValue case let .m_removeLastView__controllers_controllers(p0): return p0.intValue - case .m_showMainScreen: return 0 - case .m_showLoginScreen: return 0 - case .m_showRegisterScreen: return 0 + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_view(p0, p1): return p0.intValue + p1.intValue @@ -2508,16 +2885,20 @@ open class ProfileRouterMock: ProfileRouter, Mock { case .m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit: return ".showEditProfile(userModel:avatar:profileDidEdit:)" case .m_showSettings: return ".showSettings()" case .m_showVideoQualityView__viewModel_viewModel: return ".showVideoQualityView(viewModel:)" + case .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelect: return ".showVideoDownloadQualityView(downloadQuality:didSelect:)" case .m_showDeleteProfileView: return ".showDeleteProfileView()" case .m_backToRoot__animated_animated: return ".backToRoot(animated:)" case .m_back__animated_animated: return ".back(animated:)" case .m_backWithFade: return ".backWithFade()" case .m_dismiss__animated_animated: return ".dismiss(animated:)" case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" - case .m_showMainScreen: return ".showMainScreen()" - case .m_showLoginScreen: return ".showLoginScreen()" - case .m_showRegisterScreen: return ".showRegisterScreen()" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_view: return ".presentView(transitionStyle:view:)" @@ -2543,16 +2924,20 @@ open class ProfileRouterMock: ProfileRouter, Mock { public static func showEditProfile(userModel: Parameter, avatar: Parameter, profileDidEdit: Parameter<((UserProfile?, UIImage?)) -> Void>) -> Verify { return Verify(method: .m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit(`userModel`, `avatar`, `profileDidEdit`))} public static func showSettings() -> Verify { return Verify(method: .m_showSettings)} public static func showVideoQualityView(viewModel: Parameter) -> Verify { return Verify(method: .m_showVideoQualityView__viewModel_viewModel(`viewModel`))} + public static func showVideoDownloadQualityView(downloadQuality: Parameter, didSelect: Parameter<((DownloadQuality) -> Void)?>) -> Verify { return Verify(method: .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelect(`downloadQuality`, `didSelect`))} public static func showDeleteProfileView() -> Verify { return Verify(method: .m_showDeleteProfileView)} public static func backToRoot(animated: Parameter) -> Verify { return Verify(method: .m_backToRoot__animated_animated(`animated`))} public static func back(animated: Parameter) -> Verify { return Verify(method: .m_back__animated_animated(`animated`))} public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} - public static func showMainScreen() -> Verify { return Verify(method: .m_showMainScreen)} - public static func showLoginScreen() -> Verify { return Verify(method: .m_showLoginScreen)} - public static func showRegisterScreen() -> Verify { return Verify(method: .m_showRegisterScreen)} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_view(`transitionStyle`, `view`))} @@ -2572,6 +2957,9 @@ open class ProfileRouterMock: ProfileRouter, Mock { public static func showVideoQualityView(viewModel: Parameter, perform: @escaping (SettingsViewModel) -> Void) -> Perform { return Perform(method: .m_showVideoQualityView__viewModel_viewModel(`viewModel`), performs: perform) } + public static func showVideoDownloadQualityView(downloadQuality: Parameter, didSelect: Parameter<((DownloadQuality) -> Void)?>, perform: @escaping (DownloadQuality, ((DownloadQuality) -> Void)?) -> Void) -> Perform { + return Perform(method: .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelect(`downloadQuality`, `didSelect`), performs: perform) + } public static func showDeleteProfileView(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showDeleteProfileView, performs: perform) } @@ -2590,18 +2978,27 @@ open class ProfileRouterMock: ProfileRouter, Mock { public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) } - public static func showMainScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showMainScreen, performs: perform) + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) } - public static func showLoginScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showLoginScreen, performs: perform) + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } - public static func showRegisterScreen(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_showRegisterScreen, performs: perform) + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) } public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showForgotPasswordScreen, performs: perform) } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } diff --git a/README.md b/README.md index 668a7a554..7586b4758 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Modern vision of the mobile application for the Open EdX platform from Raccoon G 4. Ensure that the ``OpenEdXDev`` or ``OpenEdXProd`` scheme is selected. -5. Configure the [``Environment.swift`` file](https://github.com/raccoongang/new-edx-app-ios/blob/main/OpenEdX/Environment.swift) with URLs and OAuth credentials for your Open edX instance. +5. Configure `config_settings.yaml` inside `default_config` and `config.yaml` inside sub direcroties to point to your OpenEdx configuration [Configuration Docuementation](./Documentation/CONFIGURATION_MANAGEMENT.md) 6. Click the **Run** button. diff --git a/Theme.xctestplan b/Theme.xctestplan new file mode 100644 index 000000000..5cf6c3343 --- /dev/null +++ b/Theme.xctestplan @@ -0,0 +1,25 @@ +{ + "configurations" : [ + { + "id" : "21564A5A-00BD-4BA9-BA30-6B8D4818E22A", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:Theme.xcodeproj", + "identifier" : "E0D6E6AD2B16EA380089F9C9", + "name" : "ThemeTests" + } + } + ], + "version" : 1 +} diff --git a/Theme/Info.plist b/Theme/Info.plist new file mode 100644 index 000000000..e2f9ca918 --- /dev/null +++ b/Theme/Info.plist @@ -0,0 +1,14 @@ + + + + + UIAppFonts + + SF-Pro.ttf + SF-Pro-Text-Bold.ttf + SF-Pro-Text-Medium.ttf + + Configuration + $(CONFIGURATION) + + diff --git a/Theme/Theme.xcodeproj.xcworkspace/contents.xcworkspacedata b/Theme/Theme.xcodeproj.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..beb62f4fd --- /dev/null +++ b/Theme/Theme.xcodeproj.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Theme/Theme.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Theme/Theme.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Theme/Theme.xcodeproj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Theme/Theme.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Theme/Theme.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..7dde19a07 --- /dev/null +++ b/Theme/Theme.xcodeproj.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "youtubeplayerkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SvenTiigi/YouTubePlayerKit", + "state" : { + "revision" : "1fe4c8b07a61d50c2fd276e1d9c8087583c7638a", + "version" : "1.5.3" + } + } + ], + "version" : 2 +} diff --git a/Theme/Theme.xcodeproj/project.pbxproj b/Theme/Theme.xcodeproj/project.pbxproj new file mode 100644 index 000000000..e67d7eb3d --- /dev/null +++ b/Theme/Theme.xcodeproj/project.pbxproj @@ -0,0 +1,1420 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 0295B1DC297FF114003B0C65 /* fonts_file.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0295B1DA297FF0E9003B0C65 /* fonts_file.ttf */; }; + 049A2F1F1CE68BB8A9373D0D /* Pods_App_Theme_ThemeTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52EBBA416BC61CD2B78879B7 /* Pods_App_Theme_ThemeTests.framework */; }; + 0770DE5428D0B00C006D8A5D /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE5328D0B00C006D8A5D /* swiftgen.yml */; }; + 0770DE7B28D0C78C006D8A5D /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE7A28D0C78C006D8A5D /* Theme.swift */; }; + 4875747BC644481E79603124 /* Pods_App_Theme.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51EFD5ECB23A4898127124B9 /* Pods_App_Theme.framework */; }; + E04A731A2B171D2B00E31764 /* FontParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09179F82B0EF981002AB695 /* FontParserTests.swift */; }; + E0C716A72B0B05BA00858BAB /* fonts.json in Resources */ = {isa = PBXBuildFile; fileRef = E0C716A62B0B05BA00858BAB /* fonts.json */; }; + E0C716A92B0B065600858BAB /* FontParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0C716A82B0B065600858BAB /* FontParser.swift */; }; + E0C716AB2B0B0B8E00858BAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E0C716AA2B0B0B8E00858BAB /* Assets.xcassets */; }; + E0D6E69F2B161BD70089F9C9 /* ThemeAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D6E69E2B161BD70089F9C9 /* ThemeAssets.swift */; }; + E0D6E6A72B16DD4E0089F9C9 /* RoundedCorners.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D6E6A62B16DD4E0089F9C9 /* RoundedCorners.swift */; }; + E0D6E6B22B16EA380089F9C9 /* Theme.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0770DE0828D07831006D8A5D /* Theme.framework */; platformFilter = ios; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + E0D6E6B32B16EA380089F9C9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0770DDFF28D07831006D8A5D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0770DE0728D07831006D8A5D; + remoteInfo = Theme; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0295B1DA297FF0E9003B0C65 /* fonts_file.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = fonts_file.ttf; sourceTree = ""; }; + 0754BB7841E3C0F8D6464951 /* Pods-App-Core.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releasestage.xcconfig"; sourceTree = ""; }; + 0770DE0828D07831006D8A5D /* Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0770DE5328D0B00C006D8A5D /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; + 0770DE7A28D0C78C006D8A5D /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + 0E13E9173C9C4CFC19F8B6F2 /* Pods-App-Core.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugstage.xcconfig"; sourceTree = ""; }; + 185C3037FC537A93E532FB5B /* Pods-App-Theme-ThemeTests.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme-ThemeTests.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests.releasestage.xcconfig"; sourceTree = ""; }; + 1A154A95AF4EE85A4A1C083B /* Pods-App-Core.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releasedev.xcconfig"; sourceTree = ""; }; + 21B4805F3A71E72020CC2412 /* Pods-App-Theme-ThemeTests.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme-ThemeTests.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests.debugstage.xcconfig"; sourceTree = ""; }; + 2B7E6FE7843FC4CF2BFA712D /* Pods-App-Core.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debug.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debug.xcconfig"; sourceTree = ""; }; + 3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = ""; }; + 3C05B03529F8351A9CE573A5 /* Pods-App-Theme-ThemeTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme-ThemeTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests.debugdev.xcconfig"; sourceTree = ""; }; + 3E75F15A0CFA31FEFFD74BDA /* Pods-App-Theme.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Theme/Pods-App-Theme.releasestage.xcconfig"; sourceTree = ""; }; + 46BF778BEF031F5C0C986D5B /* Pods-App-Theme.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Theme/Pods-App-Theme.debugstage.xcconfig"; sourceTree = ""; }; + 487962E20A82E0E0A3240D2D /* Pods-App-Theme-ThemeTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme-ThemeTests.debug.xcconfig"; path = "Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests.debug.xcconfig"; sourceTree = ""; }; + 4E492CB27902B082FC4770F7 /* Pods-App-Theme.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Theme/Pods-App-Theme.releaseprod.xcconfig"; sourceTree = ""; }; + 4E638A48662937AB3FD07407 /* Pods-App-Theme-ThemeTests.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme-ThemeTests.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests.debugprod.xcconfig"; sourceTree = ""; }; + 51EFD5ECB23A4898127124B9 /* Pods_App_Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 52EBBA416BC61CD2B78879B7 /* Pods_App_Theme_ThemeTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Theme_ThemeTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 588E75C5DF545DC8AAC2C10F /* Pods-App-Theme.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Theme/Pods-App-Theme.releasedev.xcconfig"; sourceTree = ""; }; + 5D02E092DEDAF8A9DE9D34E7 /* Pods-App-Theme.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Theme/Pods-App-Theme.debugprod.xcconfig"; sourceTree = ""; }; + 60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = ""; }; + 72B5AEE033EE8E4086910FAA /* Pods-App-Theme-ThemeTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme-ThemeTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests.releaseprod.xcconfig"; sourceTree = ""; }; + 7AFDB8C1E97070E426F05ABC /* Pods-App-Theme.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme.release.xcconfig"; path = "Target Support Files/Pods-App-Theme/Pods-App-Theme.release.xcconfig"; sourceTree = ""; }; + 8975BD5CEEDBF3A35A67D289 /* Pods-App-Theme.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme.debug.xcconfig"; path = "Target Support Files/Pods-App-Theme/Pods-App-Theme.debug.xcconfig"; sourceTree = ""; }; + 960BB69958C7376CD354D74F /* Pods-App-Theme.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Theme/Pods-App-Theme.debugdev.xcconfig"; sourceTree = ""; }; + 9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = ""; }; + A8316465C7A08B99CB3108AA /* Pods-App-Theme-ThemeTests.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme-ThemeTests.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests.releasedev.xcconfig"; sourceTree = ""; }; + C7E5BCE79CE297B20777B27A /* Pods-App-Core.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugprod.xcconfig"; sourceTree = ""; }; + E001ADD92B1767C000DE3A03 /* Theme.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Theme.xctestplan; path = ../Theme.xctestplan; sourceTree = ""; }; + E09179F82B0EF981002AB695 /* FontParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontParserTests.swift; sourceTree = ""; }; + E0C716A62B0B05BA00858BAB /* fonts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = fonts.json; sourceTree = ""; }; + E0C716A82B0B065600858BAB /* FontParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontParser.swift; sourceTree = ""; }; + E0C716AA2B0B0B8E00858BAB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E0D6E69E2B161BD70089F9C9 /* ThemeAssets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeAssets.swift; sourceTree = ""; }; + E0D6E6A62B16DD4E0089F9C9 /* RoundedCorners.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedCorners.swift; sourceTree = ""; }; + E0D6E6AE2B16EA380089F9C9 /* ThemeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ThemeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F6E09DC289758CE811EF93D8 /* Pods-App-Theme-ThemeTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Theme-ThemeTests.release.xcconfig"; path = "Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0770DE0528D07831006D8A5D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4875747BC644481E79603124 /* Pods_App_Theme.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E0D6E6AB2B16EA380089F9C9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E0D6E6B22B16EA380089F9C9 /* Theme.framework in Frameworks */, + 049A2F1F1CE68BB8A9373D0D /* Pods_App_Theme_ThemeTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0295B1DB297FF0E9003B0C65 /* Fonts */ = { + isa = PBXGroup; + children = ( + 0295B1DA297FF0E9003B0C65 /* fonts_file.ttf */, + E0C716A62B0B05BA00858BAB /* fonts.json */, + E0C716A82B0B065600858BAB /* FontParser.swift */, + ); + path = Fonts; + sourceTree = ""; + }; + 0770DDFE28D07831006D8A5D = { + isa = PBXGroup; + children = ( + E001ADD92B1767C000DE3A03 /* Theme.xctestplan */, + 0770DE5328D0B00C006D8A5D /* swiftgen.yml */, + 0770DE0A28D07831006D8A5D /* Theme */, + E09179FA2B0F204D002AB695 /* ThemeTests */, + 0770DE0928D07831006D8A5D /* Products */, + C9DFE47E699CFFA85A77AF2C /* Pods */, + F1620A3A2C8B0699EAA61B57 /* Frameworks */, + ); + sourceTree = ""; + }; + 0770DE0928D07831006D8A5D /* Products */ = { + isa = PBXGroup; + children = ( + 0770DE0828D07831006D8A5D /* Theme.framework */, + E0D6E6AE2B16EA380089F9C9 /* ThemeTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 0770DE0A28D07831006D8A5D /* Theme */ = { + isa = PBXGroup; + children = ( + E0D6E6A52B16DD440089F9C9 /* Helpers */, + 0295B1DB297FF0E9003B0C65 /* Fonts */, + 0770DE5528D0B142006D8A5D /* SwiftGen */, + 0770DE7A28D0C78C006D8A5D /* Theme.swift */, + E0C716AA2B0B0B8E00858BAB /* Assets.xcassets */, + ); + path = Theme; + sourceTree = ""; + }; + 0770DE5528D0B142006D8A5D /* SwiftGen */ = { + isa = PBXGroup; + children = ( + E0D6E69E2B161BD70089F9C9 /* ThemeAssets.swift */, + ); + path = SwiftGen; + sourceTree = ""; + }; + C9DFE47E699CFFA85A77AF2C /* Pods */ = { + isa = PBXGroup; + children = ( + 2B7E6FE7843FC4CF2BFA712D /* Pods-App-Core.debug.xcconfig */, + 60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */, + C7E5BCE79CE297B20777B27A /* Pods-App-Core.debugprod.xcconfig */, + 9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */, + 3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */, + 1A154A95AF4EE85A4A1C083B /* Pods-App-Core.releasedev.xcconfig */, + 0E13E9173C9C4CFC19F8B6F2 /* Pods-App-Core.debugstage.xcconfig */, + 0754BB7841E3C0F8D6464951 /* Pods-App-Core.releasestage.xcconfig */, + 8975BD5CEEDBF3A35A67D289 /* Pods-App-Theme.debug.xcconfig */, + 5D02E092DEDAF8A9DE9D34E7 /* Pods-App-Theme.debugprod.xcconfig */, + 46BF778BEF031F5C0C986D5B /* Pods-App-Theme.debugstage.xcconfig */, + 960BB69958C7376CD354D74F /* Pods-App-Theme.debugdev.xcconfig */, + 7AFDB8C1E97070E426F05ABC /* Pods-App-Theme.release.xcconfig */, + 4E492CB27902B082FC4770F7 /* Pods-App-Theme.releaseprod.xcconfig */, + 3E75F15A0CFA31FEFFD74BDA /* Pods-App-Theme.releasestage.xcconfig */, + 588E75C5DF545DC8AAC2C10F /* Pods-App-Theme.releasedev.xcconfig */, + 487962E20A82E0E0A3240D2D /* Pods-App-Theme-ThemeTests.debug.xcconfig */, + 4E638A48662937AB3FD07407 /* Pods-App-Theme-ThemeTests.debugprod.xcconfig */, + 21B4805F3A71E72020CC2412 /* Pods-App-Theme-ThemeTests.debugstage.xcconfig */, + 3C05B03529F8351A9CE573A5 /* Pods-App-Theme-ThemeTests.debugdev.xcconfig */, + F6E09DC289758CE811EF93D8 /* Pods-App-Theme-ThemeTests.release.xcconfig */, + 72B5AEE033EE8E4086910FAA /* Pods-App-Theme-ThemeTests.releaseprod.xcconfig */, + 185C3037FC537A93E532FB5B /* Pods-App-Theme-ThemeTests.releasestage.xcconfig */, + A8316465C7A08B99CB3108AA /* Pods-App-Theme-ThemeTests.releasedev.xcconfig */, + ); + name = Pods; + path = ../Pods; + sourceTree = ""; + }; + E09179FA2B0F204D002AB695 /* ThemeTests */ = { + isa = PBXGroup; + children = ( + E09179F82B0EF981002AB695 /* FontParserTests.swift */, + ); + path = ThemeTests; + sourceTree = ""; + }; + E0D6E6A52B16DD440089F9C9 /* Helpers */ = { + isa = PBXGroup; + children = ( + E0D6E6A62B16DD4E0089F9C9 /* RoundedCorners.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + F1620A3A2C8B0699EAA61B57 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 51EFD5ECB23A4898127124B9 /* Pods_App_Theme.framework */, + 52EBBA416BC61CD2B78879B7 /* Pods_App_Theme_ThemeTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 0770DE0328D07831006D8A5D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 0770DE0728D07831006D8A5D /* Theme */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0770DE0F28D07831006D8A5D /* Build configuration list for PBXNativeTarget "Theme" */; + buildPhases = ( + ED83AD5255805030E042D62A /* [CP] Check Pods Manifest.lock */, + 0770DE5A28D0B1E5006D8A5D /* SwiftGen */, + 0770DE0328D07831006D8A5D /* Headers */, + 0770DE0428D07831006D8A5D /* Sources */, + 0770DE0528D07831006D8A5D /* Frameworks */, + 0770DE0628D07831006D8A5D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Theme; + productName = Core; + productReference = 0770DE0828D07831006D8A5D /* Theme.framework */; + productType = "com.apple.product-type.framework"; + }; + E0D6E6AD2B16EA380089F9C9 /* ThemeTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E0D6E6B52B16EA380089F9C9 /* Build configuration list for PBXNativeTarget "ThemeTests" */; + buildPhases = ( + 9014A6045E55D36F2E521E4D /* [CP] Check Pods Manifest.lock */, + E0D6E6AA2B16EA380089F9C9 /* Sources */, + E0D6E6AB2B16EA380089F9C9 /* Frameworks */, + E0D6E6AC2B16EA380089F9C9 /* Resources */, + D1CBF7594325D41C982947CB /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + E0D6E6B42B16EA380089F9C9 /* PBXTargetDependency */, + ); + name = ThemeTests; + productName = ThemeTests; + productReference = E0D6E6AE2B16EA380089F9C9 /* ThemeTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0770DDFF28D07831006D8A5D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1400; + TargetAttributes = { + 0770DE0728D07831006D8A5D = { + CreatedOnToolsVersion = 14.0; + LastSwiftMigration = 1400; + }; + E0D6E6AD2B16EA380089F9C9 = { + CreatedOnToolsVersion = 15.0.1; + }; + }; + }; + buildConfigurationList = 0770DE0228D07831006D8A5D /* Build configuration list for PBXProject "Theme" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + uk, + ); + mainGroup = 0770DDFE28D07831006D8A5D; + productRefGroup = 0770DE0928D07831006D8A5D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0770DE0728D07831006D8A5D /* Theme */, + E0D6E6AD2B16EA380089F9C9 /* ThemeTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0770DE0628D07831006D8A5D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E0C716AB2B0B0B8E00858BAB /* Assets.xcassets in Resources */, + 0295B1DC297FF114003B0C65 /* fonts_file.ttf in Resources */, + 0770DE5428D0B00C006D8A5D /* swiftgen.yml in Resources */, + E0C716A72B0B05BA00858BAB /* fonts.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E0D6E6AC2B16EA380089F9C9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0770DE5A28D0B1E5006D8A5D /* SwiftGen */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftGen; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\"\nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; + }; + 9014A6045E55D36F2E521E4D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-App-Theme-ThemeTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D1CBF7594325D41C982947CB /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-Theme-ThemeTests/Pods-App-Theme-ThemeTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + ED83AD5255805030E042D62A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-App-Theme-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0770DE0428D07831006D8A5D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E0D6E6A72B16DD4E0089F9C9 /* RoundedCorners.swift in Sources */, + E0C716A92B0B065600858BAB /* FontParser.swift in Sources */, + 0770DE7B28D0C78C006D8A5D /* Theme.swift in Sources */, + E0D6E69F2B161BD70089F9C9 /* ThemeAssets.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E0D6E6AA2B16EA380089F9C9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E04A731A2B171D2B00E31764 /* FontParserTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + E0D6E6B42B16EA380089F9C9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 0770DE0728D07831006D8A5D /* Theme */; + targetProxy = E0D6E6B32B16EA380089F9C9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 02DD1C9929E80CE400F35DCE /* DebugStage */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugStage; + }; + 02DD1C9A29E80CE400F35DCE /* DebugStage */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 46BF778BEF031F5C0C986D5B /* Pods-App-Theme.debugstage.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Theme; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugStage; + }; + 02DD1C9C29E80CED00F35DCE /* ReleaseStage */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseStage; + }; + 02DD1C9D29E80CED00F35DCE /* ReleaseStage */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3E75F15A0CFA31FEFFD74BDA /* Pods-App-Theme.releasestage.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Theme; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseStage; + }; + 0727875C28D2326B002E9142 /* DebugDev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugDev; + }; + 0727875D28D2326B002E9142 /* DebugDev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 960BB69958C7376CD354D74F /* Pods-App-Theme.debugdev.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Theme; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugDev; + }; + 0727875E28D23272002E9142 /* ReleaseDev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseDev; + }; + 0727875F28D23272002E9142 /* ReleaseDev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 588E75C5DF545DC8AAC2C10F /* Pods-App-Theme.releasedev.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Theme; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseDev; + }; + 0727876028D23277002E9142 /* DebugProd */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugProd; + }; + 0727876128D23277002E9142 /* DebugProd */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5D02E092DEDAF8A9DE9D34E7 /* Pods-App-Theme.debugprod.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Theme; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugProd; + }; + 0727876228D2327C002E9142 /* ReleaseProd */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseProd; + }; + 0727876328D2327C002E9142 /* ReleaseProd */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4E492CB27902B082FC4770F7 /* Pods-App-Theme.releaseprod.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Theme; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseProd; + }; + 0770DE0D28D07831006D8A5D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 0770DE0E28D07831006D8A5D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 0770DE1028D07831006D8A5D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8975BD5CEEDBF3A35A67D289 /* Pods-App-Theme.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Theme; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0770DE1128D07831006D8A5D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFDB8C1E97070E426F05ABC /* Pods-App-Theme.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.Theme; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + E0D6E6B62B16EA380089F9C9 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 487962E20A82E0E0A3240D2D /* Pods-App-Theme-ThemeTests.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ThemeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E0D6E6B72B16EA380089F9C9 /* DebugProd */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4E638A48662937AB3FD07407 /* Pods-App-Theme-ThemeTests.debugprod.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ThemeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugProd; + }; + E0D6E6B82B16EA380089F9C9 /* DebugStage */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 21B4805F3A71E72020CC2412 /* Pods-App-Theme-ThemeTests.debugstage.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ThemeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugStage; + }; + E0D6E6B92B16EA380089F9C9 /* DebugDev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3C05B03529F8351A9CE573A5 /* Pods-App-Theme-ThemeTests.debugdev.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ThemeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugDev; + }; + E0D6E6BA2B16EA380089F9C9 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F6E09DC289758CE811EF93D8 /* Pods-App-Theme-ThemeTests.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ThemeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + E0D6E6BB2B16EA380089F9C9 /* ReleaseProd */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 72B5AEE033EE8E4086910FAA /* Pods-App-Theme-ThemeTests.releaseprod.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ThemeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseProd; + }; + E0D6E6BC2B16EA380089F9C9 /* ReleaseStage */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 185C3037FC537A93E532FB5B /* Pods-App-Theme-ThemeTests.releasestage.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ThemeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseStage; + }; + E0D6E6BD2B16EA380089F9C9 /* ReleaseDev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A8316465C7A08B99CB3108AA /* Pods-App-Theme-ThemeTests.releasedev.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ThemeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseDev; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0770DE0228D07831006D8A5D /* Build configuration list for PBXProject "Theme" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0770DE0D28D07831006D8A5D /* Debug */, + 0727876028D23277002E9142 /* DebugProd */, + 02DD1C9929E80CE400F35DCE /* DebugStage */, + 0727875C28D2326B002E9142 /* DebugDev */, + 0770DE0E28D07831006D8A5D /* Release */, + 0727876228D2327C002E9142 /* ReleaseProd */, + 02DD1C9C29E80CED00F35DCE /* ReleaseStage */, + 0727875E28D23272002E9142 /* ReleaseDev */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0770DE0F28D07831006D8A5D /* Build configuration list for PBXNativeTarget "Theme" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0770DE1028D07831006D8A5D /* Debug */, + 0727876128D23277002E9142 /* DebugProd */, + 02DD1C9A29E80CE400F35DCE /* DebugStage */, + 0727875D28D2326B002E9142 /* DebugDev */, + 0770DE1128D07831006D8A5D /* Release */, + 0727876328D2327C002E9142 /* ReleaseProd */, + 02DD1C9D29E80CED00F35DCE /* ReleaseStage */, + 0727875F28D23272002E9142 /* ReleaseDev */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E0D6E6B52B16EA380089F9C9 /* Build configuration list for PBXNativeTarget "ThemeTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E0D6E6B62B16EA380089F9C9 /* Debug */, + E0D6E6B72B16EA380089F9C9 /* DebugProd */, + E0D6E6B82B16EA380089F9C9 /* DebugStage */, + E0D6E6B92B16EA380089F9C9 /* DebugDev */, + E0D6E6BA2B16EA380089F9C9 /* Release */, + E0D6E6BB2B16EA380089F9C9 /* ReleaseProd */, + E0D6E6BC2B16EA380089F9C9 /* ReleaseStage */, + E0D6E6BD2B16EA380089F9C9 /* ReleaseDev */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0770DDFF28D07831006D8A5D /* Project object */; +} diff --git a/Theme/Theme.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Theme/Theme.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..2c83c0667 --- /dev/null +++ b/Theme/Theme.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Theme/Theme.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Theme/Theme.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Theme/Theme.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Theme/Theme.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Theme/Theme.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..7dde19a07 --- /dev/null +++ b/Theme/Theme.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "youtubeplayerkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SvenTiigi/YouTubePlayerKit", + "state" : { + "revision" : "1fe4c8b07a61d50c2fd276e1d9c8087583c7638a", + "version" : "1.5.3" + } + } + ], + "version" : 2 +} diff --git a/Theme/Theme.xcodeproj/xcshareddata/xcschemes/Theme.xcscheme b/Theme/Theme.xcodeproj/xcshareddata/xcschemes/Theme.xcscheme new file mode 100644 index 000000000..39b9ba3cd --- /dev/null +++ b/Theme/Theme.xcodeproj/xcshareddata/xcschemes/Theme.xcscheme @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/Colors/Snackbar/Contents.json b/Theme/Theme/Assets.xcassets/Auth/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/Snackbar/Contents.json rename to Theme/Theme/Assets.xcassets/Auth/Contents.json diff --git a/Core/Core/Assets.xcassets/Auth/authBackground.imageset/Contents.json b/Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Auth/authBackground.imageset/Contents.json rename to Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Contents.json diff --git a/Core/Core/Assets.xcassets/Auth/authBackground.imageset/Rectangle-2.png b/Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle-2.png similarity index 100% rename from Core/Core/Assets.xcassets/Auth/authBackground.imageset/Rectangle-2.png rename to Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle-2.png diff --git a/Core/Core/Assets.xcassets/Auth/authBackground.imageset/Rectangle.png b/Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle.png similarity index 100% rename from Core/Core/Assets.xcassets/Auth/authBackground.imageset/Rectangle.png rename to Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle.png diff --git a/Core/Core/Assets.xcassets/Colors/AccentColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/AccentColor.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..00d59cb46 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.408", + "red" : "0.235" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.976", + "green" : "0.471", + "red" : "0.329" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/Alert.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/Alert.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/Alert.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/Alert.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/AvatarStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AvatarStroke.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/AvatarStroke.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/AvatarStroke.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/Background.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/Background.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/BackgroundStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/BackgroundStroke.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/BackgroundStroke.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/BackgroundStroke.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/StyledButton/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CardView/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/StyledButton/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/CardView/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/CertificateForeground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CertificateForeground.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/CertificateForeground.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/CertificateForeground.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/TextColor/Contents.json b/Theme/Theme/Assets.xcassets/Colors/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/TextColor/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Colors/LoginBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/LoginBackground.colorset/Contents.json new file mode 100644 index 000000000..8fef18d07 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/LoginBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.184", + "green" : "0.129", + "red" : "0.098" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/StyledButton/StyledButtonText.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/LoginNavigationText.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/StyledButton/StyledButtonText.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/LoginNavigationText.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/ShadowColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/ShadowColor.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/ShadowColor.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/ShadowColor.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/TextInput/Contents.json b/Theme/Theme/Assets.xcassets/Colors/Snackbar/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/TextInput/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/Snackbar/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarErrorColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/Snackbar/SnackbarErrorColor.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarErrorColor.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/Snackbar/SnackbarErrorColor.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarErrorTextColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/Snackbar/SnackbarErrorTextColor.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarErrorTextColor.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/Snackbar/SnackbarErrorTextColor.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarInfoAlert.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/Snackbar/SnackbarInfoAlert.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/Snackbar/SnackbarInfoAlert.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/Snackbar/SnackbarInfoAlert.colorset/Contents.json diff --git a/OpenEdX/Assets.xcassets/SplachBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SplashBackground.colorset/Contents.json similarity index 100% rename from OpenEdX/Assets.xcassets/SplachBackground.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/SplashBackground.colorset/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Colors/StyledButton/Contents.json b/Theme/Theme/Assets.xcassets/Colors/StyledButton/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/StyledButton/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/StyledButton/StyledButtonBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/StyledButton/StyledButtonBackground.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/StyledButton/StyledButtonBackground.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/StyledButton/StyledButtonBackground.colorset/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Colors/StyledButton/StyledButtonText.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/StyledButton/StyledButtonText.colorset/Contents.json new file mode 100644 index 000000000..22c4bb0a8 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/StyledButton/StyledButtonText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/TextColor/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextColor/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/TextColor/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/TextColor/TextPrimary.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextColor/TextPrimary.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/TextColor/TextPrimary.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/TextColor/TextPrimary.colorset/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json new file mode 100644 index 000000000..93691d3e8 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.733", + "green" : "0.647", + "red" : "0.592" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.624", + "green" : "0.533", + "red" : "0.475" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/TextInput/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextInput/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/TextInput/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/TextInput/TextInputBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputBackground.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/TextInput/TextInputBackground.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputBackground.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/TextInput/TextInputStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputStroke.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/TextInput/TextInputStroke.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputStroke.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/TextInput/TextInputUnfocusedBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputUnfocusedBackground.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/TextInput/TextInputUnfocusedBackground.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputUnfocusedBackground.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/TextInput/TextInputUnfocusedStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputUnfocusedStroke.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/TextInput/TextInputUnfocusedStroke.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputUnfocusedStroke.colorset/Contents.json diff --git a/Core/Core/Assets.xcassets/Colors/warning.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/warning.colorset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/Colors/warning.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/warning.colorset/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Colors/white.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/white.colorset/Contents.json new file mode 100644 index 000000000..22c4bb0a8 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/white.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Contents.json b/Theme/Theme/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/CourseDates/Contents.json b/Theme/Theme/Assets.xcassets/CourseDates/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/CourseDates/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/CourseDates/DatesSectionBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/CourseDates/DatesSectionBackground.colorset/Contents.json new file mode 100644 index 000000000..b73d804ea --- /dev/null +++ b/Theme/Theme/Assets.xcassets/CourseDates/DatesSectionBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.984", + "green" : "0.980", + "red" : "0.976" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.275", + "green" : "0.200", + "red" : "0.153" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/CourseDates/DatesSectionStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/CourseDates/DatesSectionStroke.colorset/Contents.json new file mode 100644 index 000000000..432fab345 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/CourseDates/DatesSectionStroke.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.878", + "green" : "0.831", + "red" : "0.800" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.439", + "green" : "0.353", + "red" : "0.306" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/CourseDates/NextWeekTimelineColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/CourseDates/NextWeekTimelineColor.colorset/Contents.json new file mode 100644 index 000000000..5cd29db93 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/CourseDates/NextWeekTimelineColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.733", + "green" : "0.647", + "red" : "0.592" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.439", + "green" : "0.353", + "red" : "0.306" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/CourseDates/ThisWeekTimelineColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/CourseDates/ThisWeekTimelineColor.colorset/Contents.json new file mode 100644 index 000000000..abd91436e --- /dev/null +++ b/Theme/Theme/Assets.xcassets/CourseDates/ThisWeekTimelineColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.392", + "green" : "0.286", + "red" : "0.239" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.624", + "green" : "0.533", + "red" : "0.475" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/CourseDates/TodayTimelineColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/CourseDates/TodayTimelineColor.colorset/Contents.json new file mode 100644 index 000000000..3e35599d4 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/CourseDates/TodayTimelineColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.667", + "red" : "0.259" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.584", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/CourseDates/UpcomingTimelineColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/CourseDates/UpcomingTimelineColor.colorset/Contents.json new file mode 100644 index 000000000..2af3cc3c3 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/CourseDates/UpcomingTimelineColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.878", + "green" : "0.831", + "red" : "0.800" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.275", + "green" : "0.200", + "red" : "0.153" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/CourseDates/pastDueTimelineColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/CourseDates/pastDueTimelineColor.colorset/Contents.json new file mode 100644 index 000000000..a72efa461 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/CourseDates/pastDueTimelineColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.302", + "green" : "0.788", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.282", + "green" : "0.761", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/ProgressLine/Contents.json b/Theme/Theme/Assets.xcassets/ProgressLine/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/ProgressLine/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OpenEdX/Assets.xcassets/AccentColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/ProgressLine/OnProgress.colorset/Contents.json similarity index 76% rename from OpenEdX/Assets.xcassets/AccentColor.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/ProgressLine/OnProgress.colorset/Contents.json index 1ffc23315..3c81fe0e7 100644 --- a/OpenEdX/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/ProgressLine/OnProgress.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x68", - "red" : "0x3C" + "blue" : "0x00", + "green" : "0xCC", + "red" : "0xF0" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x68", - "red" : "0x3C" + "blue" : "0x00", + "green" : "0xCC", + "red" : "0xF0" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/ProgressLine/ProgressDone.colorset/Contents.json b/Theme/Theme/Assets.xcassets/ProgressLine/ProgressDone.colorset/Contents.json new file mode 100644 index 000000000..8e2a9063e --- /dev/null +++ b/Theme/Theme/Assets.xcassets/ProgressLine/ProgressDone.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD7", + "green" : "0xE6", + "red" : "0xBB" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x71", + "green" : "0xA1", + "red" : "0x30" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/ProgressLine/ProgressSkip.colorset/Contents.json b/Theme/Theme/Assets.xcassets/ProgressLine/ProgressSkip.colorset/Contents.json new file mode 100644 index 000000000..4cfea1bac --- /dev/null +++ b/Theme/Theme/Assets.xcassets/ProgressLine/ProgressSkip.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDA", + "green" : "0xD9", + "red" : "0xDD" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDA", + "green" : "0xD9", + "red" : "0xDD" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/ProgressLine/SelectedAndDone.colorset/Contents.json b/Theme/Theme/Assets.xcassets/ProgressLine/SelectedAndDone.colorset/Contents.json new file mode 100644 index 000000000..65b3786f0 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/ProgressLine/SelectedAndDone.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x71", + "green" : "0xA1", + "red" : "0x30" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x4D", + "green" : "0x7D", + "red" : "0x0D" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/appLogo.imageset/Contents.json b/Theme/Theme/Assets.xcassets/appLogo.imageset/Contents.json similarity index 100% rename from Core/Core/Assets.xcassets/appLogo.imageset/Contents.json rename to Theme/Theme/Assets.xcassets/appLogo.imageset/Contents.json diff --git a/Core/Core/Assets.xcassets/appLogo.imageset/Frame 4 1.svg b/Theme/Theme/Assets.xcassets/appLogo.imageset/Frame 4 1.svg similarity index 100% rename from Core/Core/Assets.xcassets/appLogo.imageset/Frame 4 1.svg rename to Theme/Theme/Assets.xcassets/appLogo.imageset/Frame 4 1.svg diff --git a/Theme/Theme/Fonts/FontParser.swift b/Theme/Theme/Fonts/FontParser.swift new file mode 100644 index 000000000..cd2adbba6 --- /dev/null +++ b/Theme/Theme/Fonts/FontParser.swift @@ -0,0 +1,61 @@ +// +// FontParser.swift +// Core +// +// Created by SaeedBashir on 11/20/23. +// + +import Foundation + +public enum FontIdentifier: String { + case regular, medium, semiBold, bold +} + +public class FontParser { + private var fonts: [String: String] = [:] + + public init() { + fonts = loadANdParseFonts() + } + + private func loadANdParseFonts() -> [String: String] { + if let path = Bundle(for: ThemeBundle.self).path(forResource: "fonts", ofType: "json") { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) + let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) + if let fonts = jsonResult as? [String: String] { + return fonts + } + } catch { + return fallbackFonts() + } + } + return fallbackFonts() + } + + @discardableResult public func fallbackFonts() -> [String: String] { + return FontsDataFactory.fonts + } + + public func fontName(for identiifer: FontIdentifier) -> String { + guard let fontName = fonts[identiifer.rawValue] else { + assert(false, "Could not find the required font in fonts.json") + return FontIdentifier.regular.rawValue + } + + return fontName + } +} + +struct FontsDataFactory { + static let fonts: [String: String] = [ + "regular": "SFPro-Regular", + "medium": "SFPro-Medium", + "semiBold": "SFPro-Semibold", + "bold": "SFPro-Bold" + ] +} + +public final class ThemeBundle { + private init() {} +} diff --git a/Theme/Theme/Fonts/fonts.json b/Theme/Theme/Fonts/fonts.json new file mode 100644 index 000000000..030a32bc3 --- /dev/null +++ b/Theme/Theme/Fonts/fonts.json @@ -0,0 +1,6 @@ +{ + "regular": "SFPro-Regular", + "medium": "SFPro-Medium", + "semiBold": "SFPro-Semibold", + "bold": "SFPro-Bold" +} diff --git a/Core/Core/Fonts/SF-Pro.ttf b/Theme/Theme/Fonts/fonts_file.ttf similarity index 100% rename from Core/Core/Fonts/SF-Pro.ttf rename to Theme/Theme/Fonts/fonts_file.ttf diff --git a/Core/Core/View/Base/RoundedCorners.swift b/Theme/Theme/Helpers/RoundedCorners.swift similarity index 100% rename from Core/Core/View/Base/RoundedCorners.swift rename to Theme/Theme/Helpers/RoundedCorners.swift diff --git a/Theme/Theme/SwiftGen/ThemeAssets.swift b/Theme/Theme/SwiftGen/ThemeAssets.swift new file mode 100644 index 000000000..f5fedb009 --- /dev/null +++ b/Theme/Theme/SwiftGen/ThemeAssets.swift @@ -0,0 +1,227 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +#if os(macOS) + import AppKit +#elseif os(iOS) + import UIKit +#elseif os(tvOS) || os(watchOS) + import UIKit +#endif +#if canImport(SwiftUI) + import SwiftUI +#endif + +// Deprecated typealiases +@available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") +public typealias AssetColorTypeAlias = ColorAsset.Color +@available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0") +public typealias AssetImageTypeAlias = ImageAsset.Image + +// swiftlint:disable superfluous_disable_command file_length implicit_return + +// MARK: - Asset Catalogs + +// swiftlint:disable identifier_name line_length nesting type_body_length type_name +public enum ThemeAssets { + public static let authBackground = ImageAsset(name: "authBackground") + public static let accentButtonColor = ColorAsset(name: "AccentButtonColor") + public static let accentColor = ColorAsset(name: "AccentColor") + public static let alert = ColorAsset(name: "Alert") + public static let avatarStroke = ColorAsset(name: "AvatarStroke") + public static let background = ColorAsset(name: "Background") + public static let backgroundStroke = ColorAsset(name: "BackgroundStroke") + public static let cardViewBackground = ColorAsset(name: "CardViewBackground") + public static let cardViewStroke = ColorAsset(name: "CardViewStroke") + public static let certificateForeground = ColorAsset(name: "CertificateForeground") + public static let commentCellBackground = ColorAsset(name: "CommentCellBackground") + public static let loginBackground = ColorAsset(name: "LoginBackground") + public static let loginNavigationText = ColorAsset(name: "LoginNavigationText") + public static let shadowColor = ColorAsset(name: "ShadowColor") + public static let snackbarErrorColor = ColorAsset(name: "SnackbarErrorColor") + public static let snackbarErrorTextColor = ColorAsset(name: "SnackbarErrorTextColor") + public static let snackbarInfoAlert = ColorAsset(name: "SnackbarInfoAlert") + public static let splashBackground = ColorAsset(name: "SplashBackground") + public static let styledButtonBackground = ColorAsset(name: "StyledButtonBackground") + public static let styledButtonText = ColorAsset(name: "StyledButtonText") + public static let textPrimary = ColorAsset(name: "TextPrimary") + public static let textSecondary = ColorAsset(name: "TextSecondary") + public static let textInputBackground = ColorAsset(name: "TextInputBackground") + public static let textInputStroke = ColorAsset(name: "TextInputStroke") + public static let textInputUnfocusedBackground = ColorAsset(name: "TextInputUnfocusedBackground") + public static let textInputUnfocusedStroke = ColorAsset(name: "TextInputUnfocusedStroke") + public static let warning = ColorAsset(name: "warning") + public static let white = ColorAsset(name: "white") + public static let datesSectionBackground = ColorAsset(name: "DatesSectionBackground") + public static let datesSectionStroke = ColorAsset(name: "DatesSectionStroke") + public static let nextWeekTimelineColor = ColorAsset(name: "NextWeekTimelineColor") + public static let thisWeekTimelineColor = ColorAsset(name: "ThisWeekTimelineColor") + public static let todayTimelineColor = ColorAsset(name: "TodayTimelineColor") + public static let upcomingTimelineColor = ColorAsset(name: "UpcomingTimelineColor") + public static let pastDueTimelineColor = ColorAsset(name: "pastDueTimelineColor") + public static let onProgress = ColorAsset(name: "OnProgress") + public static let progressDone = ColorAsset(name: "ProgressDone") + public static let progressSkip = ColorAsset(name: "ProgressSkip") + public static let selectedAndDone = ColorAsset(name: "SelectedAndDone") + public static let appLogo = ImageAsset(name: "appLogo") +} +// swiftlint:enable identifier_name line_length nesting type_body_length type_name + +// MARK: - Implementation Details + +public final class ColorAsset { + public fileprivate(set) var name: String + + #if os(macOS) + public typealias Color = NSColor + #elseif os(iOS) || os(tvOS) || os(watchOS) + public typealias Color = UIColor + #endif + + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) + public private(set) lazy var color: Color = { + guard let color = Color(asset: self) else { + fatalError("Unable to load color asset named \(name).") + } + return color + }() + + #if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + public func color(compatibleWith traitCollection: UITraitCollection) -> Color { + let bundle = BundleToken.bundle + guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load color asset named \(name).") + } + return color + } + #endif + + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + public private(set) lazy var swiftUIColor: SwiftUI.Color = { + SwiftUI.Color(asset: self) + }() + #endif + + fileprivate init(name: String) { + self.name = name + } +} + +public extension ColorAsset.Color { + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) + convenience init?(asset: ColorAsset) { + let bundle = BundleToken.bundle + #if os(iOS) || os(tvOS) + self.init(named: asset.name, in: bundle, compatibleWith: nil) + #elseif os(macOS) + self.init(named: NSColor.Name(asset.name), bundle: bundle) + #elseif os(watchOS) + self.init(named: asset.name) + #endif + } +} + +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +public extension SwiftUI.Color { + init(asset: ColorAsset) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle) + } +} +#endif + +public struct ImageAsset { + public fileprivate(set) var name: String + + #if os(macOS) + public typealias Image = NSImage + #elseif os(iOS) || os(tvOS) || os(watchOS) + public typealias Image = UIImage + #endif + + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) + public var image: Image { + let bundle = BundleToken.bundle + #if os(iOS) || os(tvOS) + let image = Image(named: name, in: bundle, compatibleWith: nil) + #elseif os(macOS) + let name = NSImage.Name(self.name) + let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name) + #elseif os(watchOS) + let image = Image(named: name) + #endif + guard let result = image else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + + #if os(iOS) || os(tvOS) + @available(iOS 8.0, tvOS 9.0, *) + public func image(compatibleWith traitCollection: UITraitCollection) -> Image { + let bundle = BundleToken.bundle + guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + #endif + + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + public var swiftUIImage: SwiftUI.Image { + SwiftUI.Image(asset: self) + } + #endif +} + +public extension ImageAsset.Image { + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) + @available(macOS, deprecated, + message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") + convenience init?(asset: ImageAsset) { + #if os(iOS) || os(tvOS) + let bundle = BundleToken.bundle + self.init(named: asset.name, in: bundle, compatibleWith: nil) + #elseif os(macOS) + self.init(named: NSImage.Name(asset.name)) + #elseif os(watchOS) + self.init(named: asset.name) + #endif + } +} + +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +public extension SwiftUI.Image { + init(asset: ImageAsset) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle) + } + + init(asset: ImageAsset, label: Text) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle, label: label) + } + + init(decorative asset: ImageAsset) { + let bundle = BundleToken.bundle + self.init(decorative: asset.name, bundle: bundle) + } +} +#endif + +// swiftlint:disable convenience_type +private final class BundleToken { + static let bundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif + }() +} +// swiftlint:enable convenience_type diff --git a/Theme/Theme/Theme.swift b/Theme/Theme/Theme.swift new file mode 100644 index 000000000..63937d812 --- /dev/null +++ b/Theme/Theme/Theme.swift @@ -0,0 +1,210 @@ +// +// Theme.swift +// Theme +// +// Created by Saeed Bashir on 28.11.2023. +// + +import Foundation +import SwiftUI + +private var fontsParser = FontParser() + +public struct Theme { + + public struct Colors { + public private(set) static var accentColor = ThemeAssets.accentColor.swiftUIColor + public private(set) static var accentButtonColor = ThemeAssets.accentButtonColor.swiftUIColor + public private(set) static var alert = ThemeAssets.alert.swiftUIColor + public private(set) static var avatarStroke = ThemeAssets.avatarStroke.swiftUIColor + public private(set) static var background = ThemeAssets.background.swiftUIColor + public private(set) static var loginBackground = ThemeAssets.loginBackground.swiftUIColor + public private(set) static var backgroundStroke = ThemeAssets.backgroundStroke.swiftUIColor + public private(set) static var cardViewBackground = ThemeAssets.cardViewBackground.swiftUIColor + public private(set) static var cardViewStroke = ThemeAssets.cardViewStroke.swiftUIColor + public private(set) static var certificateForeground = ThemeAssets.certificateForeground.swiftUIColor + public private(set) static var commentCellBackground = ThemeAssets.commentCellBackground.swiftUIColor + public private(set) static var nextWeekTimelineColor = ThemeAssets.nextWeekTimelineColor.swiftUIColor + public private(set) static var pastDueTimelineColor = ThemeAssets.pastDueTimelineColor.swiftUIColor + public private(set) static var thisWeekTimelineColor = ThemeAssets.thisWeekTimelineColor.swiftUIColor + public private(set) static var todayTimelineColor = ThemeAssets.todayTimelineColor.swiftUIColor + public private(set) static var upcomingTimelineColor = ThemeAssets.upcomingTimelineColor.swiftUIColor + public private(set) static var shadowColor = ThemeAssets.shadowColor.swiftUIColor + public private(set) static var snackbarErrorColor = ThemeAssets.snackbarErrorColor.swiftUIColor + public private(set) static var snackbarErrorTextColor = ThemeAssets.snackbarErrorTextColor.swiftUIColor + public private(set) static var snackbarInfoAlert = ThemeAssets.snackbarInfoAlert.swiftUIColor + public private(set) static var styledButtonBackground = ThemeAssets.styledButtonBackground.swiftUIColor + public private(set) static var styledButtonText = ThemeAssets.styledButtonText.swiftUIColor + public private(set) static var textPrimary = ThemeAssets.textPrimary.swiftUIColor + public private(set) static var textSecondary = ThemeAssets.textSecondary.swiftUIColor + public private(set) static var textInputBackground = ThemeAssets.textInputBackground.swiftUIColor + public private(set) static var textInputStroke = ThemeAssets.textInputStroke.swiftUIColor + public private(set) static var textInputUnfocusedBackground = ThemeAssets.textInputUnfocusedBackground.swiftUIColor + public private(set) static var textInputUnfocusedStroke = ThemeAssets.textInputUnfocusedStroke.swiftUIColor + public private(set) static var warning = ThemeAssets.warning.swiftUIColor + public private(set) static var white = ThemeAssets.white.swiftUIColor + public private(set) static var onProgress = ThemeAssets.onProgress.swiftUIColor + public private(set) static var progressDone = ThemeAssets.progressDone.swiftUIColor + public private(set) static var progressSkip = ThemeAssets.progressSkip.swiftUIColor + public private(set) static var progressSelectedAndDone = ThemeAssets.selectedAndDone.swiftUIColor + public private(set) static var loginNavigationText = ThemeAssets.loginNavigationText.swiftUIColor + public private(set) static var datesSectionBackground = ThemeAssets.datesSectionBackground.swiftUIColor + public private(set) static var datesSectionStroke = ThemeAssets.datesSectionStroke.swiftUIColor + + public static func update( + accentColor: Color = ThemeAssets.accentColor.swiftUIColor, + alert: Color = ThemeAssets.alert.swiftUIColor, + avatarStroke: Color = ThemeAssets.avatarStroke.swiftUIColor, + background: Color = ThemeAssets.background.swiftUIColor, + backgroundStroke: Color = ThemeAssets.backgroundStroke.swiftUIColor, + cardViewBackground: Color = ThemeAssets.cardViewBackground.swiftUIColor, + cardViewStroke: Color = ThemeAssets.cardViewStroke.swiftUIColor, + certificateForeground: Color = ThemeAssets.certificateForeground.swiftUIColor, + commentCellBackground: Color = ThemeAssets.commentCellBackground.swiftUIColor, + nextWeekTimelineColor: Color = ThemeAssets.nextWeekTimelineColor.swiftUIColor, + pastDueTimelineColor: Color = ThemeAssets.pastDueTimelineColor.swiftUIColor, + thisWeekTimelineColor: Color = ThemeAssets.thisWeekTimelineColor.swiftUIColor, + todayTimelineColor: Color = ThemeAssets.todayTimelineColor.swiftUIColor, + upcomingTimelineColor: Color = ThemeAssets.upcomingTimelineColor.swiftUIColor, + shadowColor: Color = ThemeAssets.shadowColor.swiftUIColor, + snackbarErrorColor: Color = ThemeAssets.snackbarErrorColor.swiftUIColor, + snackbarErrorTextColor: Color = ThemeAssets.snackbarErrorTextColor.swiftUIColor, + snackbarInfoAlert: Color = ThemeAssets.snackbarInfoAlert.swiftUIColor, + styledButtonBackground: Color = ThemeAssets.styledButtonBackground.swiftUIColor, + styledButtonText: Color = ThemeAssets.styledButtonText.swiftUIColor, + textPrimary: Color = ThemeAssets.textPrimary.swiftUIColor, + textSecondary: Color = ThemeAssets.textSecondary.swiftUIColor, + textInputBackground: Color = ThemeAssets.textInputBackground.swiftUIColor, + textInputStroke: Color = ThemeAssets.textInputStroke.swiftUIColor, + textInputUnfocusedBackground: Color = ThemeAssets.textInputUnfocusedBackground.swiftUIColor, + textInputUnfocusedStroke: Color = ThemeAssets.textInputUnfocusedStroke.swiftUIColor, + warning: Color = ThemeAssets.warning.swiftUIColor, + white: Color = ThemeAssets.white.swiftUIColor, + onProgress: Color = ThemeAssets.onProgress.swiftUIColor, + progressDone: Color = ThemeAssets.progressDone.swiftUIColor, + progressSkip: Color = ThemeAssets.progressSkip.swiftUIColor, + datesSectionBackground: Color = ThemeAssets.datesSectionBackground.swiftUIColor, + datesSectionStroke: Color = ThemeAssets.datesSectionStroke.swiftUIColor + ) { + self.accentColor = accentColor + self.alert = alert + self.avatarStroke = avatarStroke + self.background = background + self.backgroundStroke = backgroundStroke + self.cardViewBackground = cardViewBackground + self.cardViewStroke = cardViewStroke + self.certificateForeground = certificateForeground + self.commentCellBackground = commentCellBackground + self.nextWeekTimelineColor = nextWeekTimelineColor + self.pastDueTimelineColor = pastDueTimelineColor + self.thisWeekTimelineColor = thisWeekTimelineColor + self.todayTimelineColor = todayTimelineColor + self.upcomingTimelineColor = upcomingTimelineColor + self.shadowColor = shadowColor + self.snackbarErrorColor = snackbarErrorColor + self.snackbarErrorTextColor = snackbarErrorTextColor + self.snackbarInfoAlert = snackbarInfoAlert + self.styledButtonBackground = styledButtonBackground + self.styledButtonText = styledButtonText + self.textPrimary = textPrimary + self.textSecondary = textSecondary + self.textInputBackground = textInputBackground + self.textInputStroke = textInputStroke + self.textInputUnfocusedBackground = textInputUnfocusedBackground + self.textInputUnfocusedStroke = textInputUnfocusedStroke + self.warning = warning + self.white = white + self.onProgress = onProgress + self.progressDone = progressDone + self.progressSkip = progressSkip + self.datesSectionBackground = datesSectionBackground + self.datesSectionStroke = datesSectionStroke + } + } + + // Use this structure where the computed Color.uiColor() extension is not appropriate. + public struct UIColors { + public private(set) static var textPrimary = ThemeAssets.textPrimary.color + public private(set) static var accentColor = ThemeAssets.accentColor.color + + public static func update( + textPrimary: UIColor = ThemeAssets.textPrimary.color, + accentColor: UIColor = ThemeAssets.accentColor.color + ) { + self.textPrimary = textPrimary + self.accentColor = accentColor + } + } + + public struct Fonts { + + public static let displayLarge: Font = .custom(fontsParser.fontName(for: .regular), size: 57) + public static let displayMedium: Font = .custom(fontsParser.fontName(for: .regular), size: 45) + public static let displaySmall: Font = .custom(fontsParser.fontName(for: .bold), size: 36) + + public static let headlineLarge: Font = .custom(fontsParser.fontName(for: .regular), size: 32) + public static let headlineMedium: Font = .custom(fontsParser.fontName(for: .regular), size: 28) + public static let headlineSmall: Font = .custom(fontsParser.fontName(for: .regular), size: 24) + + public static let titleLarge: Font = .custom(fontsParser.fontName(for: .bold), size: 22) + public static let titleMedium: Font = .custom(fontsParser.fontName(for: .semiBold), size: 18) + public static let titleSmall: Font = .custom(fontsParser.fontName(for: .medium), size: 14) + + public static let bodyLarge: Font = .custom(fontsParser.fontName(for: .regular), size: 16) + public static let bodyMedium: Font = .custom(fontsParser.fontName(for: .regular), size: 14) + public static let bodySmall: Font = .custom(fontsParser.fontName(for: .regular), size: 12) + + public static let labelLarge: Font = .custom(fontsParser.fontName(for: .medium), size: 14) + public static let labelMedium: Font = .custom(fontsParser.fontName(for: .regular), size: 12) + public static let labelSmall: Font = .custom(fontsParser.fontName(for: .regular), size: 10) + } + + public struct Shapes { + public static var isRoundedCorners: Bool = true + public static let screenBackgroundRadius = 24.0 + public static let cardImageRadius = 10.0 + public static let textInputShape = { + let radius: CGFloat = isRoundedCorners ? 8 : 0 + return RoundedRectangle(cornerRadius: radius) + }() + public static let buttonShape = { + let radius: CGFloat = isRoundedCorners ? 8 : 0 + return RoundedCorners(tl: radius, tr: radius, bl: radius, br: radius) + }() + public static let unitButtonShape = RoundedCorners(tl: 21, tr: 21, bl: 21, br: 21) + public static let roundedScreenBackgroundShape = RoundedCorners( + tl: Theme.Shapes.screenBackgroundRadius, + tr: Theme.Shapes.screenBackgroundRadius, + bl: Theme.Shapes.screenBackgroundRadius, + br: Theme.Shapes.screenBackgroundRadius + ) + public static let roundedScreenBackgroundShapeCroppedBottom = RoundedCorners( + tl: Theme.Shapes.screenBackgroundRadius, + tr: Theme.Shapes.screenBackgroundRadius + ) + public static let cardShape = RoundedCorners(tl: 12, tr: 12, bl: 12, br: 12) + } + + public struct Timeout { + public static let snackbarMessageShortTimeout: TimeInterval = 3 + public static let snackbarMessageLongTimeout: TimeInterval = 5 + } + +} + +public extension Theme.Fonts { + // swiftlint:disable type_name + class __ {} + static func registerFonts() { + guard let url = Bundle(for: __.self).url(forResource: "fonts_file", withExtension: "ttf") else { return } + CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil) + } + // swiftlint:enable type_name +} + +extension View { + public func loadFonts() -> some View { + Theme.Fonts.registerFonts() + return self + } +} diff --git a/Theme/ThemeTests/FontParserTests.swift b/Theme/ThemeTests/FontParserTests.swift new file mode 100644 index 000000000..ba989a6c3 --- /dev/null +++ b/Theme/ThemeTests/FontParserTests.swift @@ -0,0 +1,32 @@ +// +// FontParserTests.swift +// ThemeTests +// +// Created by SaeedBashir on 11/23/23. +// + +import Foundation +import XCTest +@testable import Theme + +class FontParserTests: XCTestCase { + let fontParser = FontParser() + + func testFontFileExistence() { + let filePath: String? = Bundle(for: ThemeBundle.self).path(forResource: "fonts", ofType: "json") + XCTAssertNotNil(filePath) + XCTAssertTrue(FileManager.default.fileExists(atPath: filePath ?? "")) + } + + func testFontDataFactory() { + fontParser.fallbackFonts() + XCTAssertNotNil(fontParser.fontName(for: .regular)) + } + + func testFontParsing() { + XCTAssertNotNil(fontParser.fontName(for: .regular)) + XCTAssertNotNil(fontParser.fontName(for: .medium)) + XCTAssertNotNil(fontParser.fontName(for: .semiBold)) + XCTAssertNotNil(fontParser.fontName(for: .bold)) + } +} diff --git a/Theme/swiftgen.yml b/Theme/swiftgen.yml new file mode 100644 index 000000000..ec5f05e3c --- /dev/null +++ b/Theme/swiftgen.yml @@ -0,0 +1,9 @@ +xcassets: + inputs: + - Theme/Assets.xcassets + outputs: + templateName: swift5 + params: + publicAccess: true + enumName: ThemeAssets + output: Theme/SwiftGen/ThemeAssets.swift diff --git a/WhatsNew/.gitignore b/WhatsNew/.gitignore new file mode 100644 index 000000000..9c22c8b85 --- /dev/null +++ b/WhatsNew/.gitignore @@ -0,0 +1,99 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/* +/WhatsNew.xcodeproj/xcuserdata/ +/WhatsNew.xcworkspace/xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## R.swift +R.generated.swift + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +.DS_Store +.idea +xcode-frameworks diff --git a/WhatsNew/Mockfile b/WhatsNew/Mockfile new file mode 100644 index 000000000..3fee3de2b --- /dev/null +++ b/WhatsNew/Mockfile @@ -0,0 +1,17 @@ +sourceryCommand: mint run krzysztofzablocki/Sourcery@2.1.2 sourcery +sourceryTemplate: ../MockTemplate.swifttemplate +unit.tests.mock: + sources: + include: + - ./../WhatsNew + - ./WhatsNew + exclude: [] + output: ./WhatsNewTests/WhatsNewMock.generated.swift + targets: + - MyAppUnitTests + import: + - Core + - WhatsNew + - Foundation + - SwiftUI + - Combine \ No newline at end of file diff --git a/WhatsNew/WhatsNew.xcodeproj/project.pbxproj b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj new file mode 100644 index 000000000..dad830243 --- /dev/null +++ b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj @@ -0,0 +1,1499 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 020A7B5F2AE131A9000BAF70 /* WhatsNewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020A7B5E2AE131A9000BAF70 /* WhatsNewModel.swift */; }; + 020A7B612AE136D2000BAF70 /* WhatsNew.json in Resources */ = {isa = PBXBuildFile; fileRef = 020A7B602AE136D2000BAF70 /* WhatsNew.json */; }; + 020AC2692AEBB69E0086E975 /* WhatsNewMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020AC2682AEBB69E0086E975 /* WhatsNewMock.generated.swift */; }; + 028A37262ADFF3F8008CA604 /* WhatsNew.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 028A371D2ADFF3F7008CA604 /* WhatsNew.framework */; }; + 028A372B2ADFF3F8008CA604 /* WhatsNewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028A372A2ADFF3F8008CA604 /* WhatsNewTests.swift */; }; + 028A373A2ADFF425008CA604 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 028A37392ADFF425008CA604 /* Core.framework */; }; + 02B54E0D2AE0331F00C56962 /* WhatsNewNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B54E0C2AE0331F00C56962 /* WhatsNewNavigationButton.swift */; }; + 02B54E0F2AE0337800C56962 /* PageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B54E0E2AE0337800C56962 /* PageControl.swift */; }; + 02B54E112AE061C100C56962 /* WhatsNewRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B54E102AE061C100C56962 /* WhatsNewRouter.swift */; }; + 02E640792ADFF5920079AEDA /* WhatsNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E640782ADFF5920079AEDA /* WhatsNewView.swift */; }; + 02E6407C2ADFF6250079AEDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 02E6407E2ADFF6250079AEDA /* Localizable.strings */; }; + 02E640812ADFFE440079AEDA /* WhatsNewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E640802ADFFE440079AEDA /* WhatsNewViewModel.swift */; }; + 02E640862ADFFF380079AEDA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 02E640852ADFFF380079AEDA /* Assets.xcassets */; }; + 02E6408A2AE004300079AEDA /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E640892AE004300079AEDA /* Strings.swift */; }; + 02E6408C2AE006680079AEDA /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 02E6408B2AE006680079AEDA /* swiftgen.yml */; }; + 02EC90AA2AE904E1007DE1E0 /* WhatsNewStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EC90A92AE904E1007DE1E0 /* WhatsNewStorage.swift */; }; + 02EC90AC2AE90C64007DE1E0 /* WhatsNewPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EC90AB2AE90C64007DE1E0 /* WhatsNewPage.swift */; }; + B3BB9B06B226989A619C6440 /* Pods_App_WhatsNew.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05AC45C7050E30F8394E0C76 /* Pods_App_WhatsNew.framework */; }; + EF5CA11A55CB49F2DA030D25 /* Pods_App_WhatsNew_WhatsNewTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8D1A5DF016EC4630637336C /* Pods_App_WhatsNew_WhatsNewTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 028A37272ADFF3F8008CA604 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 028A37142ADFF3F7008CA604 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 028A371C2ADFF3F7008CA604; + remoteInfo = WhatsNew; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 020A7B5E2AE131A9000BAF70 /* WhatsNewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewModel.swift; sourceTree = ""; }; + 020A7B602AE136D2000BAF70 /* WhatsNew.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = WhatsNew.json; path = WhatsNew/Data/WhatsNew.json; sourceTree = SOURCE_ROOT; }; + 020AC2682AEBB69E0086E975 /* WhatsNewMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WhatsNewMock.generated.swift; path = WhatsNewTests/WhatsNewMock.generated.swift; sourceTree = SOURCE_ROOT; }; + 028A371D2ADFF3F7008CA604 /* WhatsNew.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WhatsNew.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 028A37252ADFF3F7008CA604 /* WhatsNewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WhatsNewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 028A372A2ADFF3F8008CA604 /* WhatsNewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewTests.swift; sourceTree = ""; }; + 028A37392ADFF425008CA604 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 02B54E0C2AE0331F00C56962 /* WhatsNewNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewNavigationButton.swift; sourceTree = ""; }; + 02B54E0E2AE0337800C56962 /* PageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageControl.swift; sourceTree = ""; }; + 02B54E102AE061C100C56962 /* WhatsNewRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewRouter.swift; sourceTree = ""; }; + 02E640782ADFF5920079AEDA /* WhatsNewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewView.swift; sourceTree = ""; }; + 02E6407D2ADFF6250079AEDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 02E6407F2ADFF6270079AEDA /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + 02E640802ADFFE440079AEDA /* WhatsNewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewViewModel.swift; sourceTree = ""; }; + 02E640852ADFFF380079AEDA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 02E640892AE004300079AEDA /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; + 02E6408B2AE006680079AEDA /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; + 02EC90A92AE904E1007DE1E0 /* WhatsNewStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewStorage.swift; sourceTree = ""; }; + 02EC90AB2AE90C64007DE1E0 /* WhatsNewPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewPage.swift; sourceTree = ""; }; + 02EC90B12AE91BF1007DE1E0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05AC45C7050E30F8394E0C76 /* Pods_App_WhatsNew.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_WhatsNew.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0C01007F0E8CEDCD293E0A68 /* Pods-App-WhatsNew.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew.debug.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew/Pods-App-WhatsNew.debug.xcconfig"; sourceTree = ""; }; + 1E3F4487E7D3A48F5FD12DDA /* Pods-App-WhatsNew-WhatsNewTests.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew-WhatsNewTests.releasestage.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests.releasestage.xcconfig"; sourceTree = ""; }; + 34C1F2BEAF7F0DCB8E630F33 /* Pods-App-WhatsNew.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew.releasestage.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew/Pods-App-WhatsNew.releasestage.xcconfig"; sourceTree = ""; }; + 365FD817D70DFBCBDE2EAE5F /* Pods-App-WhatsNew-WhatsNewTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew-WhatsNewTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests.releaseprod.xcconfig"; sourceTree = ""; }; + 4CB92C9DBA730A1B06B076BA /* Pods-App-WhatsNew.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew.release.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew/Pods-App-WhatsNew.release.xcconfig"; sourceTree = ""; }; + 58DF8140E3B3436F58C4C8B9 /* Pods-App-WhatsNew-WhatsNewTests.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew-WhatsNewTests.releasedev.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests.releasedev.xcconfig"; sourceTree = ""; }; + 6F50A409FBCCC7C08712A25E /* Pods-App-WhatsNew.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew.debugdev.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew/Pods-App-WhatsNew.debugdev.xcconfig"; sourceTree = ""; }; + 9176CDC000731B73D2F10372 /* Pods-App-WhatsNew.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew.debugstage.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew/Pods-App-WhatsNew.debugstage.xcconfig"; sourceTree = ""; }; + 9844714991FA40ECDC228CC9 /* Pods-App-WhatsNew.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew.releasedev.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew/Pods-App-WhatsNew.releasedev.xcconfig"; sourceTree = ""; }; + A2CABA11F5E7F89EFD9A05AC /* Pods-App-WhatsNew-WhatsNewTests.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew-WhatsNewTests.debugstage.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests.debugstage.xcconfig"; sourceTree = ""; }; + A557A3CED4D6327AAE6AA02C /* Pods-App-WhatsNew-WhatsNewTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew-WhatsNewTests.release.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests.release.xcconfig"; sourceTree = ""; }; + A76975E21FF282D59CEC4452 /* Pods-App-WhatsNew.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew.debugprod.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew/Pods-App-WhatsNew.debugprod.xcconfig"; sourceTree = ""; }; + AB8156676C9C771D691ADE07 /* Pods-App-WhatsNew.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew/Pods-App-WhatsNew.releaseprod.xcconfig"; sourceTree = ""; }; + B7EAC5E8F0ED2F1F81050C30 /* Pods-App-WhatsNew-WhatsNewTests.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew-WhatsNewTests.debugprod.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests.debugprod.xcconfig"; sourceTree = ""; }; + E905D28DCAA1940E08C96896 /* Pods-App-WhatsNew-WhatsNewTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew-WhatsNewTests.debug.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests.debug.xcconfig"; sourceTree = ""; }; + F71207762CEF1763A08C7151 /* Pods-App-WhatsNew-WhatsNewTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-WhatsNew-WhatsNewTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests.debugdev.xcconfig"; sourceTree = ""; }; + F8D1A5DF016EC4630637336C /* Pods_App_WhatsNew_WhatsNewTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_WhatsNew_WhatsNewTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 028A371A2ADFF3F7008CA604 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 028A373A2ADFF425008CA604 /* Core.framework in Frameworks */, + B3BB9B06B226989A619C6440 /* Pods_App_WhatsNew.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 028A37222ADFF3F7008CA604 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 028A37262ADFF3F8008CA604 /* WhatsNew.framework in Frameworks */, + EF5CA11A55CB49F2DA030D25 /* Pods_App_WhatsNew_WhatsNewTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 020A7B5D2AE1317E000BAF70 /* Domain */ = { + isa = PBXGroup; + children = ( + 02EC90AB2AE90C64007DE1E0 /* WhatsNewPage.swift */, + ); + path = Domain; + sourceTree = ""; + }; + 028A37132ADFF3F7008CA604 = { + isa = PBXGroup; + children = ( + 02E6408B2AE006680079AEDA /* swiftgen.yml */, + 028A371F2ADFF3F7008CA604 /* WhatsNew */, + 028A37292ADFF3F8008CA604 /* WhatsNewTests */, + 028A371E2ADFF3F7008CA604 /* Products */, + 028A37382ADFF425008CA604 /* Frameworks */, + 3397DFC72A3A62728BCA5367 /* Pods */, + ); + sourceTree = ""; + }; + 028A371E2ADFF3F7008CA604 /* Products */ = { + isa = PBXGroup; + children = ( + 028A371D2ADFF3F7008CA604 /* WhatsNew.framework */, + 028A37252ADFF3F7008CA604 /* WhatsNewTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 028A371F2ADFF3F7008CA604 /* WhatsNew */ = { + isa = PBXGroup; + children = ( + 02E640722ADFF54E0079AEDA /* SwiftGen */, + 02E640822ADFFEB00079AEDA /* Data */, + 020A7B5D2AE1317E000BAF70 /* Domain */, + 02E640752ADFF5700079AEDA /* Presentation */, + 02E6407E2ADFF6250079AEDA /* Localizable.strings */, + 02E640852ADFFF380079AEDA /* Assets.xcassets */, + 02EC90B12AE91BF1007DE1E0 /* Info.plist */, + ); + path = WhatsNew; + sourceTree = ""; + }; + 028A37292ADFF3F8008CA604 /* WhatsNewTests */ = { + isa = PBXGroup; + children = ( + 02EC90B52AE92AEB007DE1E0 /* Presentation */, + 020AC2682AEBB69E0086E975 /* WhatsNewMock.generated.swift */, + ); + path = WhatsNewTests; + sourceTree = ""; + }; + 028A37382ADFF425008CA604 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 028A37392ADFF425008CA604 /* Core.framework */, + 05AC45C7050E30F8394E0C76 /* Pods_App_WhatsNew.framework */, + F8D1A5DF016EC4630637336C /* Pods_App_WhatsNew_WhatsNewTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 02B54E0B2AE0330F00C56962 /* Elements */ = { + isa = PBXGroup; + children = ( + 02B54E0C2AE0331F00C56962 /* WhatsNewNavigationButton.swift */, + 02B54E0E2AE0337800C56962 /* PageControl.swift */, + ); + path = Elements; + sourceTree = ""; + }; + 02E640722ADFF54E0079AEDA /* SwiftGen */ = { + isa = PBXGroup; + children = ( + 02E640892AE004300079AEDA /* Strings.swift */, + ); + path = SwiftGen; + sourceTree = ""; + }; + 02E640752ADFF5700079AEDA /* Presentation */ = { + isa = PBXGroup; + children = ( + 02B54E0B2AE0330F00C56962 /* Elements */, + 02E640782ADFF5920079AEDA /* WhatsNewView.swift */, + 02E640802ADFFE440079AEDA /* WhatsNewViewModel.swift */, + 02B54E102AE061C100C56962 /* WhatsNewRouter.swift */, + ); + path = Presentation; + sourceTree = ""; + }; + 02E640822ADFFEB00079AEDA /* Data */ = { + isa = PBXGroup; + children = ( + 020A7B5E2AE131A9000BAF70 /* WhatsNewModel.swift */, + 020A7B602AE136D2000BAF70 /* WhatsNew.json */, + 02EC90A92AE904E1007DE1E0 /* WhatsNewStorage.swift */, + ); + path = Data; + sourceTree = ""; + }; + 02EC90B52AE92AEB007DE1E0 /* Presentation */ = { + isa = PBXGroup; + children = ( + 028A372A2ADFF3F8008CA604 /* WhatsNewTests.swift */, + ); + path = Presentation; + sourceTree = ""; + }; + 3397DFC72A3A62728BCA5367 /* Pods */ = { + isa = PBXGroup; + children = ( + 0C01007F0E8CEDCD293E0A68 /* Pods-App-WhatsNew.debug.xcconfig */, + 4CB92C9DBA730A1B06B076BA /* Pods-App-WhatsNew.release.xcconfig */, + 9176CDC000731B73D2F10372 /* Pods-App-WhatsNew.debugstage.xcconfig */, + A76975E21FF282D59CEC4452 /* Pods-App-WhatsNew.debugprod.xcconfig */, + 6F50A409FBCCC7C08712A25E /* Pods-App-WhatsNew.debugdev.xcconfig */, + 34C1F2BEAF7F0DCB8E630F33 /* Pods-App-WhatsNew.releasestage.xcconfig */, + AB8156676C9C771D691ADE07 /* Pods-App-WhatsNew.releaseprod.xcconfig */, + 9844714991FA40ECDC228CC9 /* Pods-App-WhatsNew.releasedev.xcconfig */, + E905D28DCAA1940E08C96896 /* Pods-App-WhatsNew-WhatsNewTests.debug.xcconfig */, + A2CABA11F5E7F89EFD9A05AC /* Pods-App-WhatsNew-WhatsNewTests.debugstage.xcconfig */, + B7EAC5E8F0ED2F1F81050C30 /* Pods-App-WhatsNew-WhatsNewTests.debugprod.xcconfig */, + F71207762CEF1763A08C7151 /* Pods-App-WhatsNew-WhatsNewTests.debugdev.xcconfig */, + A557A3CED4D6327AAE6AA02C /* Pods-App-WhatsNew-WhatsNewTests.release.xcconfig */, + 1E3F4487E7D3A48F5FD12DDA /* Pods-App-WhatsNew-WhatsNewTests.releasestage.xcconfig */, + 365FD817D70DFBCBDE2EAE5F /* Pods-App-WhatsNew-WhatsNewTests.releaseprod.xcconfig */, + 58DF8140E3B3436F58C4C8B9 /* Pods-App-WhatsNew-WhatsNewTests.releasedev.xcconfig */, + ); + name = Pods; + path = ../Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 028A37182ADFF3F7008CA604 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 028A371C2ADFF3F7008CA604 /* WhatsNew */ = { + isa = PBXNativeTarget; + buildConfigurationList = 028A372F2ADFF3F8008CA604 /* Build configuration list for PBXNativeTarget "WhatsNew" */; + buildPhases = ( + E5055BD989FEEC50EF87C814 /* [CP] Check Pods Manifest.lock */, + 02E6408E2AE007090079AEDA /* SwiftGen */, + 028A37182ADFF3F7008CA604 /* Headers */, + 028A37192ADFF3F7008CA604 /* Sources */, + 028A371A2ADFF3F7008CA604 /* Frameworks */, + 028A371B2ADFF3F7008CA604 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WhatsNew; + productName = WhatsNew; + productReference = 028A371D2ADFF3F7008CA604 /* WhatsNew.framework */; + productType = "com.apple.product-type.framework"; + }; + 028A37242ADFF3F7008CA604 /* WhatsNewTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 028A37322ADFF3F8008CA604 /* Build configuration list for PBXNativeTarget "WhatsNewTests" */; + buildPhases = ( + 8685C1ADA448B11AB167C40E /* [CP] Check Pods Manifest.lock */, + 028A37212ADFF3F7008CA604 /* Sources */, + 028A37222ADFF3F7008CA604 /* Frameworks */, + 028A37232ADFF3F7008CA604 /* Resources */, + 8A74692D666D8FF13F7BA64F /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 028A37282ADFF3F8008CA604 /* PBXTargetDependency */, + ); + name = WhatsNewTests; + productName = WhatsNewTests; + productReference = 028A37252ADFF3F7008CA604 /* WhatsNewTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 028A37142ADFF3F7008CA604 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + 028A371C2ADFF3F7008CA604 = { + CreatedOnToolsVersion = 15.0; + LastSwiftMigration = 1500; + }; + 028A37242ADFF3F7008CA604 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = 028A37172ADFF3F7008CA604 /* Build configuration list for PBXProject "WhatsNew" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + uk, + ); + mainGroup = 028A37132ADFF3F7008CA604; + productRefGroup = 028A371E2ADFF3F7008CA604 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 028A371C2ADFF3F7008CA604 /* WhatsNew */, + 028A37242ADFF3F7008CA604 /* WhatsNewTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 028A371B2ADFF3F7008CA604 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 02E640862ADFFF380079AEDA /* Assets.xcassets in Resources */, + 02E6407C2ADFF6250079AEDA /* Localizable.strings in Resources */, + 020A7B612AE136D2000BAF70 /* WhatsNew.json in Resources */, + 02E6408C2AE006680079AEDA /* swiftgen.yml in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 028A37232ADFF3F7008CA604 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 02E6408E2AE007090079AEDA /* SwiftGen */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftGen; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\"\nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; + }; + 8685C1ADA448B11AB167C40E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-App-WhatsNew-WhatsNewTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8A74692D666D8FF13F7BA64F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-WhatsNew-WhatsNewTests/Pods-App-WhatsNew-WhatsNewTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E5055BD989FEEC50EF87C814 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-App-WhatsNew-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 028A37192ADFF3F7008CA604 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 02E640812ADFFE440079AEDA /* WhatsNewViewModel.swift in Sources */, + 02B54E112AE061C100C56962 /* WhatsNewRouter.swift in Sources */, + 020A7B5F2AE131A9000BAF70 /* WhatsNewModel.swift in Sources */, + 02B54E0F2AE0337800C56962 /* PageControl.swift in Sources */, + 02EC90AC2AE90C64007DE1E0 /* WhatsNewPage.swift in Sources */, + 02EC90AA2AE904E1007DE1E0 /* WhatsNewStorage.swift in Sources */, + 02E6408A2AE004300079AEDA /* Strings.swift in Sources */, + 02E640792ADFF5920079AEDA /* WhatsNewView.swift in Sources */, + 02B54E0D2AE0331F00C56962 /* WhatsNewNavigationButton.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 028A37212ADFF3F7008CA604 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 028A372B2ADFF3F8008CA604 /* WhatsNewTests.swift in Sources */, + 020AC2692AEBB69E0086E975 /* WhatsNewMock.generated.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 028A37282ADFF3F8008CA604 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 028A371C2ADFF3F7008CA604 /* WhatsNew */; + targetProxy = 028A37272ADFF3F8008CA604 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 02E6407E2ADFF6250079AEDA /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 02E6407D2ADFF6250079AEDA /* en */, + 02E6407F2ADFF6270079AEDA /* uk */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 028A372D2ADFF3F8008CA604 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 028A372E2ADFF3F8008CA604 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 028A37302ADFF3F8008CA604 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0C01007F0E8CEDCD293E0A68 /* Pods-App-WhatsNew.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WhatsNew/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.WhatsNew; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 028A37312ADFF3F8008CA604 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4CB92C9DBA730A1B06B076BA /* Pods-App-WhatsNew.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WhatsNew/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.WhatsNew; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 028A37332ADFF3F8008CA604 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E905D28DCAA1940E08C96896 /* Pods-App-WhatsNew-WhatsNewTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 028A37342ADFF3F8008CA604 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A557A3CED4D6327AAE6AA02C /* Pods-App-WhatsNew-WhatsNewTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 02E6405E2ADFF4DE0079AEDA /* DebugDev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugDev; + }; + 02E6405F2ADFF4DE0079AEDA /* DebugDev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6F50A409FBCCC7C08712A25E /* Pods-App-WhatsNew.debugdev.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WhatsNew/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.WhatsNew; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugDev; + }; + 02E640602ADFF4DE0079AEDA /* DebugDev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F71207762CEF1763A08C7151 /* Pods-App-WhatsNew-WhatsNewTests.debugdev.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugDev; + }; + 02E640612ADFF4E50079AEDA /* DebugProd */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugProd; + }; + 02E640622ADFF4E50079AEDA /* DebugProd */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A76975E21FF282D59CEC4452 /* Pods-App-WhatsNew.debugprod.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WhatsNew/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.WhatsNew; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugProd; + }; + 02E640632ADFF4E50079AEDA /* DebugProd */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B7EAC5E8F0ED2F1F81050C30 /* Pods-App-WhatsNew-WhatsNewTests.debugprod.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugProd; + }; + 02E640642ADFF4EA0079AEDA /* DebugStage */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugStage; + }; + 02E640652ADFF4EA0079AEDA /* DebugStage */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9176CDC000731B73D2F10372 /* Pods-App-WhatsNew.debugstage.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WhatsNew/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.WhatsNew; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugStage; + }; + 02E640662ADFF4EA0079AEDA /* DebugStage */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A2CABA11F5E7F89EFD9A05AC /* Pods-App-WhatsNew-WhatsNewTests.debugstage.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugStage; + }; + 02E640672ADFF4F10079AEDA /* ReleaseDev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseDev; + }; + 02E640682ADFF4F10079AEDA /* ReleaseDev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9844714991FA40ECDC228CC9 /* Pods-App-WhatsNew.releasedev.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WhatsNew/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.WhatsNew; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseDev; + }; + 02E640692ADFF4F10079AEDA /* ReleaseDev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 58DF8140E3B3436F58C4C8B9 /* Pods-App-WhatsNew-WhatsNewTests.releasedev.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseDev; + }; + 02E6406A2ADFF4F70079AEDA /* ReleaseProd */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseProd; + }; + 02E6406B2ADFF4F70079AEDA /* ReleaseProd */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB8156676C9C771D691ADE07 /* Pods-App-WhatsNew.releaseprod.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WhatsNew/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.WhatsNew; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseProd; + }; + 02E6406C2ADFF4F70079AEDA /* ReleaseProd */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 365FD817D70DFBCBDE2EAE5F /* Pods-App-WhatsNew-WhatsNewTests.releaseprod.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseProd; + }; + 02E6406D2ADFF4FD0079AEDA /* ReleaseStage */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseStage; + }; + 02E6406E2ADFF4FD0079AEDA /* ReleaseStage */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 34C1F2BEAF7F0DCB8E630F33 /* Pods-App-WhatsNew.releasestage.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WhatsNew/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.WhatsNew; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseStage; + }; + 02E6406F2ADFF4FD0079AEDA /* ReleaseStage */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1E3F4487E7D3A48F5FD12DDA /* Pods-App-WhatsNew-WhatsNewTests.releasestage.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = L8PG7LC3Y3; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseStage; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 028A37172ADFF3F7008CA604 /* Build configuration list for PBXProject "WhatsNew" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 028A372D2ADFF3F8008CA604 /* Debug */, + 02E640642ADFF4EA0079AEDA /* DebugStage */, + 02E640612ADFF4E50079AEDA /* DebugProd */, + 02E6405E2ADFF4DE0079AEDA /* DebugDev */, + 028A372E2ADFF3F8008CA604 /* Release */, + 02E6406D2ADFF4FD0079AEDA /* ReleaseStage */, + 02E6406A2ADFF4F70079AEDA /* ReleaseProd */, + 02E640672ADFF4F10079AEDA /* ReleaseDev */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 028A372F2ADFF3F8008CA604 /* Build configuration list for PBXNativeTarget "WhatsNew" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 028A37302ADFF3F8008CA604 /* Debug */, + 02E640652ADFF4EA0079AEDA /* DebugStage */, + 02E640622ADFF4E50079AEDA /* DebugProd */, + 02E6405F2ADFF4DE0079AEDA /* DebugDev */, + 028A37312ADFF3F8008CA604 /* Release */, + 02E6406E2ADFF4FD0079AEDA /* ReleaseStage */, + 02E6406B2ADFF4F70079AEDA /* ReleaseProd */, + 02E640682ADFF4F10079AEDA /* ReleaseDev */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 028A37322ADFF3F8008CA604 /* Build configuration list for PBXNativeTarget "WhatsNewTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 028A37332ADFF3F8008CA604 /* Debug */, + 02E640662ADFF4EA0079AEDA /* DebugStage */, + 02E640632ADFF4E50079AEDA /* DebugProd */, + 02E640602ADFF4DE0079AEDA /* DebugDev */, + 028A37342ADFF3F8008CA604 /* Release */, + 02E6406F2ADFF4FD0079AEDA /* ReleaseStage */, + 02E6406C2ADFF4F70079AEDA /* ReleaseProd */, + 02E640692ADFF4F10079AEDA /* ReleaseDev */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 028A37142ADFF3F7008CA604 /* Project object */; +} diff --git a/WhatsNew/WhatsNew.xcodeproj/xcshareddata/xcschemes/WhatsNew.xcscheme b/WhatsNew/WhatsNew.xcodeproj/xcshareddata/xcschemes/WhatsNew.xcscheme new file mode 100644 index 000000000..c7efb774a --- /dev/null +++ b/WhatsNew/WhatsNew.xcodeproj/xcshareddata/xcschemes/WhatsNew.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WhatsNew/WhatsNew/Assets.xcassets/1.0/Contents.json b/WhatsNew/WhatsNew/Assets.xcassets/1.0/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/WhatsNew/WhatsNew/Assets.xcassets/1.0/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhatsNew/WhatsNew/Assets.xcassets/1.0/image1_1.0.imageset/Contents.json b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image1_1.0.imageset/Contents.json new file mode 100644 index 000000000..52b4c6b95 --- /dev/null +++ b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image1_1.0.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Group 97.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "localizable" : true + } +} diff --git a/WhatsNew/WhatsNew/Assets.xcassets/1.0/image1_1.0.imageset/Group 97.png b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image1_1.0.imageset/Group 97.png new file mode 100644 index 000000000..1853bf8ab Binary files /dev/null and b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image1_1.0.imageset/Group 97.png differ diff --git a/WhatsNew/WhatsNew/Assets.xcassets/1.0/image2_1.0.imageset/Contents.json b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image2_1.0.imageset/Contents.json new file mode 100644 index 000000000..80d1a4ce5 --- /dev/null +++ b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image2_1.0.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Group 96-2.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhatsNew/WhatsNew/Assets.xcassets/1.0/image2_1.0.imageset/Group 96-2.png b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image2_1.0.imageset/Group 96-2.png new file mode 100644 index 000000000..36b711417 Binary files /dev/null and b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image2_1.0.imageset/Group 96-2.png differ diff --git a/WhatsNew/WhatsNew/Assets.xcassets/1.0/image3_1.0.imageset/Contents.json b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image3_1.0.imageset/Contents.json new file mode 100644 index 000000000..a58f59752 --- /dev/null +++ b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image3_1.0.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "globe.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhatsNew/WhatsNew/Assets.xcassets/1.0/image3_1.0.imageset/globe.png b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image3_1.0.imageset/globe.png new file mode 100644 index 000000000..bbdbb3514 Binary files /dev/null and b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image3_1.0.imageset/globe.png differ diff --git a/WhatsNew/WhatsNew/Assets.xcassets/1.0/image4_1.0.imageset/Contents.json b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image4_1.0.imageset/Contents.json new file mode 100644 index 000000000..5b0cf9a6b --- /dev/null +++ b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image4_1.0.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "feature screenshot.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhatsNew/WhatsNew/Assets.xcassets/1.0/image4_1.0.imageset/feature screenshot.jpg b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image4_1.0.imageset/feature screenshot.jpg new file mode 100644 index 000000000..5ca9740d9 Binary files /dev/null and b/WhatsNew/WhatsNew/Assets.xcassets/1.0/image4_1.0.imageset/feature screenshot.jpg differ diff --git a/WhatsNew/WhatsNew/Assets.xcassets/Contents.json b/WhatsNew/WhatsNew/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/WhatsNew/WhatsNew/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WhatsNew/WhatsNew/Data/WhatsNew.json b/WhatsNew/WhatsNew/Data/WhatsNew.json new file mode 100644 index 000000000..355c720b6 --- /dev/null +++ b/WhatsNew/WhatsNew/Data/WhatsNew.json @@ -0,0 +1,52 @@ +[ + { + "version": "1.0", + "messages": [ + { + "image": "image1_1.0", + "title": "Improved language support", + "message": "We have added more translations throughout the app so you can learn on edX your way!" + }, + { + "image": "image2_1.0", + "title": "Download videos offline", + "message": "Easily download videos without having an internet connection, so you can keep learning when there isn’t a network around" + }, + { + "image": "image3_1.0", + "title": "Reduced Network Usage", + "message": "Now you can download your content faster to get right into your next lesson!" + }, + { + "image": "image4_1.0", + "title": "Learning Site Switching", + "message": "Switch more easily between multiple learning sites. Find the new options within account settings and easily manage your accounts" + } + ] + }, + { + "version": "0.9", + "messages": [ + { + "image": "image1_1.0", + "title": "1.3 Improved language support", + "message": "We have added more translations throughout the app so you can learn on edX your way!" + }, + { + "image": "image2_1.0", + "title": "Download videos offline", + "message": "Easily download videos without having an internet connection, so you can keep learning when there isn’t a network around" + }, + { + "image": "image3_1.0", + "title": "Reduced Network Usage", + "message": "Now you can download your content faster to get right into your next lesson!" + }, + { + "image": "image4_1.0", + "title": "Learning Site Switching", + "message": "Switch more easily between multiple learning sites. Find the new options within account settings and easily manage your accounts" + } + ] + } +] diff --git a/WhatsNew/WhatsNew/Data/WhatsNewModel.swift b/WhatsNew/WhatsNew/Data/WhatsNewModel.swift new file mode 100644 index 000000000..167cd3ee2 --- /dev/null +++ b/WhatsNew/WhatsNew/Data/WhatsNewModel.swift @@ -0,0 +1,66 @@ +// +// WhatsNewModel.swift +// WhatsNew +// +// Created by  Stepanok Ivan on 19.10.2023. +// + +import Foundation + +// MARK: - WhatsNewModelElement +public struct WhatsNewModelElement: Codable { + public let version: String + public let messages: [Message] + + public init(version: String, messages: [Message]) { + self.version = version + self.messages = messages + } +} + +// MARK: - Message +public struct Message: Codable { + public let image: String + public let title: String + public let message: String + + public init(image: String, title: String, message: String) { + self.image = image + self.title = title + self.message = message + } +} + +public typealias WhatsNewModel = [WhatsNewModelElement] + +extension WhatsNewModel { + + private func compareVersions(_ version1: String, _ version2: String) -> ComparisonResult { + let v1 = version1.split(separator: ".").compactMap { Int($0) } + let v2 = version2.split(separator: ".").compactMap { Int($0) } + + for (a, b) in zip(v1, v2) where a != b { + return a < b ? .orderedAscending : .orderedDescending + } + + return v1.count < v2.count ? .orderedAscending : (v1.count > v2.count ? .orderedDescending : .orderedSame) + } + + private func findLatestVersion(_ versions: [String]) -> String? { + guard let latestVersion = versions.max(by: { compareVersions($0, $1) == .orderedAscending }) else { + return nil + } + return latestVersion + } + + var domain: [WhatsNewPage] { + guard let latestVersion = findLatestVersion(self.map { $0.version }) else { return [] } + return self.first(where: { $0.version == latestVersion })?.messages.map { + WhatsNewPage( + image: $0.image, + title: $0.title, + description: $0.message + ) + } ?? [] + } +} diff --git a/WhatsNew/WhatsNew/Data/WhatsNewStorage.swift b/WhatsNew/WhatsNew/Data/WhatsNewStorage.swift new file mode 100644 index 000000000..35d35d998 --- /dev/null +++ b/WhatsNew/WhatsNew/Data/WhatsNewStorage.swift @@ -0,0 +1,21 @@ +// +// WhatsNewStorage.swift +// WhatsNew +// +// Created by  Stepanok Ivan on 25.10.2023. +// + +import Foundation + +public protocol WhatsNewStorage { + var whatsNewVersion: String? {get set} +} + +#if DEBUG +public class WhatsNewStorageMock: WhatsNewStorage { + + public var whatsNewVersion: String? + + public init() {} +} +#endif diff --git a/WhatsNew/WhatsNew/Domain/WhatsNewPage.swift b/WhatsNew/WhatsNew/Domain/WhatsNewPage.swift new file mode 100644 index 000000000..8172ee037 --- /dev/null +++ b/WhatsNew/WhatsNew/Domain/WhatsNewPage.swift @@ -0,0 +1,14 @@ +// +// WhatsNewPage.swift +// WhatsNew +// +// Created by  Stepanok Ivan on 25.10.2023. +// + +import Foundation + +struct WhatsNewPage { + let image: String + let title: String + let description: String +} diff --git a/WhatsNew/WhatsNew/Info.plist b/WhatsNew/WhatsNew/Info.plist new file mode 100644 index 000000000..f72a0f657 --- /dev/null +++ b/WhatsNew/WhatsNew/Info.plist @@ -0,0 +1,12 @@ + + + + + + diff --git a/WhatsNew/WhatsNew/Presentation/Elements/PageControl.swift b/WhatsNew/WhatsNew/Presentation/Elements/PageControl.swift new file mode 100644 index 000000000..290369d3f --- /dev/null +++ b/WhatsNew/WhatsNew/Presentation/Elements/PageControl.swift @@ -0,0 +1,33 @@ +// +// PageControl.swift +// WhatsNew +// +// Created by  Stepanok Ivan on 18.10.2023. +// + +import SwiftUI +import Core +import Theme + +struct PageControl: View { + let numberOfPages: Int + var currentPage: Int + + private var dots: some View { + HStack(spacing: 8) { + ForEach(0 ..< numberOfPages) { page in + RoundedRectangle(cornerRadius: 4) + .frame(width: page == currentPage ? 24 : 8, height: 8) + .foregroundColor(page == currentPage ? Theme.Colors.accentColor : Theme.Colors.textSecondary) + } + } + } + + var body: some View { + VStack { + Spacer() + dots + Spacer() + } + } +} diff --git a/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift b/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift new file mode 100644 index 000000000..828fcbf52 --- /dev/null +++ b/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift @@ -0,0 +1,68 @@ +// +// CustomButton.swift +// WhatsNew +// +// Created by  Stepanok Ivan on 18.10.2023. +// + +import SwiftUI +import Core +import Theme + +struct WhatsNewNavigationButton: View { + let type: ButtonType + let action: () -> Void + + enum ButtonType { + case previous, next, done + } + + var body: some View { + Group { + HStack(spacing: 4) { + if type == .previous { + CoreAssets.arrowLeft.swiftUIImage + .renderingMode(.template) + .foregroundColor(Theme.Colors.accentColor) + } + + Text(type == .previous ? WhatsNewLocalization.buttonPrevious + : (type == .next ? WhatsNewLocalization.buttonNext : WhatsNewLocalization.buttonDone )) + .foregroundColor(type == .previous ? Theme.Colors.accentColor : Theme.Colors.white) + .font(Theme.Fonts.labelLarge) + + if type == .next { + CoreAssets.arrowLeft.swiftUIImage + .renderingMode(.template) + .rotationEffect(Angle(degrees: 180)) + .foregroundColor(Theme.Colors.white) + } + + if type == .done { + CoreAssets.checkmark.swiftUIImage + .renderingMode(.template) + .foregroundColor(Theme.Colors.white) + } + }.padding(.horizontal, 20) + .padding(.vertical, 9) + }.fixedSize() + .background( + Theme.Shapes.buttonShape + .fill( + type == .previous + ? Theme.Colors.background + : Theme.Colors.accentButtonColor + ) + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel(type == .previous ? WhatsNewLocalization.buttonPrevious + : (type == .next ? WhatsNewLocalization.buttonNext : WhatsNewLocalization.buttonDone )) + .overlay( + Theme.Shapes.buttonShape + .stroke(type == .previous + ? Theme.Colors.accentButtonColor + : Theme.Colors.background, lineWidth: 1) + ) + .onTapGesture { action() } + } +} diff --git a/WhatsNew/WhatsNew/Presentation/WhatsNewRouter.swift b/WhatsNew/WhatsNew/Presentation/WhatsNewRouter.swift new file mode 100644 index 000000000..4416e24fd --- /dev/null +++ b/WhatsNew/WhatsNew/Presentation/WhatsNewRouter.swift @@ -0,0 +1,19 @@ +// +// WhatsNewRouter.swift +// WhatsNew +// +// Created by  Stepanok Ivan on 18.10.2023. +// + +import Foundation +import Core + +public protocol WhatsNewRouter: BaseRouter { +} + +// Mark - For testing and SwiftUI preview +#if DEBUG +public class WhatsNewRouterMock: BaseRouterMock, WhatsNewRouter { + public override init() {} +} +#endif diff --git a/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift b/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift new file mode 100644 index 000000000..8fe29844d --- /dev/null +++ b/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift @@ -0,0 +1,169 @@ +// +// WhatsNewView.swift +// WhatsNew +// +// Created by  Stepanok Ivan on 18.10.2023. +// + +import SwiftUI +import Core +import Theme + +public struct WhatsNewView: View { + + private let router: WhatsNewRouter + + @ObservedObject + private var viewModel: WhatsNewViewModel + + @Environment (\.isHorizontal) + private var isHorizontal + + @State var index = 0 + + public init(router: WhatsNewRouter, viewModel: WhatsNewViewModel) { + self.router = router + self.viewModel = viewModel + } + + public var body: some View { + GeometryReader { reader in + ZStack(alignment: isHorizontal ? .center : .bottom) { + Theme.Colors.background + .ignoresSafeArea() + adaptiveStack(isHorizontal: isHorizontal) { + TabView(selection: $index) { + ForEach(Array(viewModel.newItems.enumerated()), id: \.offset) { _, new in + adaptiveStack(isHorizontal: isHorizontal) { + ZStack(alignment: .center) { + Image(new.image, bundle: Bundle(for: BundleToken.self)) + .resizable() + .scaledToFit() + .frame(minWidth: 250, maxWidth: 300) + .padding(24) + .accessibilityIdentifier("whatsnew_image") + }.frame(minHeight: 250, maxHeight: 416) + Spacer() + } + } + }.tabViewStyle(.page(indexDisplayMode: .never)) + } + if isHorizontal { + HStack { + Spacer() + + Rectangle() + .foregroundColor(Theme.Colors.background) + .frame(width: reader.size.width / 1.9) + .ignoresSafeArea() + .mask( + LinearGradient( + gradient: Gradient(colors: [ + .clear, + .black, + .black, + .black, + .black, + .black, + .black, + .black, + .black]), + startPoint: .leading, + endPoint: .trailing + ) + ) + } .allowsHitTesting(false) + } + HStack { + if isHorizontal { + Spacer() + } + VStack(spacing: 16) { + VStack { + if !viewModel.newItems.isEmpty { + Text(viewModel.newItems[viewModel.index].title) + .font(Theme.Fonts.titleMedium) + .accessibilityIdentifier("title_text") + Text(viewModel.newItems[viewModel.index].description) + .font(Theme.Fonts.bodyMedium) + .multilineTextAlignment(.center) + .accessibilityIdentifier("description_text") + } + }.frame(height: 100) + .allowsHitTesting(false) + + HStack(spacing: 36) { + WhatsNewNavigationButton(type: .previous, action: { + if index != 0 { + withAnimation(.linear(duration: 0.3)) { + index -= 1 + } + } + }) + .opacity(viewModel.index != 0 ? 1 : 0) + .accessibilityIdentifier("previous_button") + WhatsNewNavigationButton( + type: viewModel.index < viewModel.newItems.count - 1 ? .next : .done, + action: { + if index < viewModel.newItems.count - 1 { + withAnimation(.linear(duration: 0.3)) { + index += 1 + } + } else { + router.showMainOrWhatsNewScreen(sourceScreen: viewModel.sourceScreen) + } + } + ) + .accessibilityIdentifier("next_button") + } + } + .padding(.bottom, isHorizontal ? 0 : 52) + .padding(.horizontal, 24) + .frame(width: isHorizontal ? reader.size.width / 1.9 : nil) + } + VStack { + if isHorizontal { + Spacer() + } + PageControl(numberOfPages: viewModel.newItems.count, currentPage: viewModel.index) + .frame(height: isHorizontal ? 8 : nil) + .allowsHitTesting(false) + .padding(.top, isHorizontal ? 0 : 170) + .padding(.bottom, 8) + .accessibilityIdentifier("whatsnew_pagecontrol") + } + + }.onChange(of: index) { ind in + withAnimation(.linear(duration: 0.3)) { + viewModel.index = ind + } + } + .navigationTitle(WhatsNewLocalization.title) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing, content: { + Button(action: { + router.showMainOrWhatsNewScreen(sourceScreen: viewModel.sourceScreen) + }, label: { + Image(systemName: "xmark") + .foregroundColor(Theme.Colors.accentColor) + }) + .accessibilityIdentifier("close_button") + }) + } + } + } + + class BundleToken {} +} + +#if DEBUG +struct WhatsNewView_Previews: PreviewProvider { + static var previews: some View { + WhatsNewView( + router: WhatsNewRouterMock(), + viewModel: WhatsNewViewModel(storage: WhatsNewStorageMock()) + ) + .loadFonts() + } +} +#endif diff --git a/WhatsNew/WhatsNew/Presentation/WhatsNewViewModel.swift b/WhatsNew/WhatsNew/Presentation/WhatsNewViewModel.swift new file mode 100644 index 000000000..12eb34b78 --- /dev/null +++ b/WhatsNew/WhatsNew/Presentation/WhatsNewViewModel.swift @@ -0,0 +1,75 @@ +// +// WhatsNewViewModel.swift +// WhatsNew +// +// Created by  Stepanok Ivan on 18.10.2023. +// + +import SwiftUI +import Core +import Swinject + +public class WhatsNewViewModel: ObservableObject { + @Published var index: Int = 0 + @Published var newItems: [WhatsNewPage] = [] + private let storage: WhatsNewStorage + var sourceScreen: LogistrationSourceScreen + + public init(storage: WhatsNewStorage, sourceScreen: LogistrationSourceScreen = .default) { + self.storage = storage + self.sourceScreen = sourceScreen + newItems = loadWhatsNew() + } + + public func getVersion() -> String? { + guard let model = loadWhatsNewModel() else { return nil } + return model.first?.version + } + + public func shouldShowWhatsNew() -> Bool { + guard let currentVersion = getVersion() else { return false } + + // If there is no saved version in storage, we always show WhatsNew + guard let savedVersion = storage.whatsNewVersion else { return true } + + // We break down the versions into components major, minor, patch + let savedComponents = savedVersion.components(separatedBy: ".") + let currentComponents = currentVersion.components(separatedBy: ".") + + // Checking major and minor components + if savedComponents.count >= 2 && currentComponents.count >= 2 { + let savedMajor = savedComponents[0] + let savedMinor = savedComponents[1] + + let currentMajor = currentComponents[0] + let currentMinor = currentComponents[1] + + // If major or minor are different, show WhatsNew + if savedMajor != currentMajor || savedMinor != currentMinor { + return true + } + } + return false + } + + func loadWhatsNew() -> [WhatsNewPage] { + guard let domain = loadWhatsNewModel()?.domain else { return [] } + return domain + } + + private func loadWhatsNewModel() -> WhatsNewModel? { + guard let fileUrl = Bundle(for: Self.self).url(forResource: "WhatsNew", withExtension: "json") else { + print("Unable to locate WhatsNew.json") + return nil + } + + do { + let data = try Data(contentsOf: fileUrl) + let decoder = JSONDecoder() + return try decoder.decode(WhatsNewModel.self, from: data) + } catch { + print("Error decoding WhatsNew.json: \(error)") + return nil + } + } +} diff --git a/WhatsNew/WhatsNew/SwiftGen/Strings.swift b/WhatsNew/WhatsNew/SwiftGen/Strings.swift new file mode 100644 index 000000000..8b483b7b4 --- /dev/null +++ b/WhatsNew/WhatsNew/SwiftGen/Strings.swift @@ -0,0 +1,47 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +import Foundation + +// swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references + +// MARK: - Strings + +// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces +public enum WhatsNewLocalization { + /// Done + public static let buttonDone = WhatsNewLocalization.tr("Localizable", "BUTTON_DONE", fallback: "Done") + /// Next + public static let buttonNext = WhatsNewLocalization.tr("Localizable", "BUTTON_NEXT", fallback: "Next") + /// Previous + public static let buttonPrevious = WhatsNewLocalization.tr("Localizable", "BUTTON_PREVIOUS", fallback: "Previous") + /// Localizable.strings + /// WhatsNew + /// + /// Created by  Stepanok Ivan on 18.10.2023. + public static let title = WhatsNewLocalization.tr("Localizable", "TITLE", fallback: "What's New") +} +// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces + +// MARK: - Implementation Details + +extension WhatsNewLocalization { + private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { + let format = BundleToken.bundle.localizedString(forKey: key, value: value, table: table) + return String(format: format, locale: Locale.current, arguments: args) + } +} + +// swiftlint:disable convenience_type +private final class BundleToken { + static let bundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif + }() +} +// swiftlint:enable convenience_type diff --git a/WhatsNew/WhatsNew/en.lproj/Localizable.strings b/WhatsNew/WhatsNew/en.lproj/Localizable.strings new file mode 100644 index 000000000..08fffcb7b --- /dev/null +++ b/WhatsNew/WhatsNew/en.lproj/Localizable.strings @@ -0,0 +1,13 @@ +/* + Localizable.strings + WhatsNew + + Created by  Stepanok Ivan on 18.10.2023. + +*/ + +"TITLE" = "What's New"; +"BUTTON_PREVIOUS" = "Previous"; +"BUTTON_NEXT" = "Next"; +"BUTTON_DONE" = "Done"; + diff --git a/WhatsNew/WhatsNew/uk.lproj/Localizable.strings b/WhatsNew/WhatsNew/uk.lproj/Localizable.strings new file mode 100644 index 000000000..a0194425c --- /dev/null +++ b/WhatsNew/WhatsNew/uk.lproj/Localizable.strings @@ -0,0 +1,12 @@ +/* + Localizable.strings + WhatsNew + + Created by  Stepanok Ivan on 18.10.2023. + +*/ + +"TITLE" = "Що нового"; +"BUTTON_PREVIOUS" = "Назад"; +"BUTTON_NEXT" = "Далі"; +"BUTTON_DONE" = "Завершити"; diff --git a/WhatsNew/WhatsNewTests/Presentation/WhatsNewTests.swift b/WhatsNew/WhatsNewTests/Presentation/WhatsNewTests.swift new file mode 100644 index 000000000..7c6542b16 --- /dev/null +++ b/WhatsNew/WhatsNewTests/Presentation/WhatsNewTests.swift @@ -0,0 +1,27 @@ +// +// WhatsNewTests.swift +// WhatsNewTests +// +// Created by  Stepanok Ivan on 18.10.2023. +// + +import XCTest +import Core +@testable import WhatsNew + +final class WhatsNewTests: XCTestCase { + + func testGetVersion() throws { + let viewModel = WhatsNewViewModel(storage: WhatsNewStorageMock()) + let version = viewModel.getVersion() + XCTAssertNotNil(version) + XCTAssertTrue(version == "1.0") + } + + func testshouldShowWhatsNew() throws { + let viewModel = WhatsNewViewModel(storage: WhatsNewStorageMock()) + let version = viewModel.getVersion() + XCTAssertNotNil(version) + XCTAssertTrue(viewModel.shouldShowWhatsNew()) + } +} diff --git a/WhatsNew/WhatsNewTests/WhatsNewMock.generated.swift b/WhatsNew/WhatsNewTests/WhatsNewMock.generated.swift new file mode 100644 index 000000000..999f7cc25 --- /dev/null +++ b/WhatsNew/WhatsNewTests/WhatsNewMock.generated.swift @@ -0,0 +1,19 @@ +// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + + +// Generated with SwiftyMocky 4.2.0 +// Required Sourcery: 1.8.0 + + +import SwiftyMocky +import XCTest +import Core +import WhatsNew +import Foundation +import SwiftUI +import Combine + + +// SwiftyMocky: no AutoMockable found. +// Please define and inherit from AutoMockable, or annotate protocols to be mocked diff --git a/WhatsNew/swiftgen.yml b/WhatsNew/swiftgen.yml new file mode 100644 index 000000000..9aa2d1c3b --- /dev/null +++ b/WhatsNew/swiftgen.yml @@ -0,0 +1,18 @@ +strings: + inputs: + - WhatsNew/en.lproj + outputs: + - templateName: structured-swift5 + params: + publicAccess: true + enumName: WhatsNewLocalization + output: WhatsNew/SwiftGen/Strings.swift +#xcassets: +# inputs: +# - WhatsNew/Assets.xcassets +# outputs: +# templateName: swift5 +# params: +# publicAccess: true +# enumName: WhatsNewAssets +# output: WhatsNew/SwiftGen/Assets.swift diff --git a/ci_scripts/ci_prepare_env.sh b/ci_scripts/ci_prepare_env.sh index 206dcde33..030de4198 100644 --- a/ci_scripts/ci_prepare_env.sh +++ b/ci_scripts/ci_prepare_env.sh @@ -27,6 +27,7 @@ setup_xcode_cloud_environment () { bundle config path vendor/bundle bundle install --jobs 4 --retry 3 + bundle update fastlane } install_xcode_cloud_brew_dependencies () { @@ -34,10 +35,12 @@ install_xcode_cloud_brew_dependencies () { } setup_github_actions_environment() { - brew update && brew install xcodegen git-lfs imagemagick - + # brew update && brew install xcodegen git-lfs imagemagick + brew update && brew install xcodegen git-lfs + bundle config path vendor/bundle bundle install --jobs 4 --retry 3 + bundle update fastlane pod install } diff --git a/config_script/process_config.py b/config_script/process_config.py new file mode 100644 index 000000000..36340bfc3 --- /dev/null +++ b/config_script/process_config.py @@ -0,0 +1,323 @@ +import plistlib +import os +import yaml +from pathlib import Path +import sys +import json + +class PlistManager: + def __init__(self, config_dir, config_files): + self.config_dir = config_dir + self.config_files = config_files + + def get_config_paths(self): + return [Path(self.config_dir) / config_name for config_name in self.config_files] + + def get_product_name(self): + return os.getenv('PRODUCT_NAME') + + def get_bundle_identifier(self): + return os.getenv('PRODUCT_BUNDLE_IDENTIFIER') + + def get_info_plist_path(self): + return os.getenv('INFOPLIST_PATH') + + def get_wrapper_name(self): + return os.getenv('WRAPPER_NAME') + + def get_built_products_path(self): + return os.getenv('BUILT_PRODUCTS_DIR') + + def get_bundle_config_path(self): + return os.path.join(self.get_built_products_path(), self.get_wrapper_name(), 'config.plist') + + def get_app_info_plist_path(self): + built_products_path = self.get_built_products_path() + info_plist_path = self.get_info_plist_path() + + if built_products_path and info_plist_path: + return os.path.join(built_products_path, info_plist_path) + else: + return None + + def get_firebase_info_plist_path(self): + built_products_path = self.get_built_products_path() + wrapper_name = self.get_wrapper_name() + + if built_products_path and wrapper_name: + return os.path.join(built_products_path, wrapper_name, 'GoogleService-Info.plist') + else: + print("The BUILT_PRODUCTS_DIR or WRAPPER_NAME environment variable is not set.") + return None + + def get_firebase_config_path(self): + built_products_path = self.get_built_products_path() + wrapper_name = self.get_wrapper_name() + + if built_products_path and wrapper_name: + return os.path.join(built_products_path, wrapper_name, 'firebase.plist') + else: + print("The BUILT_PRODUCTS_DIR or WRAPPER_NAME environment variable is not set.") + return None + + def load_config(self): + properties = {} + + for path in self.get_config_paths(): + try: + with open(path, 'r') as file: + dict = yaml.safe_load(file) + if dict is not None: + properties = merge_dicts(properties, dict) + except FileNotFoundError: + print(f"{path} not found. Skipping.") + + return properties + + def yaml_to_plist(self): + plist_data = {} + + for path in self.get_config_paths(): + try: + with open(path, 'r') as file: + yaml_data = yaml.safe_load(file) + if yaml_data is not None: + plist_data = merge_dicts(plist_data, yaml_data) + except FileNotFoundError: + print(f"{path} not found. Skipping.") + except yaml.YAMLError as e: + print(f"Error parsing YAML file {path}: {e}") + + return plist_data + + def write_to_plist_file(self, plist, file_path): + file_name = os.path.basename(file_path) + with open(file_path, 'wb') as plist_file: + plistlib.dump(plist, plist_file) + print(f"File {file_name} has been written to:") + print(f"{file_path}") + + def print_info_plist_contents(self, plist_path): + if not plist_path: + print(f"Path is not set. {plist_path}") + try: + with open(plist_path, 'rb') as plist_file: + plist_contents = plistlib.load(plist_file) + print(plist_contents) + except Exception as e: + print(f"Error reading plist file: {e}") + + def get_info_plist_contents(self, plist_path): + if not plist_path: + print(f"Path is not set. {plist_path}") + try: + with open(plist_path, 'rb') as plist_file: + plist_contents = plistlib.load(plist_file) + return plist_contents + except Exception as e: + print(f"Error reading plist file: {e}") + return None + +def merge_dicts(d1, d2): + for k, v in d2.items(): + if k in d1: + d1[k] = dict(v,**d1[k]) + else: + d1[k] = v + return d1 + +class ConfigurationManager: + def __init__(self, plist_manager): + self.plist_manager = plist_manager + + def get_environment_variable(self, variable): + return os.getenv(variable) + + def add_url_scheme(self, scheme, plist): + body = { + 'CFBundleTypeRole': 'Editor', + 'CFBundleURLSchemes': scheme + } + existing = plist.get('CFBundleURLTypes', []) + found = any(scheme in entry.get('CFBundleURLSchemes', []) for entry in existing) + + if not found: + existing.append(body) + plist['CFBundleURLTypes'] = existing + + def add_application_query_schemes(self, schemes, plist): + existing = plist.get('LSApplicationQueriesSchemes', []) + for scheme in schemes: + if scheme not in existing: + existing.append(scheme) + plist['LSApplicationQueriesSchemes'] = existing + return plist + + def add_firebase_config(self, config, firebase_info_plist_path): + plist = {} + firebase = config.get('FIREBASE', {}) + + if firebase_info_plist_path and firebase: + plist['BUNDLE_ID'] = self.plist_manager.get_bundle_identifier() + plist['API_KEY'] = firebase.get('API_KEY', '') + plist['CLIENT_ID'] = firebase.get('CLIENT_ID', '') + plist['GOOGLE_APP_ID'] = firebase.get('GOOGLE_APP_ID', '') + plist['GCM_SENDER_ID'] = firebase.get('GCM_SENDER_ID', '') + + project_id = firebase.get('PROJECT_ID', '') + if project_id: + plist['PROJECT_ID'] = project_id + plist['STORAGE_BUCKET'] = project_id + '.appspot.com' + plist['DATABASE_URL'] = 'https://' + project_id + '.firebaseio.com' + + reversed_client_id = firebase.get('REVERSED_CLIENT_ID', '') + if reversed_client_id: + plist['REVERSED_CLIENT_ID'] = reversed_client_id + + self.plist_manager.write_to_plist_file(plist, self.plist_manager.get_firebase_info_plist_path()) + else: + print("Firebase config is empty. Skipping") + + def add_facebook_config(self, config, plist): + facebook = config.get('FACEBOOK', {}) + key = facebook.get('FACEBOOK_APP_ID') + client_token = facebook.get('CLIENT_TOKEN') + + if key and client_token: + plist["FacebookAppID"] = key + plist["FacebookClientToken"] = client_token + plist["FacebookDisplayName"] = self.plist_manager.get_product_name() + scheme = ["fb" + key] + self.add_url_scheme(scheme, plist) + + def add_google_config(self, config, plist): + google = config.get('GOOGLE', {}) + key = google.get('GOOGLE_PLUS_KEY') + client_id = google.get('CLIENT_ID') + + if key and client_id: + plist["GIDClientID"] = client_id + scheme = ['.'.join(reversed(key.split('.')))] + self.add_url_scheme(scheme, plist) + + def add_microsoft_config(self, config, plist): + microsoft = config.get('MICROSOFT', {}) + key = microsoft.get('APP_ID') + + if key: + bundle_identifier = self.plist_manager.get_bundle_identifier() + scheme = ["msauth." + bundle_identifier] + self.add_url_scheme(scheme, plist) + self.add_application_query_schemes(["msauthv2", "msauthv3"], plist) + + def update_info_plist(self, plist_data, plist_path): + if not plist_path: + print("Path is not set.") + sys.exit(1) + + try: + with open(plist_path, 'rb') as plist_file: + plist_contents = plistlib.load(plist_file) + + plist_contents.update(plist_data) + + try: + plistlib.dumps(plist_contents) + except Exception as e: + print(f"Error validating plist contents: {e}") + sys.exit(1) + + self.plist_manager.write_to_plist_file(plist_contents, plist_path) + except FileNotFoundError: + print(f"Plist file not found: {plist_path}") + sys.exit(1) + except Exception as e: + print(f"Error reading or writing plist file: {e}") + sys.exit(1) + +def parse_yaml(file_path): + try: + with open(file_path, 'r') as file: + return yaml.safe_load(file) + except Exception as e: + print(f"Unable to open or read the file '{file_path}': {e}") + return None + +CONFIG_SETTINGS_YAML_FILENAME = 'config_settings.yaml' +DEFAULT_CONFIG_PATH = './default_config/' + CONFIG_SETTINGS_YAML_FILENAME +CONFIG_DIRECTORY_NAME = 'config_directory' +CONFIG_MAPPINGS = 'config_mapping' +MAPPINGS_FILENAME = 'file_mappings.yaml' + +def get_current_config(configuration, scheme_mappings): + for key, values in scheme_mappings.items(): + if configuration in values: + return key + return None + +def process_plist_files(configuration_manager, plist_manager, config): + firebase_info_plist_path = plist_manager.get_firebase_config_path() + info_plist_path = plist_manager.get_app_info_plist_path() + info_plist_content = plist_manager.get_info_plist_contents(info_plist_path) + + configuration_manager.add_firebase_config(config, firebase_info_plist_path) + configuration_manager.add_facebook_config(config, info_plist_content) + configuration_manager.add_google_config(config, info_plist_content) + configuration_manager.add_microsoft_config(config, info_plist_content) + + configuration_manager.update_info_plist(info_plist_content, info_plist_path) + + bundle_config_path = plist_manager.get_bundle_config_path() + config_plist = plist_manager.yaml_to_plist() + plist_manager.write_to_plist_file(config_plist, bundle_config_path) + +def main(configuration, scheme_mappings): + current_config = get_current_config(configuration, scheme_mappings) + + if current_config is None: + print("Config not found in mappings. Exiting.") + sys.exit(1) + + config_settings = parse_yaml(CONFIG_SETTINGS_YAML_FILENAME) + + if not config_settings: + print("Parsing default config.") + config_settings = parse_yaml(DEFAULT_CONFIG_PATH) + + config_directory = config_settings.get(CONFIG_DIRECTORY_NAME) + config_name = config_settings.get(CONFIG_MAPPINGS, {}).get(current_config) + + if config_directory and config_name: + path = os.path.join(config_directory, config_name) + mappings_path = os.path.join(path, MAPPINGS_FILENAME) + data = parse_yaml(mappings_path) + + if data: + ios_files = data.get('ios', {}).get('files', []) + plist_manager = PlistManager(path, ios_files) + config = plist_manager.load_config() + + if config: + configuration_manager = ConfigurationManager(plist_manager) + process_plist_files(configuration_manager, plist_manager, config) + print(f"Config {configuration} parsed and written successfully.") + else: + print("Unable to parse config files") + sys.exit(1) + + else: + print("Files mappings not found") + sys.exit(1) + + else: + print("Config directory or config name is not provided") + sys.exit(1) + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: script.py ") + sys.exit(1) + + configuration = sys.argv[1] + scheme_mappings = json.loads(sys.argv[2]) + main(configuration, scheme_mappings) diff --git a/config_script/whitelabel.py b/config_script/whitelabel.py new file mode 100644 index 000000000..d7295d4d9 --- /dev/null +++ b/config_script/whitelabel.py @@ -0,0 +1,542 @@ +import argparse +import logging +import os +import shutil +import sys +import yaml +import json +import coloredlogs +from PIL import Image +import re +from textwrap import dedent + +class WhitelabelApp: + EXAMPLE_CONFIG_FILE = dedent(""" + # Notes: + # Config file can contain next options: + images_import_dir: 'path/to/asset/Images' # folder where importing images are placed + assets: + AssetName: + images_path: 'Theme/Theme/Assets.xcassets' # path where images are placed in this Asset + colors_path: 'Theme/Theme/Assets.xcassets/Colors' # path where colors are placed in this Asset + icon_path: 'Theme/Assets.xcassets' # path where the app icon is placed in this Asset + images: + image1: # Asset name + image_name: 'some_image.svg' # image to replace the existing one for image1 Asset (light/universal) + image2: # Asset name + current_path: 'SomeFolder' # Path to image2.imageset inside Assets.xcassets + image_name: 'Rectangle.png' # image to replace the existing one for image2 Asset (light/universal) + dark_image_name: 'RectangleDark.png' # image to replace the existing dark appearance for image2 Asset (dark) + colors: + LoginBackground: # color asset name in Assets + current_path: '' # optional: path to color inside colors_path + light: '#FFFFFF' + dark: '#ED5C13' + icon: + AppIcon: + current_path: '' # optional: path to icon inside icon_path + image_name: 'appIcon.jpg' # image to replace the current AppIcon - png or jpg are supported + project_config: + project_path: 'path/to/project/project.pbxproj' # path to project.pbxproj file + dev_team: '1234567890' # apple development team id + project_extra_targets: ['Target1', 'Target2'] # targets in the workspace other than 'OpenEdX' in which the new dev_team should be set + marketing_version: '1.0.1' # app marketing version + current_project_version: '2' # app build number + configurations: + config1: # build configuration name in project + app_bundle_id: "bundle.id.app.new1" # bundle ID which should be set + product_name: "Mobile App Name1" # app name which should be set + config2: # build configuration name in project + app_bundle_id: "bundle.id.app.new2" # bundle ID which should be set + product_name: "Mobile App Name2" # app name which should be set + font: + font_import_file_path: 'path/to/importing/Font_file.ttf' # path to ttf font file what should be imported to project + project_font_file_path: 'path/to/font/file/in/project/font.ttf' # path to existing ttf font file in project + project_font_names_json_path: 'path/to/names/file/in project/fonts.json' # path to existing font names json-file in project + font_names: + regular: 'FontName-Regular' + medium: 'FontName-Medium' + semiBold: 'FontName-Semibold' + bold: 'FontName-Bold' + whatsnew: + whatsnew_import_file_path: 'path/to/importing/whats_new.json' + project_whatsnew_file_path: 'path/to/json/file/in/project/whats_new.json' + """) + + def __init__(self, **kwargs): + self.assets_dir = kwargs.get('images_import_dir') + if not self.assets_dir: + self.assets_dir = '.' + + self.assets = kwargs.get('assets', {}) + self.project_config = kwargs.get('project_config', {}) + self.font = kwargs.get('font', {}) + self.whatsnew = kwargs.get('whatsnew', {}) + + if self.project_config: + if "project_path" in self.project_config: + self.config_project_path = self.project_config["project_path"] + else: + logging.error("Path to project file is not defined") + else: + logging.debug("Project settings config not found") + + def whitelabel(self): + # Update the properties, resources, and configuration of the current app. + self.copy_assets() + self.copy_font() + self.copy_whatsnew() + if self.project_config: + self.set_app_project_config() + + def copy_assets(self): + if self.assets: + for asset in self.assets.items(): + self.replace_images(asset) + self.replace_colors(asset) + self.replace_app_icon(asset) + else: + logging.debug("Assets config not found") + + def replace_images(self, asset_data): + asset = asset_data[1] + asset_name = asset_data[0] + if "images" in asset : + asset_path = asset["images_path"] if "images_path" in asset else "" + for name, image in asset["images"].items(): + current_path = image["current_path"] if "current_path" in image else "" + path_to_imageset = os.path.join(asset_path, current_path, name+'.imageset') + content_json_path = os.path.join(path_to_imageset, 'Contents.json') + image_name_original = '' + dark_image_name_original = '' + with open(content_json_path, 'r') as openfile: + json_object = json.load(openfile) + for json_image in json_object["images"]: + if "appearances" in json_image: + # dark + dark_image_name_original = json_image["filename"] + else: + # light + image_name_original = json_image["filename"] + has_dark = True if "dark_image_name" in image else False + image_name_import = image["image_name"] if "image_name" in image else '' + dark_image_name_import = image["dark_image_name"] if "dark_image_name" in image else '' + + # conditions to start updating + file_path = os.path.join(path_to_imageset, image_name_original) + dark_file_path = os.path.join(path_to_imageset, dark_image_name_original) + files_to_changes_exist = os.path.exists(file_path) and image_name_original != '' # 1 + if has_dark: + files_to_changes_exist = files_to_changes_exist and os.path.exists(dark_file_path) and dark_image_name_original != '' + contents_json_is_good = os.path.exists(content_json_path) and image_name_original != '' # 2 + if has_dark: + contents_json_is_good = contents_json_is_good and dark_image_name_original != '' + + path_to_imageset_exists = os.path.exists(path_to_imageset) # 3 + file_to_copy_path = os.path.join(self.assets_dir, image_name_import) + dark_file_to_copy_path = os.path.join(self.assets_dir, dark_image_name_import) + files_to_copy_exist = os.path.exists(file_to_copy_path) # 4 + if has_dark: + files_to_copy_exist = files_to_copy_exist and os.path.exists(dark_file_to_copy_path) + + if files_to_changes_exist and contents_json_is_good and path_to_imageset_exists and files_to_copy_exist: + # Delete current file(s) + os.remove(file_path) + if has_dark: + os.remove(dark_file_path) + # Change Contents.json + with open(content_json_path, 'r') as openfile: + contents_string = openfile.read() + contents_string = contents_string.replace(image_name_original, image_name_import) + if has_dark: + contents_string = contents_string.replace(dark_image_name_original, dark_image_name_import) + with open(content_json_path, 'w') as openfile: + openfile.write(contents_string) + # Copy new file(s) + shutil.copy(file_to_copy_path, path_to_imageset) + logging.debug(asset_name+"->images->"+name+": 'light mode'/universal image was updated with "+image_name_import) + if has_dark: + shutil.copy(dark_file_to_copy_path, path_to_imageset) + logging.debug(asset_name+"->images->"+name+": 'dark mode' image was updated with "+dark_image_name_import) + else: + # Handle errors + if not files_to_changes_exist: + logging.error(asset_name+"->images->"+name+": original file(s) doesn't exist") + elif not contents_json_is_good: + logging.error(asset_name+"->images->"+name+": Contents.json doesn't exist or wrong original file(s) in config") + elif not path_to_imageset_exists: + logging.error(asset_name+"->images->"+name+": "+ path_to_imageset + " doesn't exist") + elif not files_to_copy_exist: + logging.error(asset_name+"->images->"+name+": file(s) to copy doesn't exist") + + def replace_colors(self, asset_data): + asset = asset_data[1] + asset_name = asset_data[0] + if "colors" in asset: + colors_path = asset["colors_path"] if "colors_path" in asset else "" + for name, color in asset["colors"].items(): + current_path = color["current_path"] if "current_path" in color else "" + path_to_colorset = os.path.join(colors_path, current_path, name+'.colorset') + light_color = color["light"] + dark_color = color["dark"] + # Change Contents.json + content_json_path = os.path.join(path_to_colorset, 'Contents.json') + if os.path.exists(content_json_path): + with open(content_json_path, 'r') as openfile: + json_object = json.load(openfile) + for key in range(len(json_object["colors"])): + if "appearances" in json_object["colors"][key]: + # dark + changed_components = self.change_color_components(json_object["colors"][key]["color"]["components"], dark_color, name) + json_object["colors"][key]["color"]["components"] = changed_components + else: + # light + changed_components = self.change_color_components(json_object["colors"][key]["color"]["components"], light_color, name) + json_object["colors"][key]["color"]["components"] = changed_components + new_json = json.dumps(json_object) + with open(content_json_path, 'w') as openfile: + openfile.write(new_json) + logging.debug(asset_name+"->colors->"+name+": color was updated with light:'"+light_color+"' dark:'"+dark_color+"'") + else: + logging.error(asset_name+"->colors->"+name+": " + content_json_path + " doesn't exist") + + def change_color_components(self, components, color, name): + color = color.replace("#", "") + if len(color) != 6: + print('Config for color "'+name+'" is incorrect') + else: + components["red"] = "0x"+color[0]+color[1] + components["green"] = "0x"+color[2]+color[3] + components["blue"] = "0x"+color[4]+color[5] + return components + + def replace_app_icon(self, asset_data): + asset = asset_data[1] + asset_name = asset_data[0] + if "icon" in asset: + icon_path = asset["icon_path"] if "icon_path" in asset else "" + for name, icon in asset["icon"].items(): + current_path = icon["current_path"] if "current_path" in icon else "" + path_to_iconset = os.path.join(icon_path, current_path, name+'.appiconset') + content_json_path = os.path.join(path_to_iconset, 'Contents.json') + with open(content_json_path, 'r') as openfile: + json_object = json.load(openfile) + json_icon = json_object["images"][0] + file_to_change = json_icon["filename"] + size_to_change = json_icon["size"] + file_to_copy = icon["image_name"] + file_to_copy_path = os.path.join(self.assets_dir, file_to_copy) + file_to_change_path = os.path.join(path_to_iconset, file_to_change) + if os.path.exists(file_to_change_path): + if os.path.exists(file_to_copy_path): + # get new file width and height + img = Image.open(file_to_copy_path) + # get width and height + width = img.width + height = img.height + # Delete current file + os.remove(file_to_change_path) + # Change Contents.json + with open(content_json_path, 'r') as openfile: + contents_string = openfile.read() + contents_string = contents_string.replace(file_to_change, file_to_copy) + contents_string = contents_string.replace(size_to_change, str(width)+'x'+str(height)) + with open(content_json_path, 'w') as openfile: + openfile.write(contents_string) + # Copy new file + shutil.copy(file_to_copy_path, path_to_iconset) + logging.debug(asset_name+"->icon->"+name+": app icon was updated with "+file_to_copy) + else: + logging.error(asset_name+"->icon->"+name+": " + file_to_copy_path + " doesn't exist") + else: + logging.error(asset_name+"->icon->"+name+": " + file_to_change_path + " doesn't exist") + + def set_app_project_config(self): + self.set_build_related_params() + self.set_project_global_params() + + def set_build_related_params(self): + # check if configurations exist + if "configurations" in self.project_config: + configurations = self.project_config["configurations"] + # read project file + with open(self.config_project_path, 'r') as openfile: + config_file_string = openfile.read() + errors_texts = [] + for name, config in configurations.items(): + # replace parameters for every config + config_file_string = self.replace_parameter_in_config("app_bundle_id", config_file_string, config, name, errors_texts) + config_file_string = self.replace_parameter_in_config("product_name", config_file_string, config, name, errors_texts) + # write to project file + with open(self.config_project_path, 'w') as openfile: + openfile.write(config_file_string) + # print success message or errors if are presented + if len(errors_texts) == 0: + logging.debug("Project configurations parameters were successfully changed") + else: + for error in errors_texts: + logging.error(error) + else: + logging.error("Project configurations are not defined") + + def replace_parameter_in_config(self, parameter, config_file_string, config, config_name, errors_texts): + # if parameter is configured + if parameter in config: + parameter_value = config[parameter] + # if parameter's value is not empty + if parameter_value != '' and parameter_value is not None: + parameter_string = '' + parameter_regex = '' + # define regex rule and replacement string for every possible parameter + if parameter == "app_bundle_id": + parameter_string = "PRODUCT_BUNDLE_IDENTIFIER = "+parameter_value+";" + parameter_regex = "PRODUCT_BUNDLE_IDENTIFIER = .*;" + elif parameter == "product_name": + parameter_string = "PRODUCT_NAME = \""+parameter_value+"\";" + parameter_regex = "PRODUCT_NAME = \".*\";" + # if regex is defined + if parameter_regex != '': + # replace parameter in config file + config_file_string = self.replace_parameter_for_build_config(config_file_string, config_name, parameter_string, parameter_regex, errors_texts) + else: + errors_texts.append("project_config->configurations->"+config_name+": Regex rule for '"+parameter+"' is not defined in config script") + else: + errors_texts.append("project_config->configurations->"+config_name+": '"+parameter+"' parameter is empty in config") + else: + errors_texts.append("project_config->configurations->"+config_name+": '"+parameter+"' was not found in config") + return config_file_string + + def replace_parameter_for_build_config(self, config_file_string, config_name, new_param_string, search_param_regex, errors_texts): + # search substring for current build config only + search_string = re.search(self.regex_string_for_build_config(config_name), config_file_string) + # if build config is found + if search_string is not None: + # get build config as string + config_string = search_string.group() + config_string_out = config_string + # search parameter in config_string + parameter_search_string = re.search(search_param_regex, config_string) + if parameter_search_string is not None: + # get parameter_string as string + parameter_string = parameter_search_string.group() + # replace existing parameter value with new value + config_string_out = config_string.replace(parameter_string, new_param_string) + else: + errors_texts.append("project_config->configurations->"+config_name+": Check regex please. Can't find place in project file where insert '"+new_param_string+"'") + # if something found + if config_string != config_string_out: + config_file_string = config_file_string.replace(config_string, config_string_out) + else: + errors_texts.append("project_config->configurations->"+config_name+": not found in project file") + return config_file_string + + def regex_string_for_build_config(self, build_config): + # regex to search build config inside project file + return f"/\\* {build_config} \\*/ = {{\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = [\\s|\\S]*\t\t\tname = {build_config};" + + def set_project_global_params(self): + # set values for 'global' parameters + self.set_global_parameter("dev_team") + self.set_global_parameter("marketing_version") + self.set_global_parameter("current_project_version") + self.set_team_for_extra_targets() + + def set_global_parameter(self, parameter): + # if parameter is defined in config + if parameter in self.project_config: + parameter_value = self.project_config[parameter] + # if parameter value is not empty + if parameter_value != '' and parameter_value is not None: + # read project file + with open(self.config_project_path, 'r') as openfile: + config_file_string = openfile.read() + config_file_string_out = config_file_string + parameter_string = '' + parameter_regex = '' + # define regex rule and replacement string for every possible parameter + if parameter == "dev_team": + parameter_string = 'DEVELOPMENT_TEAM = '+parameter_value+';' + parameter_regex = 'DEVELOPMENT_TEAM = .{10};' + elif parameter == "marketing_version": + parameter_string = 'MARKETING_VERSION = '+parameter_value+';' + parameter_regex = 'MARKETING_VERSION = .*;' + elif parameter == "current_project_version": + parameter_string = 'CURRENT_PROJECT_VERSION = '+parameter_value+';' + parameter_regex = 'CURRENT_PROJECT_VERSION = .*;' + # if regex is defined + if parameter_regex != '': + # replace all regex findings with new parameters string + config_file_string_out = re.sub(parameter_regex, parameter_string, config_file_string) + else: + logging.error("Regex rule for '"+parameter+"' is not defined in config script") + # if any entries were found and replaced + if config_file_string_out != config_file_string: + # write to project file + with open(self.config_project_path, 'w') as openfile: + openfile.write(config_file_string_out) + logging.debug("'"+parameter+"' was set successfuly") + # if nothing was found + elif re.search(parameter_regex, config_file_string) is None: + logging.error("Check regex please. Nothing was found for '"+parameter+"' in project file") + # if parameter was found but it's replaced already + elif re.search(parameter_regex, config_file_string).group() == parameter_string and parameter_string != '': + logging.debug("Looks like '"+parameter+"' is set already") + # if parameter was not found and it's not empty + elif parameter_string != '': + logging.error("No '"+parameter+"' is found in project file") + else: + logging.error("'"+parameter+"' is empty in config") + else: + logging.error("'"+parameter+"' is not defined") + + def set_team_for_extra_targets(self): + # if parameter is defined in config + if "project_extra_targets" in self.project_config: + if "dev_team" in self.project_config: + targets = self.project_config["project_extra_targets"] + for target in targets: + self.set_team_for_target(target) + else: + logging.error("'dev_team' is not defined in config") + else: + logging.error("'project_extra_targets' are not defined in config") + + def set_team_for_target(self, target): + dev_team = self.project_config["dev_team"] + # related path to target config + path_to_target_config = os.path.join(self.config_project_path.replace("/project.pbxproj", ""), "..", target,target+".xcodeproj", "project.pbxproj") + # read project file for target + with open(path_to_target_config, 'r') as openfile: + config_file_string = openfile.read() + config_file_string_out = config_file_string + # string and regex for dev team + parameter_string = 'DEVELOPMENT_TEAM = '+dev_team+';' + parameter_regex = 'DEVELOPMENT_TEAM = .{10};' + # replace all regex findings with new parameters string + config_file_string_out = re.sub(parameter_regex, parameter_string, config_file_string) + # if something was changed + if config_file_string_out != config_file_string: + # write to target's project file + with open(path_to_target_config, 'w') as openfile: + openfile.write(config_file_string_out) + logging.debug("DEVELOPMENT_TEAM for '"+target+"' target was set successfuly") + # if nothing was found + elif re.search(parameter_regex, config_file_string) is None: + logging.error("Check regex please. Nothing was found for 'DEVELOPMENT_TEAM' in '"+target+" target project file") + else: + logging.debug("Looks like DEVELOPMENT_TEAM for '"+target+"' target is set already") + + def copy_font(self): + # check if font config exists + if self.font: + if "font_import_file_path" in self.font: + font_import_file_path = self.font["font_import_file_path"] + if "project_font_file_path" in self.font: + project_font_file_path = self.font["project_font_file_path"] + # if source and destination file exist + self.copy_file(font_import_file_path, project_font_file_path, "Font") + else: + logging.error("'project_font_file_path' not found in config") + else: + logging.error("'font_import_file_path' not found in config") + # read font names from config + if "font_names" in self.font: + font_names = self.font["font_names"] + # set font names + self.set_font_names(font_names) + else: + logging.error("'font_names' not found in config") + else: + logging.debug("Font not found in config") + + def copy_file(self, file_src, file_dest, title): + # try to copy file and show success/error message + try: + shutil.copy(file_src, file_dest) + except IOError as e: + logging.error("Unable to copy file. "+e) + else: + logging.debug(title+" file was copied to project") + + def set_font_names(self, font_names): + if "project_font_names_json_path" in self.font: + project_font_names_json_path = self.font["project_font_names_json_path"] + # read names json file from project + with open(project_font_names_json_path, 'r') as openfile: + json_object = json.load(openfile) + # go through font types and replace with values from config + for key, _ in json_object.items(): + if key in font_names: + config_font_name_value = font_names[key] + json_object[key] = config_font_name_value + # generate new json + new_json = json.dumps(json_object) + # write to json file + with open(project_font_names_json_path, 'w') as openfile: + openfile.write(new_json) + logging.debug("Font names were set successfuly") + else: + logging.error("'project_font_names_json_path' not found in config") + + def copy_whatsnew(self): + # check if whatsnew config exists + if self.whatsnew: + if "whatsnew_import_file_path" in self.whatsnew: + whatsnew_import_file_path = self.whatsnew["whatsnew_import_file_path"] + if "project_whatsnew_file_path" in self.whatsnew: + project_whatsnew_file_path = self.whatsnew["project_whatsnew_file_path"] + # if source and destination file exist + if os.path.exists(whatsnew_import_file_path): + if os.path.exists(project_whatsnew_file_path): + self.copy_file(whatsnew_import_file_path, project_whatsnew_file_path, "What's new") + else: + logging.error(project_whatsnew_file_path+" file doesn't exist") + else: + logging.error(whatsnew_import_file_path+" file doesn't exist") + else: + logging.error("'project_whatsnew_file_path' not found in config") + else: + logging.error("'whatsnew_import_file_path' not found in config") + else: + logging.debug("What's New not found in config") + +def main(): + """ + Parse the command line arguments, and pass them to WhitelabelApp. + """ + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--help-config-file', action='store_true', help="Print out a sample config-file, and exit") + parser.add_argument('--config-file', '-c', help="Path to the configuration file") + parser.add_argument('--verbose', '-v', action='count', help="Enable verbose logging.") + args = parser.parse_args() + + if args.help_config_file: + print(WhitelabelApp.EXAMPLE_CONFIG_FILE) + sys.exit(0) + + if not args.config_file: + parser.print_help() + sys.exit(1) + + if args.verbose is None: + args.verbose = 0 + log_level = logging.WARN + if args.verbose > 0: + log_level = logging.DEBUG + logging.basicConfig(level=log_level) + logger = logging.getLogger(name='whitelabel_config') + coloredlogs.install(level=log_level, logger=logger) + + with open(args.config_file) as f: + config = yaml.safe_load(f) or {} + + # Use the config_file's directory as the default config_dir + config.setdefault('config_dir', os.path.dirname(args.config_file)) + + whitelabeler = WhitelabelApp(**config) + whitelabeler.whitelabel() + + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/default_config/config_settings.yaml b/default_config/config_settings.yaml new file mode 100644 index 000000000..249e93fc3 --- /dev/null +++ b/default_config/config_settings.yaml @@ -0,0 +1,5 @@ +config_directory: './default_config' +config_mapping: + prod: 'prod' + stage: 'stage' + dev: 'dev' diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml new file mode 100644 index 000000000..04091f6f7 --- /dev/null +++ b/default_config/dev/config.yaml @@ -0,0 +1,10 @@ +API_HOST_URL: 'http://localhost:8000' +ENVIRONMENT_DISPLAY_NAME: 'Localhost' +FEEDBACK_EMAIL_ADDRESS: 'support@example.com' +OAUTH_CLIENT_ID: '' + +UI_COMPONENTS: + COURSE_BANNER_ENABLED: true + COURSE_TOP_TAB_BAR_ENABLED: false + COURSE_UNIT_PROGRESS_ENABLED: false + COURSE_NESTED_LIST_ENABLED: false diff --git a/default_config/dev/file_mappings.yaml b/default_config/dev/file_mappings.yaml new file mode 100644 index 000000000..86d84fa91 --- /dev/null +++ b/default_config/dev/file_mappings.yaml @@ -0,0 +1,3 @@ +ios: + files: + - config.yaml diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml new file mode 100644 index 000000000..04091f6f7 --- /dev/null +++ b/default_config/prod/config.yaml @@ -0,0 +1,10 @@ +API_HOST_URL: 'http://localhost:8000' +ENVIRONMENT_DISPLAY_NAME: 'Localhost' +FEEDBACK_EMAIL_ADDRESS: 'support@example.com' +OAUTH_CLIENT_ID: '' + +UI_COMPONENTS: + COURSE_BANNER_ENABLED: true + COURSE_TOP_TAB_BAR_ENABLED: false + COURSE_UNIT_PROGRESS_ENABLED: false + COURSE_NESTED_LIST_ENABLED: false diff --git a/default_config/prod/file_mappings.yaml b/default_config/prod/file_mappings.yaml new file mode 100644 index 000000000..86d84fa91 --- /dev/null +++ b/default_config/prod/file_mappings.yaml @@ -0,0 +1,3 @@ +ios: + files: + - config.yaml diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml new file mode 100644 index 000000000..04091f6f7 --- /dev/null +++ b/default_config/stage/config.yaml @@ -0,0 +1,10 @@ +API_HOST_URL: 'http://localhost:8000' +ENVIRONMENT_DISPLAY_NAME: 'Localhost' +FEEDBACK_EMAIL_ADDRESS: 'support@example.com' +OAUTH_CLIENT_ID: '' + +UI_COMPONENTS: + COURSE_BANNER_ENABLED: true + COURSE_TOP_TAB_BAR_ENABLED: false + COURSE_UNIT_PROGRESS_ENABLED: false + COURSE_NESTED_LIST_ENABLED: false diff --git a/default_config/stage/file_mappings.yaml b/default_config/stage/file_mappings.yaml new file mode 100644 index 000000000..86d84fa91 --- /dev/null +++ b/default_config/stage/file_mappings.yaml @@ -0,0 +1,3 @@ +ios: + files: + - config.yaml diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0f4ba890a..35445da9e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -14,7 +14,7 @@ # update_fastlane before_all do - xcversion(version: "~> 14.3") + xcversion(version: "~> 15.0.0") ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "180" ENV["FASTLANE_XCODE_LIST_TIMEOUT"] = "180" diff --git a/generateAllMocks.sh b/generateAllMocks.sh index ac1fe0dd6..0c4b5cfc7 100755 --- a/generateAllMocks.sh +++ b/generateAllMocks.sh @@ -12,4 +12,6 @@ cd ../Discovery cd ../Discussion ./../Pods/SwiftyMocky/bin/swiftymocky generate cd ../Profile +./../Pods/SwiftyMocky/bin/swiftymocky generate +cd ../WhatsNew ./../Pods/SwiftyMocky/bin/swiftymocky generate \ No newline at end of file