From 1bc4a75e88d11624f78e07a6c4be5e99d8df2841 Mon Sep 17 00:00:00 2001 From: Anton Yarmolenko Date: Tue, 2 Jan 2024 18:38:17 +0100 Subject: [PATCH 1/2] Theming whitelabel.py script (#214) * chore: added whitelabel script, added login background color * style: added rounded corner FF * chore: changed assets for progress bar and unibutton color * chore: added multiple assets supporting * chore: move whitelabel config into config repo * chore: moved extra Assets to Theme target * chore: return back splash screen background color * chore: added logs for whitelabel script * chore: rounded buttons theming * chore: changes in whitelabel script * chore: fix after merge * chore: added themed button shape for social login buttons * style: add login navigation text color as separate * chore: added app icon changing * chore: add comment * chore: added bundle id changing * chore: set dev team in project file * chore: got rid original images names from config, now read from Content.json * chore: reorder configs * chore: added documentation * chore: missed quotes * refactor: theme config * chore: backbutton color on sign in view * chore: fixed misspelling * refactor: change to snake_style stage1 * refactor: changed to snake_style stage2 * chore: added app versions, refactor and unify code to search parameters in project file * chore: added install dependencies to documentation * chore: fixed according feedback, improved documentation * chore: fixed how to get config example * chore: improve documentation --- .../Presentation/Login/SignInView.swift | 4 +- .../Registration/SignUpView.swift | 4 +- .../Reset Password/ResetPasswordView.swift | 4 +- Core/Core.xcodeproj/project.pbxproj | 4 + Core/Core/Configuration/Config/Config.swift | 1 + .../Configuration/Config/ThemeConfig.swift | 28 ++ Core/Core/View/Base/ProgressBar.swift | 2 +- Core/Core/View/Base/SocialAuthButton.swift | 3 +- Core/Core/View/Base/StyledButton.swift | 9 +- Core/Core/View/Base/UnitButtonView.swift | 12 +- Documentation/Theming_implementation.md | 80 ++++ OpenEdX/AppDelegate.swift | 2 +- OpenEdX/Base.lproj/LaunchScreen.storyboard | 10 +- .../AccentButtonColor.colorset/Contents.json | 38 ++ .../LoginBackground.colorset/Contents.json | 38 ++ .../Contents.json | 38 ++ .../SplashBackground.colorset/Contents.json | 38 ++ Theme/Theme/SwiftGen/ThemeAssets.swift | 4 + Theme/Theme/Theme.swift | 14 +- .../Elements/WhatsNewNavigationButton.swift | 14 +- config_script/whitelabel.py | 409 ++++++++++++++++++ 21 files changed, 725 insertions(+), 31 deletions(-) create mode 100644 Core/Core/Configuration/Config/ThemeConfig.swift create mode 100644 Documentation/Theming_implementation.md create mode 100644 Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json create mode 100644 Theme/Theme/Assets.xcassets/Colors/LoginBackground.colorset/Contents.json create mode 100644 Theme/Theme/Assets.xcassets/Colors/LoginNavigationText.colorset/Contents.json create mode 100644 Theme/Theme/Assets.xcassets/Colors/SplashBackground.colorset/Contents.json create mode 100644 config_script/whitelabel.py diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index 86f717f8c..b411209ab 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -35,7 +35,7 @@ public struct SignInView: View { 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) @@ -141,7 +141,7 @@ public struct SignInView: View { } .padding(.horizontal, 24) .padding(.top, 50) - }.roundedBackground(Theme.Colors.background) + }.roundedBackground(Theme.Colors.loginBackground) .scrollAvoidKeyboard(dismissKeyboardByTap: true) } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 17986ec7e..97e1053cd 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -39,12 +39,12 @@ public struct SignUpView: View { ZStack { HStack { Text(CoreLocalization.SignIn.registerBtn) - .titleSettings(color: Theme.Colors.white) + .titleSettings(color: Theme.Colors.loginNavigationText) } VStack { Button(action: { viewModel.router.back() }, label: { CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .backButtonStyle(color: Theme.Colors.white) + .backButtonStyle(color: Theme.Colors.loginNavigationText) }) .foregroundColor(Theme.Colors.styledButtonText) .padding(.leading, isHorizontal ? 48 : 0) diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift index 84703fa5d..cb91ac413 100644 --- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift +++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift @@ -34,8 +34,8 @@ public struct ResetPasswordView: View { VStack(alignment: .center) { NavigationBar(title: AuthLocalization.Forgot.title, - titleColor: Theme.Colors.white, - leftButtonColor: Theme.Colors.white, + titleColor: Theme.Colors.loginNavigationText, + leftButtonColor: Theme.Colors.loginNavigationText, leftButtonAction: { viewModel.router.back() }).padding(.leading, isHorizontal ? 48 : 0) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index c3289cc3d..d7552335e 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -118,6 +118,7 @@ 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE5E28D0B22C006D8A5D /* Strings.swift */; }; 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE6028D0B2CB006D8A5D /* Assets.swift */; }; 07DDFCBD29A780BB00572595 /* UINavigationController+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */; }; + A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; }; BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA30427D2B20B299009B64B7 /* SocialAuthError.swift */; }; BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */; }; BA8B3A2F2AD546A700D25EF5 /* DebugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */; }; @@ -291,6 +292,7 @@ 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 = ""; }; BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthResponse.swift; sourceTree = ""; }; BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLog.swift; sourceTree = ""; }; @@ -707,6 +709,7 @@ BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */, BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */, BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */, + A53A32342B233DEC005FE38A /* ThemeConfig.swift */, ); path = Config; sourceTree = ""; @@ -1040,6 +1043,7 @@ 024BE3DF29B2615500BCDEE2 /* CGColorExtension.swift in Sources */, 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */, 0727878928D31734002E9142 /* User.swift in Sources */, + A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */, 02280F5B294B4E6F0032823A /* Connectivity.swift in Sources */, 02066B482906F73400F4307E /* PickerMenu.swift in Sources */, ); diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift index 75c6dc7c4..7cd10c224 100644 --- a/Core/Core/Configuration/Config/Config.swift +++ b/Core/Core/Configuration/Config/Config.swift @@ -22,6 +22,7 @@ public protocol ConfigProtocol { var google: GoogleConfig { get } var appleSignIn: AppleSignInConfig { get } var features: FeaturesConfig { get } + var theme: ThemeConfig { get } var uiComponents: UIComponentsConfig { get } } 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/View/Base/ProgressBar.swift b/Core/Core/View/Base/ProgressBar.swift index d3ddb9b45..cf1c12802 100644 --- a/Core/Core/View/Base/ProgressBar.swift +++ b/Core/Core/View/Base/ProgressBar.swift @@ -39,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/SocialAuthButton.swift b/Core/Core/View/Base/SocialAuthButton.swift index 71ea25dbc..3ebd367e7 100644 --- a/Core/Core/View/Base/SocialAuthButton.swift +++ b/Core/Core/View/Base/SocialAuthButton.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Theme public struct SocialAuthButton: View { @@ -54,7 +55,7 @@ public struct SocialAuthButton: View { .frame(maxWidth: idiom == .pad ? 260: .infinity, minHeight: 42) .background(backgroundColor) .clipShape( - RoundedRectangle(cornerRadius: cornerRadius) + Theme.Shapes.buttonShape ) } diff --git a/Core/Core/View/Base/StyledButton.swift b/Core/Core/View/Base/StyledButton.swift index d7dea0da2..7190c2469 100644 --- a/Core/Core/View/Base/StyledButton.swift +++ b/Core/Core/View/Base/StyledButton.swift @@ -22,7 +22,7 @@ public struct StyledButton: View { 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, @@ -58,9 +58,10 @@ 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 ? Theme.Colors.white : borderColor) + 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 3f50e3279..99f1f34d6 100644 --- a/Core/Core/View/Base/UnitButtonView.swift +++ b/Core/Core/View/Base/UnitButtonView.swift @@ -143,22 +143,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 @@ -168,14 +168,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/Documentation/Theming_implementation.md b/Documentation/Theming_implementation.md new file mode 100644 index 000000000..7d8ba6f61 --- /dev/null +++ b/Documentation/Theming_implementation.md @@ -0,0 +1,80 @@ +# 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 +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 + 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 +``` +### 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' +## \ No newline at end of file diff --git a/OpenEdX/AppDelegate.swift b/OpenEdX/AppDelegate.swift index 563d587f1..8b6b5bd1f 100644 --- a/OpenEdX/AppDelegate.swift +++ b/OpenEdX/AppDelegate.swift @@ -37,11 +37,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { 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, 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/Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json new file mode 100644 index 000000000..00d59cb46 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.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/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/Theme/Theme/Assets.xcassets/Colors/LoginNavigationText.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/LoginNavigationText.colorset/Contents.json new file mode 100644 index 000000000..22c4bb0a8 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/LoginNavigationText.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/SplashBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SplashBackground.colorset/Contents.json new file mode 100644 index 000000000..99fc4a9bb --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/SplashBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFE", + "green" : "0x7B", + "red" : "0x51" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2F", + "green" : "0x21", + "red" : "0x19" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/SwiftGen/ThemeAssets.swift b/Theme/Theme/SwiftGen/ThemeAssets.swift index 5ce3fb3bf..b50937911 100644 --- a/Theme/Theme/SwiftGen/ThemeAssets.swift +++ b/Theme/Theme/SwiftGen/ThemeAssets.swift @@ -25,6 +25,7 @@ public typealias AssetImageTypeAlias = ImageAsset.Image // 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") @@ -34,10 +35,13 @@ public enum ThemeAssets { 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") diff --git a/Theme/Theme/Theme.swift b/Theme/Theme/Theme.swift index f593091a6..8e9ec9aab 100644 --- a/Theme/Theme/Theme.swift +++ b/Theme/Theme/Theme.swift @@ -14,9 +14,11 @@ 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 @@ -36,6 +38,7 @@ public struct Theme { 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 loginNavigationText = ThemeAssets.loginNavigationText.swiftUIColor public static func update( accentColor: Color = ThemeAssets.accentColor.swiftUIColor, @@ -112,10 +115,17 @@ public struct Theme { } 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 = RoundedRectangle(cornerRadius: 8) - public static let buttonShape = RoundedCorners(tl: 8, tr: 8, bl: 8, br: 8) + 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, diff --git a/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift b/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift index abf6c95c9..828fcbf52 100644 --- a/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift +++ b/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift @@ -46,17 +46,21 @@ struct WhatsNewNavigationButton: View { }.padding(.horizontal, 20) .padding(.vertical, 9) }.fixedSize() - .background(type == .previous + .background( + Theme.Shapes.buttonShape + .fill( + type == .previous ? Theme.Colors.background - : Theme.Colors.accentColor) + : Theme.Colors.accentButtonColor + ) + ) .accessibilityElement(children: .ignore) .accessibilityLabel(type == .previous ? WhatsNewLocalization.buttonPrevious : (type == .next ? WhatsNewLocalization.buttonNext : WhatsNewLocalization.buttonDone )) - .cornerRadius(8) .overlay( - RoundedRectangle(cornerRadius: 8) + Theme.Shapes.buttonShape .stroke(type == .previous - ? Theme.Colors.accentColor + ? Theme.Colors.accentButtonColor : Theme.Colors.background, lineWidth: 1) ) .onTapGesture { action() } diff --git a/config_script/whitelabel.py b/config_script/whitelabel.py new file mode 100644 index 000000000..ef11674dd --- /dev/null +++ b/config_script/whitelabel.py @@ -0,0 +1,409 @@ +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: + 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 + 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 + """) + + def __init__(self, **kwargs): + self.assets_dir = kwargs.get('import_dir') + if not self.assets_dir: + self.assets_dir = '.' + + self.assets = kwargs.get('assets', {}) + self.project_config = kwargs.get('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") + + def whitelabel(self): + # Update the properties, resources, and configuration of the current app. + self.copy_assets() + 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 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 configuration is 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") + + 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 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 From 3bca8bfa994163635e1128f0404007c6d0d4761f Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:23:54 +0100 Subject: [PATCH 2/2] feat: api migration (#213) * change api endpoints New endpoints: Delete account api/user/v1/accounts/deactivate_logout/ Dashboard Courses api/mobile/v3/users/{USERNAME}/course_enrollments Add Comment / Response api/discussion/v1/comments/ Discovery And Search api/courses/v1/courses/ Get Course Details api/mobile/v3/course_info/{COURSE_ID}/info Get Course Structure api/mobile/v3/course_info/blocks/ * Added mobile_search to the search endpoint * Remove unused Data_Mycourse.swift Rename DashCourse -> DashboardCourse * Fix CI? * refactor: address review feedback * fix: SignInViewModel DI missed argument * Change CourseEndpoint.getCourseDetail endpoint * Fix Data_Dashboard parsing CourseMode * Add verified mode to the CourseMode * Fix: Parse enums with fallback to the unknown value * Added the string value to the StartType enum * Remove unused enums --- Core/Core.xcodeproj/project.pbxproj | 8 +- Core/Core/Data/Model/Data_Certificate.swift | 24 ++ Core/Core/Data/Model/Data_Dashboard.swift | 268 +++++++++++++++--- Core/Core/Data/Model/Data_Discovery.swift | 9 +- Core/Core/Data/Model/Data_MyCourse.swift | 159 ----------- Course/Course/Data/CourseRepository.swift | 27 +- .../Course/Data/Network/CourseEndpoint.swift | 19 +- .../Data/Network/DashboardEndpoint.swift | 2 +- .../Data/Network/DiscoveryEndpoint.swift | 3 +- .../Data/Network/DiscussionEndpoint.swift | 2 +- OpenEdX/DI/ScreenAssembly.swift | 2 +- OpenEdX/Router.swift | 8 +- .../Data/Network/ProfileEndpoint.swift | 2 +- ci_scripts/ci_prepare_env.sh | 2 + 14 files changed, 299 insertions(+), 236 deletions(-) create mode 100644 Core/Core/Data/Model/Data_Certificate.swift delete mode 100644 Core/Core/Data/Model/Data_MyCourse.swift diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index d7552335e..e055bfc89 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -55,7 +55,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 */; }; @@ -118,6 +117,7 @@ 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE5E28D0B22C006D8A5D /* Strings.swift */; }; 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE6028D0B2CB006D8A5D /* Assets.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 */; }; A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; }; BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA30427D2B20B299009B64B7 /* SocialAuthError.swift */; }; BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */; }; @@ -219,7 +219,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 = ""; }; @@ -285,6 +284,7 @@ 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 = ""; }; 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 = ""; }; 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 = ""; }; @@ -495,13 +495,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 = ""; @@ -1003,7 +1003,6 @@ 0233D5712AF13EC800BAC8BD /* SelectMailClientView.swift in Sources */, BAFB99842B0E282E007D09F9 /* MicrosoftConfig.swift in Sources */, 02B2B594295C5C7A00914876 /* Thread.swift in Sources */, - 027DB33528D8C8FE002B6862 /* Data_MyCourse.swift in Sources */, 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */, BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */, 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */, @@ -1042,6 +1041,7 @@ 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 */, 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/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index 39903dc87..e21456a1f 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -26,25 +26,30 @@ public protocol CourseRepositoryProtocol { public class CourseRepository: CourseRepositoryProtocol { private let api: API - private let appStorage: CoreStorage + private let coreStorage: CoreStorage private let config: ConfigProtocol private let persistence: CoursePersistenceProtocol - public init(api: API, - appStorage: CoreStorage, - config: ConfigProtocol, - 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) + let response = try await api.requestData( + CourseEndpoint.getCourseDetail(courseID: courseID, username: coreStorage.user?.username ?? "") + ).mapResponse(DataLayer.CourseDetailsResponse.self) .domain(baseURL: config.baseURL.absoluteString) + persistence.saveCourseDetails(course: response) + return response } @@ -54,7 +59,7 @@ public class CourseRepository: CourseRepositoryProtocol { 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) @@ -77,7 +82,7 @@ public class CourseRepository: CourseRepositoryProtocol { 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) ) @@ -96,7 +101,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 } diff --git a/Course/Course/Data/Network/CourseEndpoint.swift b/Course/Course/Data/Network/CourseEndpoint.swift index 63ef3b4e1..3253501e7 100644 --- a/Course/Course/Data/Network/CourseEndpoint.swift +++ b/Course/Course/Data/Network/CourseEndpoint.swift @@ -10,7 +10,7 @@ import Core import Alamofire enum CourseEndpoint: EndPointType { - case getCourseDetail(courseID: String) + case getCourseDetail(courseID: String, username: String) case getCourseBlocks(courseID: String, userName: String) case pageHTML(pageUrlString: String) case enrollToCourse(courseID: String) @@ -23,11 +23,11 @@ enum CourseEndpoint: EndPointType { var path: String { switch self { - case .getCourseDetail(let courseID): - return "/mobile_api_extensions/v1/courses/\(courseID)" + case .getCourseDetail(let courseID, _): + return "/api/courses/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" @@ -35,13 +35,13 @@ enum CourseEndpoint: EndPointType { 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(courseID: let courseID): + case .getCourseDates(let courseID): return "/api/course_home/v1/dates/\(courseID)" } } @@ -77,8 +77,9 @@ enum CourseEndpoint: EndPointType { var task: HTTPTask { switch self { - case .getCourseDetail: - return .requestParameters(encoding: URLEncoding.queryString) + case let .getCourseDetail(_, username): + let params: [String: Encodable] = ["username": username] + return .requestParameters(parameters: params, encoding: URLEncoding.queryString) case let .getCourseBlocks(courseID, userName): let params: [String: Encodable] = [ "username": userName, 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/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift b/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift index 7ce334202..35bfeb384 100644 --- a/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift +++ b/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift @@ -18,7 +18,7 @@ enum DiscoveryEndpoint: EndPointType { case .getDiscovery: return "/api/courses/v1/courses/" case .searchCourses: - return "/mobile_api_extensions/courses/v1/courses/" + return "/api/courses/v1/courses/" } } @@ -48,6 +48,7 @@ enum DiscoveryEndpoint: EndPointType { let params: Parameters = [ "username": username, "mobile": true, + "mobile_search": true, "page": page, "search_term": searchTerm ] 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/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 49be31f3d..710764940 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -203,7 +203,7 @@ class ScreenAssembly: Assembly { container.register(CourseRepositoryProtocol.self) { r in CourseRepository( api: r.resolve(API.self)!, - appStorage: r.resolve(CoreStorage.self)!, + coreStorage: r.resolve(CoreStorage.self)!, config: r.resolve(ConfigProtocol.self)!, persistence: r.resolve(CoursePersistenceProtocol.self)! ) diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index de07dd027..5e69b64ad 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -80,7 +80,6 @@ public class Router: AuthorizationRouter, argument: sourceScreen )! - let controller = UIHostingController(rootView: MainScreenView(viewModel: viewModel)) navigationController.viewControllers = [controller] navigationController.setViewControllers([controller], animated: true) @@ -104,7 +103,12 @@ public class Router: AuthorizationRouter, let controller = UIHostingController(rootView: view) navigationController.setViewControllers([controller], animated: true) } else { - let view = SignInView(viewModel: Container.shared.resolve(SignInViewModel.self)!) + let view = SignInView( + viewModel: Container.shared.resolve( + SignInViewModel.self, + argument: LogistrationSourceScreen.default + )! + ) let controller = UIHostingController(rootView: view) navigationController.setViewControllers([controller], animated: false) } 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/ci_scripts/ci_prepare_env.sh b/ci_scripts/ci_prepare_env.sh index a340aabb9..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 () { @@ -39,6 +40,7 @@ setup_github_actions_environment() { bundle config path vendor/bundle bundle install --jobs 4 --retry 3 + bundle update fastlane pod install }