From 4706755d4b7ab0a48b55885f46348bff5b8d642a Mon Sep 17 00:00:00 2001
From: IvanStepanok <128456094+IvanStepanok@users.noreply.github.com>
Date: Tue, 7 May 2024 16:14:09 +0300
Subject: [PATCH 01/55] feat: [FC-0047] Account Settings and Profile split
(#397)
* feat: Separate account settings and profile editing
* feat: changes according to desing
* feat: update background colors
* feat: update missed email value in tests
* fix: resume old design
* fix: changes after the code review
* fix: code formatting on ManageAccountView
* fix: changes after code review
fix navigation titles and back buttons on few screens
* feat: updates after shafqat-muneer code review
* feat: fixes after review
* feat: changes after code review
---
.../Presentation/Login/SignInView.swift | 4 +-
.../Registration/SignUpView.swift | 2 +-
.../Reset Password/ResetPasswordView.swift | 4 +-
.../deleteAccount.imageset/Contents.json | 15 +
.../deleteAccount.imageset/deleteAccount.svg | 14 +
.../settings.imageset/Contents.json | 15 +
.../settings.imageset/settingsIcon.svg | 4 +
Core/Core/Data/Model/Data_UserProfile.swift | 3 +-
Core/Core/Domain/Model/UserProfile.swift | 6 +-
Core/Core/SwiftGen/Assets.swift | 2 +
Core/Core/View/Base/NavigationBar.swift | 4 +-
.../View/Base/VideoDownloadQualityView.swift | 127 +++++----
.../Outline/CourseOutlineView.swift | 3 +-
.../VideoDownloadQualityContainerView.swift | 12 +-
.../NativeDiscovery/CourseDetailsView.swift | 4 +-
.../WebDiscovery/DiscoveryWebview.swift | 2 +-
.../WebPrograms/ProgramWebviewView.swift | 2 +-
OpenEdX/DI/ScreenAssembly.swift | 16 +-
OpenEdX/Router.swift | 17 +-
OpenEdX/View/MainScreenView.swift | 36 ++-
Profile/Profile.xcodeproj/project.pbxproj | 16 ++
Profile/Profile/Data/ProfileRepository.swift | 12 +-
.../DeleteAccount/DeleteAccountView.swift | 2 +-
.../EditProfile/EditProfileView.swift | 14 +-
.../Presentation/Profile/ProfileView.swift | 260 ++++++------------
.../Profile/ProfileViewModel.swift | 97 +------
.../Subviews/ProfileSupportInfoView.swift | 3 +-
.../Profile/Presentation/ProfileRouter.swift | 8 +
.../Settings/ManageAccountView.swift | 214 ++++++++++++++
.../Settings/ManageAccountViewModel.swift | 76 +++++
.../Presentation/Settings/SettingsView.swift | 243 +++++++++++-----
.../Settings/SettingsViewModel.swift | 111 +++++++-
.../Settings/VideoQualityView.swift | 150 ++++++----
.../Settings/VideoSettingsView.swift | 148 ++++++++++
Profile/Profile/SwiftGen/Strings.swift | 16 +-
Profile/Profile/en.lproj/Localizable.strings | 8 +-
Profile/Profile/uk.lproj/Localizable.strings | 2 +
.../EditProfileViewModelTests.swift | 66 +++--
.../Profile/ProfileViewModelTests.swift | 133 +--------
.../Settings/SettingsViewModelTests.swift | 203 ++++++++++++++
.../ProfileTests/ProfileMock.generated.swift | 30 ++
.../Theme/Assets.xcassets/Auth/Contents.json | 6 -
.../Contents.json | 0
.../Rectangle-2.png | Bin
.../Rectangle.png | Bin
Theme/Theme/SwiftGen/ThemeAssets.swift | 2 +-
46 files changed, 1438 insertions(+), 674 deletions(-)
create mode 100644 Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json
create mode 100644 Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/deleteAccount.svg
create mode 100644 Core/Core/Assets.xcassets/settings.imageset/Contents.json
create mode 100644 Core/Core/Assets.xcassets/settings.imageset/settingsIcon.svg
create mode 100644 Profile/Profile/Presentation/Settings/ManageAccountView.swift
create mode 100644 Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift
create mode 100644 Profile/Profile/Presentation/Settings/VideoSettingsView.swift
create mode 100644 Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift
delete mode 100644 Theme/Theme/Assets.xcassets/Auth/Contents.json
rename Theme/Theme/Assets.xcassets/{Auth/authBackground.imageset => headerBackground.imageset}/Contents.json (100%)
rename Theme/Theme/Assets.xcassets/{Auth/authBackground.imageset => headerBackground.imageset}/Rectangle-2.png (100%)
rename Theme/Theme/Assets.xcassets/{Auth/authBackground.imageset => headerBackground.imageset}/Rectangle.png (100%)
diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift
index 209da1912..20bfcb659 100644
--- a/Authorization/Authorization/Presentation/Login/SignInView.swift
+++ b/Authorization/Authorization/Presentation/Login/SignInView.swift
@@ -27,7 +27,7 @@ public struct SignInView: View {
public var body: some View {
ZStack(alignment: .top) {
VStack {
- ThemeAssets.authBackground.swiftUIImage
+ ThemeAssets.headerBackground.swiftUIImage
.resizable()
.edgesIgnoringSafeArea(.top)
.accessibilityIdentifier("auth_bg_image")
@@ -144,7 +144,7 @@ public struct SignInView: View {
HStack(alignment: .center) {
ProgressBar(size: 40, lineWidth: 8)
.padding(20)
- .accessibilityIdentifier("progressbar")
+ .accessibilityIdentifier("progress_bar")
}.frame(maxWidth: .infinity)
} else {
StyledButton(CoreLocalization.SignIn.logInBtn) {
diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift
index 4c6e154c0..2401ad846 100644
--- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift
+++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift
@@ -29,7 +29,7 @@ public struct SignUpView: View {
public var body: some View {
ZStack(alignment: .top) {
VStack {
- ThemeAssets.authBackground.swiftUIImage
+ ThemeAssets.headerBackground.swiftUIImage
.resizable()
.edgesIgnoringSafeArea(.top)
}
diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift
index a66562028..27dedad2f 100644
--- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift
+++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift
@@ -28,7 +28,7 @@ public struct ResetPasswordView: View {
GeometryReader { proxy in
ZStack(alignment: .top) {
VStack {
- ThemeAssets.authBackground.swiftUIImage
+ ThemeAssets.headerBackground.swiftUIImage
.resizable()
.edgesIgnoringSafeArea(.top)
}
@@ -117,7 +117,7 @@ public struct ResetPasswordView: View {
HStack(alignment: .center) {
ProgressBar(size: 40, lineWidth: 8)
.padding(20)
- .accessibilityIdentifier("progressbar")
+ .accessibilityIdentifier("progress_bar")
}.frame(maxWidth: .infinity)
} else {
StyledButton(AuthLocalization.Forgot.request) {
diff --git a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json
new file mode 100644
index 000000000..9a8a529ea
--- /dev/null
+++ b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "deleteAccount.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/deleteAccount.svg b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/deleteAccount.svg
new file mode 100644
index 000000000..9c2a082f2
--- /dev/null
+++ b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/deleteAccount.svg
@@ -0,0 +1,14 @@
+
diff --git a/Core/Core/Assets.xcassets/settings.imageset/Contents.json b/Core/Core/Assets.xcassets/settings.imageset/Contents.json
new file mode 100644
index 000000000..aa6427af7
--- /dev/null
+++ b/Core/Core/Assets.xcassets/settings.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "settingsIcon.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Core/Core/Assets.xcassets/settings.imageset/settingsIcon.svg b/Core/Core/Assets.xcassets/settings.imageset/settingsIcon.svg
new file mode 100644
index 000000000..c1181ff8e
--- /dev/null
+++ b/Core/Core/Assets.xcassets/settings.imageset/settingsIcon.svg
@@ -0,0 +1,4 @@
+
diff --git a/Core/Core/Data/Model/Data_UserProfile.swift b/Core/Core/Data/Model/Data_UserProfile.swift
index d3541cfd2..fe0e675bf 100644
--- a/Core/Core/Data/Model/Data_UserProfile.swift
+++ b/Core/Core/Data/Model/Data_UserProfile.swift
@@ -133,6 +133,7 @@ public extension DataLayer.UserProfile {
country: country ?? "",
spokenLanguage: languageProficiencies?[safe: 0]?.code ?? "",
shortBiography: bio ?? "",
- isFullProfile: accountPrivacy?.boolValue ?? true)
+ isFullProfile: accountPrivacy?.boolValue ?? true,
+ email: email ?? "")
}
}
diff --git a/Core/Core/Domain/Model/UserProfile.swift b/Core/Core/Domain/Model/UserProfile.swift
index 03b19990a..2ad1b6456 100644
--- a/Core/Core/Domain/Model/UserProfile.swift
+++ b/Core/Core/Domain/Model/UserProfile.swift
@@ -17,6 +17,7 @@ public struct UserProfile: Hashable {
public let spokenLanguage: String?
public let shortBiography: String
public let isFullProfile: Bool
+ public let email: String
public init(
avatarUrl: String,
@@ -27,7 +28,8 @@ public struct UserProfile: Hashable {
country: String,
spokenLanguage: String? = nil,
shortBiography: String,
- isFullProfile: Bool
+ isFullProfile: Bool,
+ email: String
) {
self.avatarUrl = avatarUrl
self.name = name
@@ -38,6 +40,7 @@ public struct UserProfile: Hashable {
self.spokenLanguage = spokenLanguage
self.shortBiography = shortBiography
self.isFullProfile = isFullProfile
+ self.email = email
}
public init() {
@@ -50,5 +53,6 @@ public struct UserProfile: Hashable {
self.spokenLanguage = ""
self.shortBiography = ""
self.isFullProfile = true
+ self.email = ""
}
}
diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift
index e25f452d0..a6cb7b057 100644
--- a/Core/Core/SwiftGen/Assets.swift
+++ b/Core/Core/SwiftGen/Assets.swift
@@ -75,6 +75,7 @@ public enum CoreAssets {
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")
@@ -113,6 +114,7 @@ public enum CoreAssets {
public static let noWifiMini = ImageAsset(name: "noWifiMini")
public static let notAvaliable = ImageAsset(name: "notAvaliable")
public static let playVideo = ImageAsset(name: "playVideo")
+ public static let settings = ImageAsset(name: "settings")
public static let star = ImageAsset(name: "star")
public static let starOutline = ImageAsset(name: "star_outline")
public static let warning = ImageAsset(name: "warning")
diff --git a/Core/Core/View/Base/NavigationBar.swift b/Core/Core/View/Base/NavigationBar.swift
index 8815cf2b8..489d31236 100644
--- a/Core/Core/View/Base/NavigationBar.swift
+++ b/Core/Core/View/Base/NavigationBar.swift
@@ -23,6 +23,7 @@ public struct NavigationBar: View {
private let rightButtonType: ButtonType?
private let rightButtonAction: (() -> Void)?
@Binding private var rightButtonIsActive: Bool
+ @Environment (\.isHorizontal) private var isHorizontal
public init(title: String,
titleColor: Color = Theme.Colors.navigationBarTintColor,
@@ -53,8 +54,9 @@ public struct NavigationBar: View {
if leftButton {
VStack {
BackNavigationButton(color: leftButtonColor, action: leftButtonAction)
- .padding(8)
.backViewStyle()
+ .padding(.leading, isHorizontal ? 48 : 0)
+ .accessibilityIdentifier("back_button")
}
.frame(minWidth: 0,
maxWidth: .infinity,
diff --git a/Core/Core/View/Base/VideoDownloadQualityView.swift b/Core/Core/View/Base/VideoDownloadQualityView.swift
index 2401a4f27..06e3f7c5a 100644
--- a/Core/Core/View/Base/VideoDownloadQualityView.swift
+++ b/Core/Core/View/Base/VideoDownloadQualityView.swift
@@ -33,11 +33,14 @@ public struct VideoDownloadQualityView: View {
@StateObject
private var viewModel: VideoDownloadQualityViewModel
private var analytics: CoreAnalytics
+ private var router: BaseRouter
+ @Environment (\.isHorizontal) private var isHorizontal
public init(
downloadQuality: DownloadQuality,
didSelect: ((DownloadQuality) -> Void)?,
- analytics: CoreAnalytics
+ analytics: CoreAnalytics,
+ router: BaseRouter
) {
self._viewModel = StateObject(
wrappedValue: .init(
@@ -46,64 +49,94 @@ public struct VideoDownloadQualityView: View {
)
)
self.analytics = analytics
+ self.router = router
}
public var body: some View {
GeometryReader { proxy in
ZStack(alignment: .top) {
- // MARK: - Page Body
- ScrollView {
- VStack(alignment: .leading, spacing: 24) {
- ForEach(viewModel.downloadQuality, id: \.self) { quality in
- Button(action: {
- analytics.videoQualityChanged(
- .videoDownloadQualityChanged,
- bivalue: .videoDownloadQualityChanged,
- value: quality.value ?? "",
- oldValue: viewModel.selectedDownloadQuality.value ?? ""
- )
-
- viewModel.selectedDownloadQuality = quality
- }, label: {
- HStack {
- SettingsCell(
- title: quality.title,
- description: quality.description
+ VStack {
+ ThemeAssets.headerBackground.swiftUIImage
+ .resizable()
+ .edgesIgnoringSafeArea(.top)
+ }
+ .frame(maxWidth: .infinity, maxHeight: 200)
+ .accessibilityIdentifier("auth_bg_image")
+
+ // MARK: - Page name
+ VStack(alignment: .center) {
+ ZStack {
+ HStack {
+ Text(CoreLocalization.Settings.videoDownloadQualityTitle)
+ .titleSettings(color: Theme.Colors.loginNavigationText)
+ .accessibilityIdentifier("manage_account_text")
+ }
+ VStack {
+ BackNavigationButton(
+ color: Theme.Colors.loginNavigationText,
+ action: {
+ router.back()
+ }
+ )
+ .backViewStyle()
+ .padding(.leading, isHorizontal ? 48 : 0)
+ .accessibilityIdentifier("back_button")
+
+ }.frame(minWidth: 0,
+ maxWidth: .infinity,
+ alignment: .topLeading)
+ }
+ // MARK: - Page Body
+ ScrollView {
+ VStack(alignment: .leading, spacing: 24) {
+ ForEach(viewModel.downloadQuality, id: \.self) { quality in
+ Button(action: {
+ analytics.videoQualityChanged(
+ .videoDownloadQualityChanged,
+ bivalue: .videoDownloadQualityChanged,
+ value: quality.value ?? "",
+ oldValue: viewModel.selectedDownloadQuality.value ?? ""
)
- .accessibilityElement(children: .ignore)
- .accessibilityLabel("\(quality.title) \(quality.description ?? "")")
- Spacer()
- CoreAssets.checkmark.swiftUIImage
- .renderingMode(.template)
- .foregroundColor(Theme.Colors.accentXColor)
- .opacity(quality == viewModel.selectedDownloadQuality ? 1 : 0)
- .accessibilityIdentifier("checkmark_image")
- }
- .foregroundColor(Theme.Colors.textPrimary)
- })
- .accessibilityIdentifier("select_quality_button")
- Divider()
+ 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(Theme.Colors.accentXColor)
+ .opacity(quality == viewModel.selectedDownloadQuality ? 1 : 0)
+ .accessibilityIdentifier("checkmark_image")
+
+ }
+ .foregroundColor(Theme.Colors.textPrimary)
+ })
+ .accessibilityIdentifier("select_quality_button")
+ Divider()
+ }
}
+ .frameLimit(width: proxy.size.width)
+ .padding(.horizontal, 24)
+ .padding(.top, 24)
}
- .frame(
- minWidth: 0,
- maxWidth: .infinity,
- alignment: .topLeading
- )
- .padding(.horizontal, 24)
- .frameLimit(width: proxy.size.width)
+ .roundedBackground(Theme.Colors.background)
}
- .padding(.top, 8)
}
- .navigationBarHidden(false)
- .navigationBarBackButtonHidden(false)
- .navigationTitle(CoreLocalization.Settings.videoDownloadQualityTitle)
- .background(
- Theme.Colors.background
- .ignoresSafeArea()
- )
}
+ .navigationBarHidden(true)
+ .navigationBarBackButtonHidden(true)
+ .navigationTitle(CoreLocalization.Settings.videoDownloadQualityTitle)
+ .ignoresSafeArea(.all, edges: .horizontal)
+ .background(
+ Theme.Colors.background
+ .ignoresSafeArea()
+ )
}
}
diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift
index 18575dc7a..741b76bdf 100644
--- a/Course/Course/Presentation/Outline/CourseOutlineView.swift
+++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift
@@ -223,7 +223,8 @@ public struct CourseOutlineView: View {
VideoDownloadQualityContainerView(
downloadQuality: $0.downloadQuality,
didSelect: viewModel.update(downloadQuality:),
- analytics: viewModel.coreAnalytics
+ analytics: viewModel.coreAnalytics,
+ router: viewModel.router
)
}
}
diff --git a/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift
index 4418d3513..09fcf8001 100644
--- a/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift
+++ b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift
@@ -16,11 +16,18 @@ struct VideoDownloadQualityContainerView: View {
private var downloadQuality: DownloadQuality
private var didSelect: ((DownloadQuality) -> Void)?
private let analytics: CoreAnalytics
+ private let router: CourseRouter
- init(downloadQuality: DownloadQuality, didSelect: ((DownloadQuality) -> Void)?, analytics: CoreAnalytics) {
+ init(
+ downloadQuality: DownloadQuality,
+ didSelect: ((DownloadQuality) -> Void)?,
+ analytics: CoreAnalytics,
+ router: CourseRouter
+ ) {
self.downloadQuality = downloadQuality
self.didSelect = didSelect
self.analytics = analytics
+ self.router = router
}
var body: some View {
@@ -28,7 +35,8 @@ struct VideoDownloadQualityContainerView: View {
VideoDownloadQualityView(
downloadQuality: downloadQuality,
didSelect: didSelect,
- analytics: analytics
+ analytics: analytics,
+ router: router
)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift
index 427cd4ade..2300433ef 100644
--- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift
+++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift
@@ -47,7 +47,7 @@ public struct CourseDetailsView: View {
ProgressBar(size: 40, lineWidth: 8)
.padding(.top, 200)
.padding(.horizontal)
- .accessibilityIdentifier("progressbar")
+ .accessibilityIdentifier("progress_bar")
}.frame(width: proxy.size.width)
} else {
RefreshableScrollViewCompat(action: {
@@ -132,7 +132,7 @@ public struct CourseDetailsView: View {
ProgressBar(size: 40, lineWidth: 8)
.padding(.top, 20)
.frame(maxWidth: .infinity)
- .accessibilityIdentifier("progressbar")
+ .accessibilityIdentifier("progress_bar")
}
}
}
diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift
index 84052bc6d..be01b0be4 100644
--- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift
+++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift
@@ -102,7 +102,7 @@ public struct DiscoveryWebview: View {
lineWidth: 8
)
.padding(.vertical, proxy.size.height / 2)
- .accessibilityIdentifier("progressbar")
+ .accessibilityIdentifier("progress_bar")
}
.frame(width: proxy.size.width, height: proxy.size.height)
}
diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift
index f97dd2c8c..ad28e6938 100644
--- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift
+++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift
@@ -78,7 +78,7 @@ public struct ProgramWebviewView: View {
lineWidth: 8
)
.padding(.vertical, proxy.size.height / 2)
- .accessibilityIdentifier("progressbar")
+ .accessibilityIdentifier("progress_bar")
}
.frame(width: proxy.size.width, height: proxy.size.height)
}
diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift
index ec0ed004c..131cf674b 100644
--- a/OpenEdX/DI/ScreenAssembly.swift
+++ b/OpenEdX/DI/ScreenAssembly.swift
@@ -188,7 +188,6 @@ class ScreenAssembly: Assembly {
container.register(ProfileViewModel.self) { r in
ProfileViewModel(
interactor: r.resolve(ProfileInteractorProtocol.self)!,
- downloadManager: r.resolve(DownloadManagerProtocol.self)!,
router: r.resolve(ProfileRouter.self)!,
analytics: r.resolve(ProfileAnalytics.self)!,
config: r.resolve(ConfigProtocol.self)!,
@@ -208,8 +207,21 @@ class ScreenAssembly: Assembly {
container.register(SettingsViewModel.self) { r in
SettingsViewModel(
interactor: r.resolve(ProfileInteractorProtocol.self)!,
+ downloadManager: r.resolve(DownloadManagerProtocol.self)!,
router: r.resolve(ProfileRouter.self)!,
- analytics: r.resolve(CoreAnalytics.self)!
+ analytics: r.resolve(ProfileAnalytics.self)!,
+ coreAnalytics: r.resolve(CoreAnalytics.self)!,
+ config: r.resolve(ConfigProtocol.self)!
+ )
+ }
+
+ container.register(ManageAccountViewModel.self) { r in
+ ManageAccountViewModel(
+ router: r.resolve(ProfileRouter.self)!,
+ analytics: r.resolve(ProfileAnalytics.self)!,
+ config: r.resolve(ConfigProtocol.self)!,
+ connectivity: r.resolve(ConnectivityProtocol.self)!,
+ interactor: r.resolve(ProfileInteractorProtocol.self)!
)
}
diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift
index 6c96146b3..80fd75ad6 100644
--- a/OpenEdX/Router.swift
+++ b/OpenEdX/Router.swift
@@ -694,6 +694,20 @@ public class Router: AuthorizationRouter,
navigationController.pushViewController(controller, animated: true)
}
+ public func showVideoSettings() {
+ let viewModel = Container.shared.resolve(SettingsViewModel.self)!
+ let view = VideoSettingsView(viewModel: viewModel)
+ let controller = UIHostingController(rootView: view)
+ navigationController.pushViewController(controller, animated: true)
+ }
+
+ public func showManageAccount() {
+ let viewModel = Container.shared.resolve(ManageAccountViewModel.self)!
+ let view = ManageAccountView(viewModel: viewModel)
+ let controller = UIHostingController(rootView: view)
+ navigationController.pushViewController(controller, animated: true)
+ }
+
public func showVideoQualityView(viewModel: SettingsViewModel) {
let view = VideoQualityView(viewModel: viewModel)
let controller = UIHostingController(rootView: view)
@@ -708,7 +722,8 @@ public class Router: AuthorizationRouter,
let view = VideoDownloadQualityView(
downloadQuality: downloadQuality,
didSelect: didSelect,
- analytics: analytics
+ analytics: analytics,
+ router: self
)
let controller = UIHostingController(rootView: view)
navigationController.pushViewController(controller, animated: true)
diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift
index f51ebc476..98e349542 100644
--- a/OpenEdX/View/MainScreenView.swift
+++ b/OpenEdX/View/MainScreenView.swift
@@ -17,12 +17,11 @@ import Theme
struct MainScreenView: View {
- @State private var settingsTapped: Bool = false
@State private var disableAllTabs: Bool = false
- @State private var updateAvaliable: Bool = false
+ @State private var updateAvailable: Bool = false
@ObservedObject private(set) var viewModel: MainScreenViewModel
-
+
init(viewModel: MainScreenViewModel) {
self.viewModel = viewModel
UITabBar.appearance().isTranslucent = false
@@ -35,7 +34,7 @@ struct MainScreenView: View {
for: .normal
)
}
-
+
var body: some View {
TabView(selection: $viewModel.selection) {
let config = Container.shared.resolve(ConfigProtocol.self)
@@ -56,7 +55,7 @@ struct MainScreenView: View {
)
}
- if updateAvaliable {
+ if updateAvailable {
UpdateNotificationView(config: viewModel.config)
}
}
@@ -73,7 +72,7 @@ struct MainScreenView: View {
viewModel: Container.shared.resolve(DashboardViewModel.self)!,
router: Container.shared.resolve(DashboardRouter.self)!
)
- if updateAvaliable {
+ if updateAvailable {
UpdateNotificationView(config: viewModel.config)
}
}
@@ -96,7 +95,7 @@ struct MainScreenView: View {
.accessibilityIdentifier("indevelopment_program_text")
}
- if updateAvaliable {
+ if updateAvailable {
UpdateNotificationView(config: viewModel.config)
}
}
@@ -110,7 +109,7 @@ struct MainScreenView: View {
VStack {
ProfileView(
- viewModel: Container.shared.resolve(ProfileViewModel.self)!, settingsTapped: $settingsTapped
+ viewModel: Container.shared.resolve(ProfileViewModel.self)!
)
}
.tabItem {
@@ -125,17 +124,14 @@ struct MainScreenView: View {
.navigationTitle(titleBar())
.toolbar {
ToolbarItem(placement: .navigationBarTrailing, content: {
- if viewModel.selection == .profile {
- Button(action: {
- settingsTapped.toggle()
- }, label: {
- CoreAssets.edit.swiftUIImage.renderingMode(.template)
- .foregroundColor(Theme.Colors.navigationBarTintColor)
- })
- .accessibilityIdentifier("edit_profile_button")
- } else {
- VStack {}
- }
+ Button(action: {
+ let router = Container.shared.resolve(ProfileRouter.self)!
+ router.showSettings()
+ }, label: {
+ CoreAssets.settings.swiftUIImage.renderingMode(.template)
+ .foregroundColor(Theme.Colors.accentColor)
+ })
+ .accessibilityIdentifier("edit_profile_button")
})
}
.onReceive(NotificationCenter.default.publisher(for: .onAppUpgradeAccountSettingsTapped)) { _ in
@@ -143,7 +139,7 @@ struct MainScreenView: View {
disableAllTabs = true
}
.onReceive(NotificationCenter.default.publisher(for: .onNewVersionAvaliable)) { _ in
- updateAvaliable = true
+ updateAvailable = true
}
.onChange(of: viewModel.selection) { _ in
if disableAllTabs {
diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj
index f4ebc7e78..a9a09fc33 100644
--- a/Profile/Profile.xcodeproj/project.pbxproj
+++ b/Profile/Profile.xcodeproj/project.pbxproj
@@ -10,6 +10,7 @@
020102D129784B3100BBF80C /* EditProfileViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020102D029784B3100BBF80C /* EditProfileViewModelTests.swift */; };
020306C82932B13F000949EA /* EditProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020306C72932B13F000949EA /* EditProfileView.swift */; };
020306CA2932B14D000949EA /* EditProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020306C92932B14D000949EA /* EditProfileViewModel.swift */; };
+ 020CBD8D2BC53E1B003D6B4E /* VideoSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020CBD8C2BC53E1B003D6B4E /* VideoSettingsView.swift */; };
021D924628DC634300ACC565 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924528DC634300ACC565 /* ProfileView.swift */; };
021D924C28DC884A00ACC565 /* ProfileEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924B28DC884A00ACC565 /* ProfileEndpoint.swift */; };
021D924E28DC88BB00ACC565 /* ProfileRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924D28DC88BB00ACC565 /* ProfileRepository.swift */; };
@@ -24,6 +25,8 @@
025DE1A028DB4D9D0053E0F4 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 025DE19F28DB4D9D0053E0F4 /* Core.framework */; };
0262149229AE57A1008BD75A /* DeleteAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0262149129AE57A1008BD75A /* DeleteAccountView.swift */; };
0262149429AE57B1008BD75A /* DeleteAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0262149329AE57B1008BD75A /* DeleteAccountViewModel.swift */; };
+ 0288C4EA2BC6AE2D009158B9 /* ManageAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0288C4E92BC6AE2D009158B9 /* ManageAccountView.swift */; };
+ 0288C4EC2BC6AE82009158B9 /* ManageAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0288C4EB2BC6AE82009158B9 /* ManageAccountViewModel.swift */; };
029301DA2938948500E99AB8 /* ProfileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029301D92938948500E99AB8 /* ProfileType.swift */; };
02A4833329B7710A00D33F33 /* DeleteAccountViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833229B7710A00D33F33 /* DeleteAccountViewModelTests.swift */; };
02A9A91D2978194A00B55797 /* ProfileViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */; };
@@ -34,6 +37,7 @@
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 */; };
+ 02FE9A802BC707D500B3C206 /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FE9A7F2BC707D400B3C206 /* SettingsViewModelTests.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 */; };
@@ -54,6 +58,7 @@
020102D029784B3100BBF80C /* EditProfileViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileViewModelTests.swift; sourceTree = ""; };
020306C72932B13F000949EA /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = ""; };
020306C92932B14D000949EA /* EditProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileViewModel.swift; sourceTree = ""; };
+ 020CBD8C2BC53E1B003D6B4E /* VideoSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoSettingsView.swift; sourceTree = ""; };
020F834A28DB4CCD0062FA70 /* Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; };
021D924528DC634300ACC565 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; };
021D924B28DC884A00ACC565 /* ProfileEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEndpoint.swift; sourceTree = ""; };
@@ -69,6 +74,8 @@
025DE19F28DB4D9D0053E0F4 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0262149129AE57A1008BD75A /* DeleteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountView.swift; sourceTree = ""; };
0262149329AE57B1008BD75A /* DeleteAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountViewModel.swift; sourceTree = ""; };
+ 0288C4E92BC6AE2D009158B9 /* ManageAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAccountView.swift; sourceTree = ""; };
+ 0288C4EB2BC6AE82009158B9 /* ManageAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAccountViewModel.swift; sourceTree = ""; };
029301D92938948500E99AB8 /* ProfileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileType.swift; sourceTree = ""; };
02A4833229B7710A00D33F33 /* DeleteAccountViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeleteAccountViewModelTests.swift; path = ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift; sourceTree = SOURCE_ROOT; };
02A9A91A2978194A00B55797 /* ProfileTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ProfileTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -80,6 +87,7 @@
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 = ""; };
+ 02FE9A7F2BC707D400B3C206 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SettingsViewModelTests.swift; path = ProfileTests/Presentation/Settings/SettingsViewModelTests.swift; sourceTree = SOURCE_ROOT; };
0796C8C829B7905300444B05 /* ProfileBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileBottomSheet.swift; sourceTree = ""; };
0E5054C44435557666B6D885 /* Pods-App-Profile.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debugstage.xcconfig"; sourceTree = ""; };
3674C51E1BE41D834B5C4E99 /* Pods-App-Profile.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debugdev.xcconfig"; sourceTree = ""; };
@@ -239,6 +247,9 @@
isa = PBXGroup;
children = (
0259104329C39C9E004B5A55 /* SettingsView.swift */,
+ 0288C4E92BC6AE2D009158B9 /* ManageAccountView.swift */,
+ 0288C4EB2BC6AE82009158B9 /* ManageAccountViewModel.swift */,
+ 020CBD8C2BC53E1B003D6B4E /* VideoSettingsView.swift */,
0259104729C3A5F0004B5A55 /* VideoQualityView.swift */,
0259104529C39CCF004B5A55 /* SettingsViewModel.swift */,
);
@@ -257,6 +268,7 @@
02A4832F29B770B600D33F33 /* Profile */ = {
isa = PBXGroup;
children = (
+ 02FE9A7F2BC707D400B3C206 /* SettingsViewModelTests.swift */,
02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */,
);
path = Profile;
@@ -572,6 +584,7 @@
buildActionMask = 2147483647;
files = (
021D924E28DC88BB00ACC565 /* ProfileRepository.swift in Sources */,
+ 0288C4EC2BC6AE82009158B9 /* ManageAccountViewModel.swift in Sources */,
0796C8C929B7905300444B05 /* ProfileBottomSheet.swift in Sources */,
021D924C28DC884A00ACC565 /* ProfileEndpoint.swift in Sources */,
BAD9CA3F2B29BF5C00DE790A /* ProfileSupportInfoView.swift in Sources */,
@@ -587,9 +600,11 @@
0248F9B128DDB09D0041327E /* Strings.swift in Sources */,
020306CA2932B14D000949EA /* EditProfileViewModel.swift in Sources */,
0259104829C3A5F0004B5A55 /* VideoQualityView.swift in Sources */,
+ 0288C4EA2BC6AE2D009158B9 /* ManageAccountView.swift in Sources */,
021D924628DC634300ACC565 /* ProfileView.swift in Sources */,
02F3BFE7292539850051930C /* ProfileRouter.swift in Sources */,
02D0FD0B2AD6984D0020D752 /* UserProfileViewModel.swift in Sources */,
+ 020CBD8D2BC53E1B003D6B4E /* VideoSettingsView.swift in Sources */,
02F175352A4DAD030019CD70 /* ProfileAnalytics.swift in Sources */,
0262149429AE57B1008BD75A /* DeleteAccountViewModel.swift in Sources */,
);
@@ -601,6 +616,7 @@
files = (
02A9A92B29781A6300B55797 /* ProfileMock.generated.swift in Sources */,
02A4833329B7710A00D33F33 /* DeleteAccountViewModelTests.swift in Sources */,
+ 02FE9A802BC707D500B3C206 /* SettingsViewModelTests.swift in Sources */,
02A9A91D2978194A00B55797 /* ProfileViewModelTests.swift in Sources */,
020102D129784B3100BBF80C /* EditProfileViewModelTests.swift in Sources */,
);
diff --git a/Profile/Profile/Data/ProfileRepository.swift b/Profile/Profile/Data/ProfileRepository.swift
index 7608ff849..ffeca922d 100644
--- a/Profile/Profile/Data/ProfileRepository.swift
+++ b/Profile/Profile/Data/ProfileRepository.swift
@@ -164,7 +164,8 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol {
yearOfBirth: 0,
country: "",
shortBiography: "",
- isFullProfile: false)
+ isFullProfile: false,
+ email: "")
}
func getMyProfileOffline() -> Core.UserProfile? {
@@ -182,7 +183,8 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol {
of his music, writing and drawings, on film, and in interviews. His songwriting partnership with Paul McCartney
remains the most successful in history
""",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
}
@@ -201,7 +203,8 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol {
of his music, writing and drawings, on film, and in interviews. His songwriting partnership with Paul McCartney
remains the most successful in history
""",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
}
@@ -224,7 +227,8 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol {
yearOfBirth: 1970,
country: "USA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
}
diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift
index c2f5dc7fb..044e4eb18 100644
--- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift
+++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift
@@ -104,7 +104,7 @@ public struct DeleteAccountView: View {
ProgressBar(size: 40, lineWidth: 8)
.padding(.top, 20)
.padding(.horizontal)
- .accessibilityIdentifier("progressbar")
+ .accessibilityIdentifier("progress_bar")
} else {
StyledButton(
ProfileLocalization.DeleteAccount.comfirm,
diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift
index 9e2fe176f..2ec255976 100644
--- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift
+++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift
@@ -123,15 +123,6 @@ public struct EditProfileView: View {
}
})
- Button(ProfileLocalization.Edit.deleteAccount, action: {
- viewModel.trackProfileDeleteAccountClicked()
- viewModel.router.showDeleteProfileView()
- })
- .font(Theme.Fonts.labelLarge)
- .foregroundColor(Theme.Colors.alert)
- .padding(.top, 44)
- .accessibilityIdentifier("delete_account_button")
-
Spacer(minLength: 84)
}
.padding(.horizontal, 24)
@@ -204,7 +195,7 @@ public struct EditProfileView: View {
ProgressBar(size: 40, lineWidth: 8)
.padding(.top, 150)
.padding(.horizontal)
- .accessibilityIdentifier("progressbar")
+ .accessibilityIdentifier("progress_bar")
}
}
.navigationBarHidden(false)
@@ -260,7 +251,8 @@ struct EditProfileView_Previews: PreviewProvider {
yearOfBirth: 0,
country: "Ukraine",
shortBiography: "",
- isFullProfile: true
+ isFullProfile: true,
+ email: "peter@example.org"
)
EditProfileView(
diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift
index 03c920b2f..b643845bd 100644
--- a/Profile/Profile/Presentation/Profile/ProfileView.swift
+++ b/Profile/Profile/Presentation/Profile/ProfileView.swift
@@ -11,15 +11,13 @@ import Kingfisher
import Theme
public struct ProfileView: View {
-
+
@StateObject private var viewModel: ProfileViewModel
- @Binding var settingsTapped: Bool
-
- public init(viewModel: ProfileViewModel, settingsTapped: Binding) {
+
+ public init(viewModel: ProfileViewModel) {
self._viewModel = StateObject(wrappedValue: { viewModel }())
- self._settingsTapped = settingsTapped
}
-
+
public var body: some View {
GeometryReader { proxy in
ZStack(alignment: .top) {
@@ -35,24 +33,9 @@ public struct ProfileView: View {
)
.accessibilityAction {}
.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
- }
- if let updatedImage {
- self.viewModel.updatedAvatar = updatedImage
- }
- }
- )
- })
.navigationBarHidden(false)
.navigationBarBackButtonHidden(false)
+ .navigationTitle(ProfileLocalization.title)
// MARK: - Offline mode SnackBar
OfflineSnackBarView(
@@ -81,7 +64,7 @@ public struct ProfileView: View {
}
}
}
- .onFirstAppear {
+ .onAppear {
Task {
await viewModel.getMyProfile()
}
@@ -97,170 +80,96 @@ public struct ProfileView: View {
}
}
}
-
+
private var progressBar: some View {
ProgressBar(size: 40, lineWidth: 8)
.padding(.top, 200)
.padding(.horizontal)
}
-
+
+ private var editProfileButton: some View {
+ StyledButton(
+ ProfileLocalization.editProfile,
+ action: {
+ 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
+ }
+ if let updatedImage {
+ self.viewModel.updatedAvatar = updatedImage
+ }
+ }
+ )
+ },
+ color: .clear,
+ textColor: Theme.Colors.accentColor,
+ borderColor: Theme.Colors.accentColor
+ ).padding(.all, 24)
+ }
+
private var content: some View {
VStack {
if viewModel.isShowProgress {
ProgressBar(size: 40, lineWidth: 8)
.padding(.top, 200)
.padding(.horizontal)
- .accessibilityIdentifier("progressbar")
+ .accessibilityIdentifier("progress_bar")
} else {
- UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar)
- .padding(.top, 30)
- .accessibilityIdentifier("user_avatar_image")
- Text(viewModel.userModel?.name ?? "")
- .font(Theme.Fonts.headlineSmall)
- .foregroundColor(Theme.Colors.textPrimary)
- .padding(.top, 20)
- .accessibilityIdentifier("user_name_text")
- Text("@\(viewModel.userModel?.username ?? "")")
- .font(Theme.Fonts.labelLarge)
- .padding(.top, 4)
- .foregroundColor(Theme.Colors.textSecondary)
- .padding(.bottom, 10)
- .accessibilityIdentifier("user_username_text")
+ HStack(alignment: .center, spacing: 12) {
+ UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar)
+ .accessibilityIdentifier("user_avatar_image")
+ VStack(alignment: .leading, spacing: 4) {
+ Text(viewModel.userModel?.name ?? "")
+ .font(Theme.Fonts.headlineSmall)
+ .foregroundColor(Theme.Colors.textPrimary)
+ .accessibilityIdentifier("user_name_text")
+ Text("@\(viewModel.userModel?.username ?? "")")
+ .font(Theme.Fonts.labelLarge)
+ .foregroundColor(Theme.Colors.textSecondary)
+ .accessibilityIdentifier("user_username_text")
+ }
+ Spacer()
+ }.padding(.all, 24)
profileInfo
- VStack(alignment: .leading, spacing: 14) {
- settings
- ProfileSupportInfoView(viewModel: viewModel)
- logOutButton
- }
+ editProfileButton
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)
- .foregroundColor(Theme.Colors.textSecondary)
+ if let bio = viewModel.userModel?.shortBiography, bio != "" {
+ VStack(alignment: .leading, spacing: 6) {
+ Text(ProfileLocalization.about)
+ .font(Theme.Fonts.titleSmall)
+ .foregroundColor(Theme.Colors.textPrimary)
.accessibilityIdentifier("profile_info_text")
-
- VStack(alignment: .leading, spacing: 16) {
- if viewModel.userModel?.yearOfBirth != 0 {
- HStack {
- Text(ProfileLocalization.Edit.Fields.yearOfBirth)
- .foregroundColor(Theme.Colors.textSecondary)
- .accessibilityIdentifier("yob_text")
- Text(String(viewModel.userModel?.yearOfBirth ?? 0))
- .foregroundColor(Theme.Colors.textPrimary)
- .accessibilityIdentifier("yob_value_text")
- }
- .font(Theme.Fonts.titleMedium)
- }
- if let bio = viewModel.userModel?.shortBiography, bio != "" {
- HStack(alignment: .top) {
- Text(ProfileLocalization.bio + " ")
- .foregroundColor(Theme.Colors.textPrimary)
- + Text(bio)
- }
- .accessibilityIdentifier("bio_text")
- }
- }
- .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)
- .foregroundColor(Theme.Colors.textSecondary)
- .accessibilityIdentifier("settings_text")
-
- VStack(alignment: .leading, spacing: 27) {
- Button(action: {
- viewModel.trackProfileVideoSettingsClicked()
- viewModel.router.showSettings()
- }, label: {
- HStack {
- Text(ProfileLocalization.settingsVideo)
- .font(Theme.Fonts.titleMedium)
- Spacer()
- Image(systemName: "chevron.right")
- }
- })
- .accessibilityIdentifier("video_settings_button")
-
- }
- .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.trackLogoutClickedClicked()
- viewModel.router.presentView(
- transitionStyle: .crossDissolve,
- animated: true
- ) {
- 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")
- }
- })
+ Text(bio)
+ .font(Theme.Fonts.bodyMedium)
+ .foregroundColor(Theme.Colors.textPrimary)
+ .accessibilityIdentifier("bio_text")
+ }
.accessibilityElement(children: .ignore)
- .accessibilityLabel(ProfileLocalization.logout)
- .accessibilityIdentifier("logout_button")
+ .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
+ )
}
- .foregroundColor(Theme.Colors.alert)
- .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground,
- strokeColor: .clear)
- .padding(.top, 24)
- .padding(.bottom, 60)
}
}
@@ -270,19 +179,18 @@ struct ProfileView_Previews: PreviewProvider {
let router = ProfileRouterMock()
let vm = ProfileViewModel(
interactor: ProfileInteractor.mock,
- downloadManager: DownloadManagerMock(),
router: router,
analytics: ProfileAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity()
)
-
- ProfileView(viewModel: vm, settingsTapped: .constant(false))
+
+ ProfileView(viewModel: vm)
.preferredColorScheme(.light)
.previewDisplayName("DiscoveryView Light")
.loadFonts()
-
- ProfileView(viewModel: vm, settingsTapped: .constant(false))
+
+ ProfileView(viewModel: vm)
.preferredColorScheme(.dark)
.previewDisplayName("DiscoveryView Dark")
.loadFonts()
@@ -291,10 +199,8 @@ 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
@@ -303,25 +209,21 @@ struct UserAvatar: View {
}
self._image = image
}
-
var body: some View {
ZStack {
- Circle()
- .foregroundColor(Theme.Colors.avatarStroke)
- .frame(width: 104, height: 104)
if let image {
Image(uiImage: image)
.resizable()
.scaledToFill()
- .frame(width: 100, height: 100)
- .cornerRadius(50)
+ .frame(width: 80, height: 80)
+ .cornerRadius(40)
} else {
KFImage(url)
.onFailureImage(CoreAssets.noCourseImage.image)
.resizable()
.scaledToFill()
- .frame(width: 100, height: 100)
- .cornerRadius(50)
+ .frame(width: 80, height: 80)
+ .cornerRadius(40)
}
}
}
diff --git a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift
index d509224a6..39854471e 100644
--- a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift
+++ b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift
@@ -22,79 +22,28 @@ 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: ConfigProtocol
let connectivity: ConnectivityProtocol
private let interactor: ProfileInteractorProtocol
- private let downloadManager: DownloadManagerProtocol
private let analytics: ProfileAnalytics
public init(
interactor: ProfileInteractorProtocol,
- downloadManager: DownloadManagerProtocol,
router: ProfileRouter,
analytics: ProfileAnalytics,
config: ConfigProtocol,
connectivity: ConnectivityProtocol
) {
self.interactor = interactor
- self.downloadManager = downloadManager
self.router = router
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? {
- let osVersion = UIDevice.current.systemVersion
- let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
- let deviceModel = UIDevice.current.model
- let feedbackDetails = "OS version: \(osVersion)\nApp version: \(appVersion)\nDevice model: \(deviceModel)"
-
- let recipientAddress = config.feedbackEmail
- let emailSubject = "Feedback"
- let emailBody = "\n\n\(feedbackDetails)\n".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
- let emailURL = URL(string: "mailto:\(recipientAddress)?subject=\(emailSubject)&body=\(emailBody)")
- return emailURL
- }
-
+
@MainActor
public func getMyProfile(withProgress: Bool = true) async {
do {
@@ -110,9 +59,7 @@ public class ProfileViewModel: ObservableObject {
isShowProgress = false
} catch let error {
isShowProgress = false
- if error.isUpdateRequeiredError {
- self.versionState = .updateRequired
- } else if error.isInternetError {
+ if error.isInternetError {
errorMessage = CoreLocalization.Error.slowOrNoInternetConnection
} else {
errorMessage = CoreLocalization.Error.unknownError
@@ -120,47 +67,7 @@ public class ProfileViewModel: ObservableObject {
}
}
- @MainActor
- func logOut() async {
- try? await interactor.logOut()
- try? await downloadManager.cancelAllDownloading()
- router.showStartupScreen()
- analytics.userLogout(force: false)
- }
-
- func trackProfileVideoSettingsClicked() {
- analytics.profileVideoSettingsClicked()
- }
-
- func trackEmailSupportClicked() {
- analytics.emailSupportClicked()
- }
-
- func trackCookiePolicyClicked() {
- analytics.cookiePolicyClicked()
- }
-
- func trackTOSClicked() {
- analytics.tosClicked()
- }
-
- func trackFAQClicked() {
- analytics.faqClicked()
- }
-
- func trackDataSellClicked() {
- analytics.dataSellClicked()
- }
-
- func trackPrivacyPolicyClicked() {
- analytics.privacyPolicyClicked()
- }
-
func trackProfileEditClicked() {
analytics.profileEditClicked()
}
-
- func trackLogoutClickedClicked() {
- analytics.profileEvent(.userLogoutClicked, biValue: .userLogoutClicked)
- }
}
diff --git a/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift
index 1b8c2ae63..5b8f74713 100644
--- a/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift
+++ b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift
@@ -20,7 +20,7 @@ struct ProfileSupportInfoView: View {
let title: String
}
- @ObservedObject var viewModel: ProfileViewModel
+ @ObservedObject var viewModel: SettingsViewModel
var body: some View {
Text(ProfileLocalization.supportInfo)
@@ -28,6 +28,7 @@ struct ProfileSupportInfoView: View {
.font(Theme.Fonts.labelLarge)
.foregroundColor(Theme.Colors.textSecondary)
.accessibilityIdentifier("support_info_text")
+ .padding(.top, 12)
VStack(alignment: .leading, spacing: 24) {
viewModel.contactSupport().map(supportInfo)
diff --git a/Profile/Profile/Presentation/ProfileRouter.swift b/Profile/Profile/Presentation/ProfileRouter.swift
index 8d9539e92..624a05e21 100644
--- a/Profile/Profile/Presentation/ProfileRouter.swift
+++ b/Profile/Profile/Presentation/ProfileRouter.swift
@@ -20,6 +20,10 @@ public protocol ProfileRouter: BaseRouter {
func showSettings()
+ func showVideoSettings()
+
+ func showManageAccount()
+
func showVideoQualityView(viewModel: SettingsViewModel)
func showVideoDownloadQualityView(
@@ -46,6 +50,10 @@ public class ProfileRouterMock: BaseRouterMock, ProfileRouter {
public func showSettings() {}
+ public func showVideoSettings() {}
+
+ public func showManageAccount() {}
+
public func showVideoQualityView(viewModel: SettingsViewModel) {}
public func showVideoDownloadQualityView(
diff --git a/Profile/Profile/Presentation/Settings/ManageAccountView.swift b/Profile/Profile/Presentation/Settings/ManageAccountView.swift
new file mode 100644
index 000000000..f4a38ab34
--- /dev/null
+++ b/Profile/Profile/Presentation/Settings/ManageAccountView.swift
@@ -0,0 +1,214 @@
+//
+// ManageAccountView.swift
+// Profile
+//
+// Created by Stepanok Ivan on 10.04.2024.
+//
+
+import SwiftUI
+import Core
+import Theme
+
+public struct ManageAccountView: View {
+
+ @ObservedObject
+ private var viewModel: ManageAccountViewModel
+
+ @Environment (\.isHorizontal) private var isHorizontal
+
+ public init(viewModel: ManageAccountViewModel) {
+ self.viewModel = viewModel
+ }
+
+ public var body: some View {
+ GeometryReader { proxy in
+ ZStack(alignment: .top) {
+ VStack {
+ ThemeAssets.headerBackground.swiftUIImage
+ .resizable()
+ .edgesIgnoringSafeArea(.top)
+ }
+ .frame(maxWidth: .infinity, maxHeight: 200)
+ .accessibilityIdentifier("auth_bg_image")
+
+ // MARK: - Page name
+ VStack(alignment: .center) {
+ ZStack {
+ HStack {
+ Text(ProfileLocalization.manageAccount)
+ .titleSettings(color: Theme.Colors.loginNavigationText)
+ .accessibilityIdentifier("manage_account_text")
+ }
+ VStack {
+ BackNavigationButton(
+ color: Theme.Colors.loginNavigationText,
+ action: {
+ viewModel.router.back()
+ }
+ )
+ .backViewStyle()
+ .padding(.leading, isHorizontal ? 48 : 0)
+ .accessibilityIdentifier("back_button")
+
+ }.frame(minWidth: 0,
+ maxWidth: .infinity,
+ alignment: .topLeading)
+ }
+
+ // MARK: - Page Body
+ RefreshableScrollViewCompat(
+ action: {
+ await viewModel.getMyProfile(withProgress: false)
+ },
+ content: {
+ VStack(alignment: .leading, spacing: 12) {
+ if viewModel.isShowProgress {
+ ProgressBar(size: 40, lineWidth: 8)
+ .padding(.top, 200)
+ .padding(.horizontal)
+ .accessibilityIdentifier("progress_bar")
+ } else {
+ userAvatar
+ editProfileButton
+ deleteAccount
+ }
+ }
+ })
+ .frameLimit(width: proxy.size.width)
+ .padding(.top, 24)
+ .padding(.horizontal, isHorizontal ? 24 : 0)
+ .roundedBackground(Theme.Colors.background)
+ }
+ .navigationBarHidden(true)
+ .navigationBarBackButtonHidden(true)
+ .navigationTitle(ProfileLocalization.manageAccount)
+
+ // 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)
+ }
+ .transition(.move(edge: .bottom))
+ .onAppear {
+ doAfter(Theme.Timeout.snackbarMessageLongTimeout) {
+ viewModel.errorMessage = nil
+ }
+ }
+ }
+ }
+ }
+ .background(
+ Theme.Colors.background
+ .ignoresSafeArea()
+ )
+ .ignoresSafeArea(.all, edges: .horizontal)
+ .onFirstAppear {
+ Task {
+ await viewModel.getMyProfile()
+ }
+ }
+ }
+
+ private var userAvatar: some View {
+ HStack(alignment: .center, spacing: 12) {
+ UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar)
+ .accessibilityIdentifier("user_avatar_image")
+ VStack(alignment: .leading, spacing: 4) {
+ Text(viewModel.userModel?.name ?? "")
+ .font(Theme.Fonts.headlineSmall)
+ .foregroundColor(Theme.Colors.textPrimary)
+ .accessibilityIdentifier("user_name_text")
+ Text("\(viewModel.userModel?.email ?? "")")
+ .font(Theme.Fonts.labelLarge)
+ .foregroundColor(Theme.Colors.textSecondary)
+ .accessibilityIdentifier("user_username_text")
+ }
+ Spacer()
+ }.padding(.all, 24)
+ .frame(
+ minWidth: 0,
+ maxWidth: .infinity,
+ alignment: .center
+ )
+ }
+
+ private var deleteAccount: some View {
+ Button(action: {
+ viewModel.trackProfileDeleteAccountClicked()
+ viewModel.router.showDeleteProfileView()
+ }, label: {
+ HStack {
+ CoreAssets.deleteAccount.swiftUIImage
+ Text(ProfileLocalization.Edit.deleteAccount)
+ }
+ })
+ .frame(
+ minWidth: 0,
+ maxWidth: .infinity,
+ alignment: .center
+ )
+ .font(Theme.Fonts.labelLarge)
+ .foregroundColor(Theme.Colors.alert)
+ .padding(.top, 12)
+ .accessibilityIdentifier("delete_account_button")
+ }
+
+ private var editProfileButton: some View {
+ HStack(alignment: .center) {
+ StyledButton(
+ ProfileLocalization.editProfile,
+ action: {
+ 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
+ }
+ if let updatedImage {
+ self.viewModel.updatedAvatar = updatedImage
+ }
+ }
+ )
+ },
+ color: .clear,
+ textColor: Theme.Colors.accentColor,
+ borderColor: Theme.Colors.accentColor
+ ).padding(.horizontal, 24)
+ }
+ .frame(
+ minWidth: 0,
+ maxWidth: .infinity,
+ alignment: .center
+ )
+ }
+}
+
+#if DEBUG
+struct ManageAccountView_Previews: PreviewProvider {
+ static var previews: some View {
+ let router = ProfileRouterMock()
+ let vm = ManageAccountViewModel(
+ router: router,
+ analytics: ProfileAnalyticsMock(),
+ config: ConfigMock(),
+ connectivity: Connectivity(),
+ interactor: ProfileInteractor.mock
+ )
+
+ ManageAccountView(viewModel: vm)
+ .loadFonts()
+ }
+}
+#endif
diff --git a/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift b/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift
new file mode 100644
index 000000000..55014a340
--- /dev/null
+++ b/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift
@@ -0,0 +1,76 @@
+//
+// ManageAccountViewModel.swift
+// Profile
+//
+// Created by Stepanok Ivan on 10.04.2024.
+//
+
+import Foundation
+import Core
+import SwiftUI
+
+public class ManageAccountViewModel: ObservableObject {
+
+ @Published public var userModel: UserProfile?
+ @Published public var updatedAvatar: UIImage?
+ @Published private(set) var isShowProgress = false
+ @Published var showError: Bool = false
+ var errorMessage: String? {
+ didSet {
+ withAnimation {
+ showError = errorMessage != nil
+ }
+ }
+ }
+
+ let router: ProfileRouter
+ let analytics: ProfileAnalytics
+ let config: ConfigProtocol
+ let connectivity: ConnectivityProtocol
+ private let interactor: ProfileInteractorProtocol
+
+ public init(
+ router: ProfileRouter,
+ analytics: ProfileAnalytics,
+ config: ConfigProtocol,
+ connectivity: ConnectivityProtocol,
+ interactor: ProfileInteractorProtocol
+ ) {
+ self.router = router
+ self.analytics = analytics
+ self.config = config
+ self.connectivity = connectivity
+ self.interactor = interactor
+ }
+
+ @MainActor
+ public func getMyProfile(withProgress: Bool = true) async {
+ do {
+ let userModel = interactor.getMyProfileOffline()
+ if userModel == nil && connectivity.isInternetAvaliable {
+ isShowProgress = withProgress
+ } else {
+ self.userModel = userModel
+ }
+ if connectivity.isInternetAvaliable {
+ self.userModel = try await interactor.getMyProfile()
+ }
+ isShowProgress = false
+ } catch let error {
+ isShowProgress = false
+ if error.isInternetError {
+ errorMessage = CoreLocalization.Error.slowOrNoInternetConnection
+ } else {
+ errorMessage = CoreLocalization.Error.unknownError
+ }
+ }
+ }
+
+ func trackProfileDeleteAccountClicked() {
+ analytics.profileDeleteAccountClicked()
+ }
+
+ func trackProfileEditClicked() {
+ analytics.profileEditClicked()
+ }
+}
diff --git a/Profile/Profile/Presentation/Settings/SettingsView.swift b/Profile/Profile/Presentation/Settings/SettingsView.swift
index 20b6f1e1e..047e238ad 100644
--- a/Profile/Profile/Presentation/Settings/SettingsView.swift
+++ b/Profile/Profile/Presentation/Settings/SettingsView.swift
@@ -15,6 +15,8 @@ public struct SettingsView: View {
@ObservedObject
private var viewModel: SettingsViewModel
+ @Environment (\.isHorizontal) private var isHorizontal
+
public init(viewModel: SettingsViewModel) {
self.viewModel = viewModel
}
@@ -22,79 +24,67 @@ public struct SettingsView: View {
public var body: some View {
GeometryReader { proxy in
ZStack(alignment: .top) {
+ VStack {
+ ThemeAssets.headerBackground.swiftUIImage
+ .resizable()
+ .edgesIgnoringSafeArea(.top)
+ }
+ .frame(maxWidth: .infinity, maxHeight: 200)
+ .accessibilityIdentifier("auth_bg_image")
- // MARK: - Page Body
- ScrollView {
- VStack(alignment: .leading, spacing: 24) {
- if viewModel.isShowProgress {
- ProgressBar(size: 40, lineWidth: 8)
- .padding(.top, 200)
- .padding(.horizontal)
- .accessibilityIdentifier("progressbar")
- } else {
- // MARK: Wi-fi
- HStack {
- SettingsCell(
- title: ProfileLocalization.Settings.wifiTitle,
- description: ProfileLocalization.Settings.wifiDescription
- )
- Toggle(isOn: $viewModel.wifiOnly, label: {})
- .toggleStyle(SwitchToggleStyle(tint: Theme.Colors.toggleSwitchColor))
- .frame(width: 50)
- .accessibilityIdentifier("download_agreement_switch")
- }.foregroundColor(Theme.Colors.textPrimary)
- Divider()
-
- // MARK: Streaming Quality
- HStack {
- Button(action: {
- viewModel.router.showVideoQualityView(viewModel: viewModel)
- }, label: {
- SettingsCell(title: ProfileLocalization.Settings.videoQualityTitle,
- description: viewModel.selectedQuality.settingsDescription())
- })
- .accessibilityIdentifier("video_stream_quality_button")
- // Spacer()
- Image(systemName: "chevron.right")
- .padding(.trailing, 12)
- .frame(width: 10)
- .accessibilityIdentifier("video_stream_quality_image")
- }
- Divider()
-
- // MARK: Download Quality
- HStack {
- Button {
- viewModel.router.showVideoDownloadQualityView(
- downloadQuality: viewModel.userSettings.downloadQuality,
- didSelect: viewModel.update(downloadQuality:),
- analytics: viewModel.analytics
- )
- } label: {
- SettingsCell(
- title: CoreLocalization.Settings.videoDownloadQualityTitle,
- description: viewModel.userSettings.downloadQuality.settingsDescription
- )
+ // MARK: - Page name
+ VStack(alignment: .center) {
+ ZStack {
+ HStack {
+ Text(ProfileLocalization.settings)
+ .titleSettings(color: Theme.Colors.loginNavigationText)
+ .accessibilityIdentifier("register_text")
+ }
+ VStack {
+ BackNavigationButton(
+ color: Theme.Colors.loginNavigationText,
+ action: {
+ viewModel.router.back()
}
- .accessibilityIdentifier("video_download_quality_button")
- // Spacer()
- Image(systemName: "chevron.right")
- .padding(.trailing, 12)
- .frame(width: 10)
- .accessibilityIdentifier("video_download_quality_image")
+ )
+ .backViewStyle()
+ .padding(.leading, isHorizontal ? 48 : 0)
+ .accessibilityIdentifier("back_button")
+
+ }.frame(minWidth: 0,
+ maxWidth: .infinity,
+ alignment: .topLeading)
+ }
+
+ // MARK: - Page Body
+ ScrollView {
+ VStack(alignment: .leading, spacing: 12) {
+ if viewModel.isShowProgress {
+ ProgressBar(size: 40, lineWidth: 8)
+ .padding(.top, 200)
+ .padding(.horizontal)
+ .accessibilityIdentifier("progress_bar")
+ } else {
+ manageAccount
+ settings
+ ProfileSupportInfoView(viewModel: viewModel)
+ logOutButton
}
- Divider()
}
+ .frame(
+ minWidth: 0,
+ maxWidth: .infinity,
+ alignment: .topLeading
+ )
+ .frameLimit(width: proxy.size.width)
+ .padding(.top, 24)
+ .padding(.horizontal, isHorizontal ? 24 : 0)
}
- .frame(
- minWidth: 0,
- maxWidth: .infinity,
- alignment: .topLeading
- )
- .padding(.horizontal, 24)
- .frameLimit(width: proxy.size.width)
+ .roundedBackground(Theme.Colors.background)
}
- .padding(.top, 8)
+ .navigationBarHidden(true)
+ .navigationBarBackButtonHidden(true)
+ .navigationTitle(ProfileLocalization.settings)
// MARK: - Error Alert
if viewModel.showError {
@@ -110,14 +100,114 @@ public struct SettingsView: View {
}
}
}
- .navigationBarHidden(false)
- .navigationBarBackButtonHidden(false)
- .navigationTitle(ProfileLocalization.Settings.videoSettingsTitle)
- .background(
- Theme.Colors.background
- .ignoresSafeArea()
- )
}
+ .background(
+ Theme.Colors.background
+ .ignoresSafeArea()
+ )
+ .ignoresSafeArea(.all, edges: .horizontal)
+ }
+
+ // MARK: - Manage Account
+ @ViewBuilder
+ private var manageAccount: some View {
+ VStack(alignment: .leading, spacing: 27) {
+ Button(action: {
+ viewModel.trackProfileVideoSettingsClicked()
+ viewModel.router.showManageAccount()
+ }, label: {
+ HStack {
+ Text(ProfileLocalization.manageAccount)
+ .font(Theme.Fonts.titleMedium)
+ Spacer()
+ Image(systemName: "chevron.right")
+ }
+ })
+ .accessibilityIdentifier("video_settings_button")
+ }
+ .accessibilityElement(children: .ignore)
+ .accessibilityLabel(ProfileLocalization.settingsVideo)
+ .cardStyle(
+ bgColor: Theme.Colors.textInputUnfocusedBackground,
+ strokeColor: .clear
+ )
+ }
+
+ // MARK: - Settings
+
+ @ViewBuilder
+ private var settings: some View {
+ Text(ProfileLocalization.settings)
+ .padding(.horizontal, 24)
+ .font(Theme.Fonts.labelLarge)
+ .foregroundColor(Theme.Colors.textSecondary)
+ .accessibilityIdentifier("settings_text")
+ .padding(.top, 12)
+
+ VStack(alignment: .leading, spacing: 27) {
+ Button(action: {
+ viewModel.trackProfileVideoSettingsClicked()
+ viewModel.router.showVideoSettings()
+ }, label: {
+ HStack {
+ Text(ProfileLocalization.settingsVideo)
+ .font(Theme.Fonts.titleMedium)
+ Spacer()
+ Image(systemName: "chevron.right")
+ }
+ })
+ .accessibilityIdentifier("video_settings_button")
+
+ }
+ .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.trackLogoutClickedClicked()
+ viewModel.router.presentView(
+ transitionStyle: .crossDissolve,
+ animated: true
+ ) {
+ 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)
+ .accessibilityIdentifier("logout_button")
+ }
+ .foregroundColor(Theme.Colors.alert)
+ .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, strokeColor: .clear)
+ .padding(.top, 24)
+ .padding(.bottom, 60)
}
}
@@ -127,8 +217,11 @@ struct SettingsView_Previews: PreviewProvider {
let router = ProfileRouterMock()
let vm = SettingsViewModel(
interactor: ProfileInteractor.mock,
+ downloadManager: DownloadManagerMock(),
router: router,
- analytics: CoreAnalyticsMock()
+ analytics: ProfileAnalyticsMock(),
+ coreAnalytics: CoreAnalyticsMock(),
+ config: ConfigMock()
)
SettingsView(viewModel: vm)
diff --git a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift
index 499623a89..e31e09eea 100644
--- a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift
+++ b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift
@@ -8,6 +8,7 @@
import Foundation
import Core
import SwiftUI
+import Combine
public class SettingsViewModel: ObservableObject {
@@ -40,6 +41,16 @@ public class SettingsViewModel: ObservableObject {
]
.enumerated()
)
+
+ enum VersionState {
+ case actual
+ case updateNeeded
+ case updateRequired
+ }
+
+ @Published var versionState: VersionState = .actual
+ @Published var currentVersion: String = ""
+ @Published var latestVersion: String = ""
var errorMessage: String? {
didSet {
@@ -50,26 +61,122 @@ public class SettingsViewModel: ObservableObject {
}
@Published private(set) var userSettings: UserSettings
+
+ private var cancellables = Set()
private let interactor: ProfileInteractorProtocol
+ private let downloadManager: DownloadManagerProtocol
let router: ProfileRouter
- let analytics: CoreAnalytics
+ let analytics: ProfileAnalytics
+ let coreAnalytics: CoreAnalytics
+ let config: ConfigProtocol
- public init(interactor: ProfileInteractorProtocol, router: ProfileRouter, analytics: CoreAnalytics) {
+ public init(
+ interactor: ProfileInteractorProtocol,
+ downloadManager: DownloadManagerProtocol,
+ router: ProfileRouter,
+ analytics: ProfileAnalytics,
+ coreAnalytics: CoreAnalytics,
+ config: ConfigProtocol
+ ) {
self.interactor = interactor
+ self.downloadManager = downloadManager
self.router = router
self.analytics = analytics
+ self.coreAnalytics = coreAnalytics
+ self.config = config
let userSettings = interactor.getSettings()
self.userSettings = userSettings
self.wifiOnly = userSettings.wifiOnly
self.selectedQuality = userSettings.streamingQuality
+ generateVersionState()
+ }
+
+ 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? {
+ let osVersion = UIDevice.current.systemVersion
+ let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
+ let deviceModel = UIDevice.current.model
+ let feedbackDetails = "OS version: \(osVersion)\nApp version: \(appVersion)\nDevice model: \(deviceModel)"
+
+ let recipientAddress = config.feedbackEmail
+ let emailSubject = "Feedback"
+ let emailBody = "\n\n\(feedbackDetails)\n".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
+ let emailURL = URL(string: "mailto:\(recipientAddress)?subject=\(emailSubject)&body=\(emailBody)")
+ return emailURL
}
func update(downloadQuality: DownloadQuality) {
self.userSettings.downloadQuality = downloadQuality
interactor.saveSettings(userSettings)
}
+
+ func openAppStore() {
+ guard let appStoreURL = URL(string: config.appStoreLink) else { return }
+ UIApplication.shared.open(appStoreURL)
+ }
+
+ @MainActor
+ func logOut() async {
+ try? await interactor.logOut()
+ try? await downloadManager.cancelAllDownloading()
+ router.showStartupScreen()
+ analytics.userLogout(force: false)
+ }
+
+ func trackProfileVideoSettingsClicked() {
+ analytics.profileVideoSettingsClicked()
+ }
+
+ func trackEmailSupportClicked() {
+ analytics.emailSupportClicked()
+ }
+
+ func trackCookiePolicyClicked() {
+ analytics.cookiePolicyClicked()
+ }
+
+ func trackTOSClicked() {
+ analytics.tosClicked()
+ }
+
+ func trackFAQClicked() {
+ analytics.faqClicked()
+ }
+
+ func trackDataSellClicked() {
+ analytics.dataSellClicked()
+ }
+
+ func trackPrivacyPolicyClicked() {
+ analytics.privacyPolicyClicked()
+ }
+
+ func trackProfileEditClicked() {
+ analytics.profileEditClicked()
+ }
+
+ func trackLogoutClickedClicked() {
+ analytics.profileEvent(.userLogoutClicked, biValue: .userLogoutClicked)
+ }
+
}
public extension StreamingQuality {
diff --git a/Profile/Profile/Presentation/Settings/VideoQualityView.swift b/Profile/Profile/Presentation/Settings/VideoQualityView.swift
index b3decab29..a52565c19 100644
--- a/Profile/Profile/Presentation/Settings/VideoQualityView.swift
+++ b/Profile/Profile/Presentation/Settings/VideoQualityView.swift
@@ -14,6 +14,7 @@ public struct VideoQualityView: View {
@ObservedObject
private var viewModel: SettingsViewModel
+ @Environment (\.isHorizontal) private var isHorizontal
public init(viewModel: SettingsViewModel) {
self.viewModel = viewModel
@@ -22,72 +23,102 @@ public struct VideoQualityView: View {
public var body: some View {
GeometryReader { proxy in
ZStack(alignment: .top) {
- // MARK: - Page Body
- ScrollView {
- VStack(alignment: .leading, spacing: 24) {
- if viewModel.isShowProgress {
- ProgressBar(size: 40, lineWidth: 8)
- .padding(.top, 200)
- .padding(.horizontal)
- .accessibilityIdentifier("progressbar")
- } else {
+ VStack {
+ ThemeAssets.headerBackground.swiftUIImage
+ .resizable()
+ .edgesIgnoringSafeArea(.top)
+ }
+ .frame(maxWidth: .infinity, maxHeight: 200)
+ .accessibilityIdentifier("auth_bg_image")
+
+ // MARK: - Page name
+ VStack(alignment: .center) {
+ ZStack {
+ HStack {
+ Text(ProfileLocalization.Settings.videoQualityTitle)
+ .titleSettings(color: Theme.Colors.loginNavigationText)
+ .accessibilityIdentifier("manage_account_text")
+ }
+ VStack {
+ BackNavigationButton(
+ color: Theme.Colors.loginNavigationText,
+ action: {
+ viewModel.router.back()
+ }
+ )
+ .backViewStyle()
+ .padding(.leading, isHorizontal ? 48 : 0)
+ .accessibilityIdentifier("back_button")
- ForEach(viewModel.quality, id: \.offset) { _, quality in
- Button(action: {
- viewModel.analytics.videoQualityChanged(
- .videoStreamQualityChanged,
- bivalue: .videoStreamQualityChanged,
- value: quality.value ?? "",
- oldValue: viewModel.selectedQuality.value ?? ""
- )
- viewModel.selectedQuality = quality
- }, label: {
- HStack {
- SettingsCell(
- title: quality.title(),
- description: quality.description()
+ }.frame(minWidth: 0,
+ maxWidth: .infinity,
+ alignment: .topLeading)
+ }
+ // MARK: - Page Body
+ ScrollView {
+ VStack(alignment: .leading, spacing: 24) {
+ if viewModel.isShowProgress {
+ ProgressBar(size: 40, lineWidth: 8)
+ .padding(.top, 200)
+ .padding(.horizontal)
+ .accessibilityIdentifier("progress_bar")
+ } else {
+ ForEach(viewModel.quality, id: \.offset) { _, quality in
+ Button(action: {
+ viewModel.coreAnalytics.videoQualityChanged(
+ .videoStreamQualityChanged,
+ bivalue: .videoStreamQualityChanged,
+ value: quality.value ?? "",
+ oldValue: viewModel.selectedQuality.value ?? ""
)
- Spacer()
- CoreAssets.checkmark.swiftUIImage
- .renderingMode(.template)
- .foregroundColor(Theme.Colors.accentXColor)
- .opacity(quality == viewModel.selectedQuality ? 1 : 0)
- }.foregroundColor(Theme.Colors.textPrimary)
- })
- .accessibilityIdentifier("select_quality_button")
- Divider()
+ viewModel.selectedQuality = quality
+ }, label: {
+ HStack {
+ SettingsCell(
+ title: quality.title(),
+ description: quality.description()
+ )
+ Spacer()
+ CoreAssets.checkmark.swiftUIImage
+ .renderingMode(.template)
+ .foregroundColor(Theme.Colors.accentXColor)
+ .opacity(quality == viewModel.selectedQuality ? 1 : 0)
+ }.foregroundColor(Theme.Colors.textPrimary)
+ })
+ .accessibilityIdentifier("select_quality_button")
+ Divider()
+ }
}
- }
- }.frame(minWidth: 0,
- maxWidth: .infinity,
- alignment: .topLeading)
- .padding(.horizontal, 24)
- .frameLimit(width: proxy.size.width)
- }
- .padding(.top, 8)
-
- // MARK: - Error Alert
- if viewModel.showError {
- VStack {
- Spacer()
- SnackBarView(message: viewModel.errorMessage)
+ }.frameLimit(width: proxy.size.width)
+ .padding(.horizontal, 24)
+ .padding(.top, 24)
}
- .transition(.move(edge: .bottom))
- .onAppear {
- doAfter(Theme.Timeout.snackbarMessageLongTimeout) {
- viewModel.errorMessage = nil
+ .roundedBackground(Theme.Colors.background)
+
+ // MARK: - Error Alert
+ if viewModel.showError {
+ VStack {
+ Spacer()
+ SnackBarView(message: viewModel.errorMessage)
+ }
+ .transition(.move(edge: .bottom))
+ .onAppear {
+ doAfter(Theme.Timeout.snackbarMessageLongTimeout) {
+ viewModel.errorMessage = nil
+ }
}
}
}
}
- .navigationBarHidden(false)
- .navigationBarBackButtonHidden(false)
- .navigationTitle(ProfileLocalization.Settings.videoQualityTitle)
- .background(
- Theme.Colors.background
- .ignoresSafeArea()
- )
}
+ .navigationBarHidden(true)
+ .navigationBarBackButtonHidden(true)
+ .navigationTitle(ProfileLocalization.Settings.videoQualityTitle)
+ .ignoresSafeArea(.all, edges: .horizontal)
+ .background(
+ Theme.Colors.background
+ .ignoresSafeArea()
+ )
}
}
@@ -97,8 +128,11 @@ struct VideoQualityView_Previews: PreviewProvider {
let router = ProfileRouterMock()
let vm = SettingsViewModel(
interactor: ProfileInteractor.mock,
+ downloadManager: DownloadManagerMock(),
router: router,
- analytics: CoreAnalyticsMock()
+ analytics: ProfileAnalyticsMock(),
+ coreAnalytics: CoreAnalyticsMock(),
+ config: ConfigMock()
)
VideoQualityView(viewModel: vm)
diff --git a/Profile/Profile/Presentation/Settings/VideoSettingsView.swift b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift
new file mode 100644
index 000000000..98e14ebb2
--- /dev/null
+++ b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift
@@ -0,0 +1,148 @@
+//
+// VideoSettingsView.swift
+// Profile
+//
+// Created by Stepanok Ivan on 09.04.2024.
+//
+
+import SwiftUI
+import Core
+import Theme
+
+public struct VideoSettingsView: View {
+
+ @ObservedObject
+ private var viewModel: SettingsViewModel
+ @Environment (\.isHorizontal) private var isHorizontal
+
+ public init(viewModel: SettingsViewModel) {
+ self.viewModel = viewModel
+ }
+
+ public var body: some View {
+ GeometryReader { proxy in
+ ZStack(alignment: .top) {
+ VStack {
+ ThemeAssets.headerBackground.swiftUIImage
+ .resizable()
+ .edgesIgnoringSafeArea(.top)
+ }
+ .frame(maxWidth: .infinity, maxHeight: 200)
+ .accessibilityIdentifier("auth_bg_image")
+
+ // MARK: - Page name
+ VStack(alignment: .center) {
+ ZStack {
+ HStack {
+ Text(ProfileLocalization.Settings.videoSettingsTitle)
+ .titleSettings(color: Theme.Colors.loginNavigationText)
+ .accessibilityIdentifier("manage_account_text")
+ }
+ VStack {
+ BackNavigationButton(
+ color: Theme.Colors.loginNavigationText,
+ action: {
+ viewModel.router.back()
+ }
+ )
+ .backViewStyle()
+ .padding(.leading, isHorizontal ? 48 : 0)
+ .accessibilityIdentifier("back_button")
+
+ }.frame(minWidth: 0,
+ maxWidth: .infinity,
+ alignment: .topLeading)
+ }
+ // MARK: - Page Body
+ ScrollView {
+ VStack(alignment: .leading, spacing: 24) {
+ // MARK: Wi-fi
+ HStack {
+ SettingsCell(
+ title: ProfileLocalization.Settings.wifiTitle,
+ description: ProfileLocalization.Settings.wifiDescription
+ )
+ Toggle(isOn: $viewModel.wifiOnly, label: {})
+ .toggleStyle(SwitchToggleStyle(tint: Theme.Colors.toggleSwitchColor))
+ .frame(width: 50)
+ .accessibilityIdentifier("download_agreement_switch")
+ }.foregroundColor(Theme.Colors.textPrimary)
+ Divider()
+
+ // MARK: Streaming Quality
+ HStack {
+ Button(action: {
+ viewModel.router.showVideoQualityView(viewModel: viewModel)
+ }, label: {
+ SettingsCell(title: ProfileLocalization.Settings.videoQualityTitle,
+ description: viewModel.selectedQuality.settingsDescription())
+ })
+ .accessibilityIdentifier("video_stream_quality_button")
+ Image(systemName: "chevron.right")
+ .padding(.trailing, 12)
+ .frame(width: 10)
+ .accessibilityIdentifier("video_stream_quality_image")
+ }
+ Divider()
+
+ // MARK: Download Quality
+ HStack {
+ Button {
+ viewModel.router.showVideoDownloadQualityView(
+ downloadQuality: viewModel.userSettings.downloadQuality,
+ didSelect: viewModel.update(downloadQuality:),
+ analytics: viewModel.coreAnalytics
+ )
+ } label: {
+ SettingsCell(
+ title: CoreLocalization.Settings.videoDownloadQualityTitle,
+ description: viewModel.userSettings.downloadQuality.settingsDescription
+ )
+ }
+ .accessibilityIdentifier("video_download_quality_button")
+ Image(systemName: "chevron.right")
+ .padding(.trailing, 12)
+ .frame(width: 10)
+ .accessibilityIdentifier("video_download_quality_image")
+ }
+ Divider()
+ }
+ .frameLimit(width: proxy.size.width)
+ .padding(.horizontal, 24)
+ .padding(.top, 24)
+ }
+ .roundedBackground(Theme.Colors.background)
+ }
+ }
+ }
+ .navigationBarHidden(true)
+ .navigationBarBackButtonHidden(true)
+ .navigationTitle(ProfileLocalization.Settings.videoSettingsTitle)
+ .ignoresSafeArea(.all, edges: .horizontal)
+ .background(
+ Theme.Colors.background
+ .ignoresSafeArea()
+ )
+ }
+}
+
+#if DEBUG
+struct VideoSettingsView_Previews: PreviewProvider {
+ static var previews: some View {
+ let router = ProfileRouterMock()
+ let vm = SettingsViewModel(
+ interactor: ProfileInteractor.mock,
+ downloadManager: DownloadManagerMock(),
+ router: router,
+ analytics: ProfileAnalyticsMock(),
+ coreAnalytics: CoreAnalyticsMock(),
+ config: ConfigMock()
+ )
+
+ VideoSettingsView(viewModel: vm)
+ .preferredColorScheme(.light)
+ .previewDisplayName("SettingsView Light")
+ .loadFonts()
+ }
+}
+#endif
diff --git a/Profile/Profile/SwiftGen/Strings.swift b/Profile/Profile/SwiftGen/Strings.swift
index d1adacf55..5c26b5557 100644
--- a/Profile/Profile/SwiftGen/Strings.swift
+++ b/Profile/Profile/SwiftGen/Strings.swift
@@ -10,6 +10,8 @@ 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 ProfileLocalization {
+ /// About Me
+ public static let about = ProfileLocalization.tr("Localizable", "ABOUT", fallback: "About Me")
/// Bio:
public static let bio = ProfileLocalization.tr("Localizable", "BIO", fallback: "Bio:")
/// Contact support
@@ -18,8 +20,8 @@ public enum ProfileLocalization {
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")
+ /// 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
@@ -30,6 +32,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")
+ /// Manage Account
+ public static let manageAccount = ProfileLocalization.tr("Localizable", "MANAGE_ACCOUNT", fallback: "Manage Account")
/// Privacy policy
public static let privacy = ProfileLocalization.tr("Localizable", "PRIVACY", fallback: "Privacy policy")
/// Settings
@@ -64,8 +68,8 @@ public enum ProfileLocalization {
public static let password = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.PASSWORD", fallback: "Password")
/// Enter password
public static let passwordDescription = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.PASSWORD_DESCRIPTION", fallback: "Enter password")
- /// Delete account
- public static let title = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.TITLE", fallback: "Delete account")
+ /// Delete Account
+ public static let title = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.TITLE", fallback: "Delete Account")
/// delete your account?
public static let wantToDelete = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.WANT_TO_DELETE", fallback: "delete your account?")
}
@@ -76,8 +80,8 @@ public enum ProfileLocalization {
public static let title = ProfileLocalization.tr("Localizable", "DELETE_ALERT.TITLE", fallback: "Warning!")
}
public enum Edit {
- /// Delete account
- public static let deleteAccount = ProfileLocalization.tr("Localizable", "EDIT.DELETE_ACCOUNT", fallback: "Delete account")
+ /// Delete Account
+ public static let deleteAccount = ProfileLocalization.tr("Localizable", "EDIT.DELETE_ACCOUNT", fallback: "Delete Account")
/// A limited profile only shares your username and profile photo.
public static let limitedProfileDescription = ProfileLocalization.tr("Localizable", "EDIT.LIMITED_PROFILE_DESCRIPTION", fallback: "A limited profile only shares your username and profile photo.")
/// You must be over 13 years old to have a profile with full access to information.
diff --git a/Profile/Profile/en.lproj/Localizable.strings b/Profile/Profile/en.lproj/Localizable.strings
index df2a437f8..be69b026d 100644
--- a/Profile/Profile/en.lproj/Localizable.strings
+++ b/Profile/Profile/en.lproj/Localizable.strings
@@ -8,7 +8,8 @@
"TITLE" = "Profile";
"INFO" = "Profile info";
-"EDIT_PROFILE" = "Edit profile";
+"ABOUT" = "About Me";
+"EDIT_PROFILE" = "Edit Profile";
"YEAR_OF_BIRTH" = "Year of birth:";
"BIO" = "Bio:";
"SETTINGS" = "Settings";
@@ -20,6 +21,7 @@
"COOKIE_POLICY" = "Cookie policy";
"DO_NOT_SELL_INFORMATION" = "Do not sell my personal information";
"FAQ_TITLE" = "View FAQ";
+"MANAGE_ACCOUNT" = "Manage Account";
"LOGOUT" = "Log out";
"SWITCH_TO" = "Switch to";
@@ -37,7 +39,7 @@
"EDIT.TOO_YONG_USER" = "You must be over 13 years old to have a profile with full access to information.";
"EDIT.LIMITED_PROFILE_DESCRIPTION" = "A limited profile only shares your username and profile photo.";
-"EDIT.DELETE_ACCOUNT" = "Delete account";
+"EDIT.DELETE_ACCOUNT" = "Delete Account";
"EDIT.FIELDS.YEAR_OF_BIRTH" = "Year of birth";
"EDIT.FIELDS.LOCATION" = "Location";
@@ -49,7 +51,7 @@
"EDIT.BOTTOM_SHEET.REMOVE" = "Remove photo";
"EDIT.BOTTOM_SHEET.CANCEL" = "Cancel";
-"DELETE_ACCOUNT.TITLE" = "Delete account";
+"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, please enter your account password.";
diff --git a/Profile/Profile/uk.lproj/Localizable.strings b/Profile/Profile/uk.lproj/Localizable.strings
index f0e4d0503..dbc3c5379 100644
--- a/Profile/Profile/uk.lproj/Localizable.strings
+++ b/Profile/Profile/uk.lproj/Localizable.strings
@@ -8,6 +8,7 @@
"TITLE" = "Профіль";
"INFO" = "Дані профілю";
+"ABOUT" = "Про Meне";
"EDIT_PROFILE" = "Редагування";
"YEAR_OF_BIRTH" = "Рік народження:";
"BIO" = "Біо:";
@@ -24,6 +25,7 @@
"SWITCH_TO" = "Переключити на";
"FULL_PROFILE" = "повний профіль";
"LIMITED_PROFILE" = "обмежений профіль";
+"MANAGE_ACCOUNT" = "Налаштування Профілю";
"LOGOUT_ALERT.TITLE" = "Підтвердження виходу";
"LOGOUT_ALERT.TEXT" = "Ви впевнені, що бажаєте вийти?";
diff --git a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift
index 2980fb4a8..50f77a155 100644
--- a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift
+++ b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift
@@ -28,7 +28,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -65,7 +66,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -102,7 +104,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -134,7 +137,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -166,7 +170,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -198,7 +203,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -230,7 +236,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -262,7 +269,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -294,7 +302,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -330,7 +339,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -366,7 +376,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -401,7 +412,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -436,7 +448,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -484,7 +497,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -527,7 +541,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -583,7 +598,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -637,7 +653,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -675,7 +692,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -707,7 +725,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -738,7 +757,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: true
+ isFullProfile: true,
+ email: ""
)
let languages = [
@@ -775,7 +795,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
@@ -806,7 +827,8 @@ final class EditProfileViewModelTests: XCTestCase {
country: "UA",
spokenLanguage: "UA",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
Given(interactor, .getSpokenLanguages(willReturn: []))
diff --git a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift
index 40e56bbfa..44d3b96be 100644
--- a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift
+++ b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift
@@ -30,7 +30,8 @@ final class ProfileViewModelTests: XCTestCase {
yearOfBirth: 2000,
country: "Ua",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
Given(interactor, .getUserProfile(username: .value("Steve"), willReturn: user))
@@ -92,7 +93,6 @@ final class ProfileViewModelTests: XCTestCase {
let connectivity = ConnectivityProtocolMock()
let viewModel = ProfileViewModel(
interactor: interactor,
- downloadManager: DownloadManagerMock(),
router: router,
analytics: analytics,
config: ConfigMock(),
@@ -107,7 +107,8 @@ final class ProfileViewModelTests: XCTestCase {
yearOfBirth: 2000,
country: "Ua",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
Given(connectivity, .isInternetAvaliable(getter: true))
@@ -131,7 +132,6 @@ final class ProfileViewModelTests: XCTestCase {
let connectivity = ConnectivityProtocolMock()
let viewModel = ProfileViewModel(
interactor: interactor,
- downloadManager: DownloadManagerMock(),
router: router,
analytics: analytics,
config: ConfigMock(),
@@ -146,7 +146,8 @@ final class ProfileViewModelTests: XCTestCase {
yearOfBirth: 2000,
country: "Ua",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
Given(connectivity, .isInternetAvaliable(getter: false))
@@ -169,7 +170,6 @@ final class ProfileViewModelTests: XCTestCase {
let connectivity = ConnectivityProtocolMock()
let viewModel = ProfileViewModel(
interactor: interactor,
- downloadManager: DownloadManagerMock(),
router: router,
analytics: analytics,
config: ConfigMock(),
@@ -184,7 +184,8 @@ final class ProfileViewModelTests: XCTestCase {
yearOfBirth: 2000,
country: "Ua",
shortBiography: "Bio",
- isFullProfile: false
+ isFullProfile: false,
+ email: ""
)
let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet))
@@ -209,7 +210,6 @@ final class ProfileViewModelTests: XCTestCase {
let connectivity = ConnectivityProtocolMock()
let viewModel = ProfileViewModel(
interactor: interactor,
- downloadManager: DownloadManagerMock(),
router: router,
analytics: analytics,
config: ConfigMock(),
@@ -227,121 +227,4 @@ final class ProfileViewModelTests: XCTestCase {
XCTAssertFalse(viewModel.isShowProgress)
XCTAssertTrue(viewModel.showError)
}
-
- func testLogOutSuccess() async throws {
- let interactor = ProfileInteractorProtocolMock()
- let router = ProfileRouterMock()
- let analytics = ProfileAnalyticsMock()
- let connectivity = ConnectivityProtocolMock()
- let viewModel = ProfileViewModel(
- interactor: interactor,
- downloadManager: DownloadManagerMock(),
- router: router,
- analytics: analytics,
- config: ConfigMock(),
- connectivity: connectivity
- )
-
- Given(connectivity, .isInternetAvaliable(getter: true))
-
- await viewModel.logOut()
-
- Verify(router, .showStartupScreen())
- XCTAssertFalse(viewModel.showError)
- }
-
- func testTrackProfileVideoSettingsClicked() {
- let interactor = ProfileInteractorProtocolMock()
- let router = ProfileRouterMock()
- let analytics = ProfileAnalyticsMock()
- let connectivity = ConnectivityProtocolMock()
- let viewModel = ProfileViewModel(
- interactor: interactor,
- downloadManager: DownloadManagerMock(),
- router: router,
- analytics: analytics,
- config: ConfigMock(),
- connectivity: connectivity
- )
-
- viewModel.trackProfileVideoSettingsClicked()
-
- Verify(analytics, 1, .profileVideoSettingsClicked())
- }
-
- func testTrackEmailSupportClicked() {
- let interactor = ProfileInteractorProtocolMock()
- let router = ProfileRouterMock()
- let analytics = ProfileAnalyticsMock()
- let connectivity = ConnectivityProtocolMock()
- let viewModel = ProfileViewModel(
- interactor: interactor,
- downloadManager: DownloadManagerMock(),
- router: router,
- analytics: analytics,
- config: ConfigMock(),
- connectivity: connectivity
- )
-
- viewModel.trackEmailSupportClicked()
-
- Verify(analytics, 1, .emailSupportClicked())
- }
-
- func testTrackCookiePolicyClicked() {
- let interactor = ProfileInteractorProtocolMock()
- let router = ProfileRouterMock()
- let analytics = ProfileAnalyticsMock()
- let connectivity = ConnectivityProtocolMock()
- let viewModel = ProfileViewModel(
- interactor: interactor,
- downloadManager: DownloadManagerMock(),
- router: router,
- analytics: analytics,
- config: ConfigMock(),
- connectivity: connectivity
- )
-
- viewModel.trackCookiePolicyClicked()
-
- Verify(analytics, 1, .cookiePolicyClicked())
- }
-
- func testTrackPrivacyPolicyClicked() {
- let interactor = ProfileInteractorProtocolMock()
- let router = ProfileRouterMock()
- let analytics = ProfileAnalyticsMock()
- let connectivity = ConnectivityProtocolMock()
- let viewModel = ProfileViewModel(
- interactor: interactor,
- downloadManager: DownloadManagerMock(),
- router: router,
- analytics: analytics,
- config: ConfigMock(),
- connectivity: connectivity
- )
-
- viewModel.trackPrivacyPolicyClicked()
-
- Verify(analytics, 1, .privacyPolicyClicked())
- }
-
- func testTrackProfileEditClicked() {
- let interactor = ProfileInteractorProtocolMock()
- let router = ProfileRouterMock()
- let analytics = ProfileAnalyticsMock()
- let connectivity = ConnectivityProtocolMock()
- let viewModel = ProfileViewModel(
- interactor: interactor,
- downloadManager: DownloadManagerMock(),
- router: router,
- analytics: analytics,
- config: ConfigMock(),
- connectivity: connectivity
- )
-
- viewModel.trackProfileEditClicked()
-
- Verify(analytics, 1, .profileEditClicked())
- }
}
diff --git a/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift b/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift
new file mode 100644
index 000000000..b9c77c6eb
--- /dev/null
+++ b/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift
@@ -0,0 +1,203 @@
+//
+// SettingsViewModelTests.swift
+// ProfileTests
+//
+// Created by Stepanok Ivan on 10.04.2024.
+//
+
+import SwiftyMocky
+import XCTest
+@testable import Core
+@testable import Profile
+import Alamofire
+import SwiftUI
+
+final class SettingsViewModelTests: XCTestCase {
+
+ func testLogOutSuccess() async throws {
+ let interactor = ProfileInteractorProtocolMock()
+ let router = ProfileRouterMock()
+ let analytics = ProfileAnalyticsMock()
+ let coreAnalytics = CoreAnalyticsMock()
+
+ Given(
+ interactor,
+ .getSettings(
+ willReturn: UserSettings(
+ wifiOnly: true,
+ streamingQuality: .auto,
+ downloadQuality: .auto
+ )
+ )
+ )
+
+ let viewModel = SettingsViewModel(
+ interactor: interactor,
+ downloadManager: DownloadManagerMock(),
+ router: router,
+ analytics: analytics,
+ coreAnalytics: coreAnalytics,
+ config: ConfigMock()
+ )
+
+ await viewModel.logOut()
+
+ Verify(router, .showStartupScreen())
+ XCTAssertFalse(viewModel.showError)
+ }
+
+ func testTrackProfileVideoSettingsClicked() {
+ let interactor = ProfileInteractorProtocolMock()
+ let router = ProfileRouterMock()
+ let analytics = ProfileAnalyticsMock()
+ let coreAnalytics = CoreAnalyticsMock()
+
+ Given(
+ interactor,
+ .getSettings(
+ willReturn: UserSettings(
+ wifiOnly: true,
+ streamingQuality: .auto,
+ downloadQuality: .auto
+ )
+ )
+ )
+
+ let viewModel = SettingsViewModel(
+ interactor: interactor,
+ downloadManager: DownloadManagerMock(),
+ router: router,
+ analytics: analytics,
+ coreAnalytics: coreAnalytics,
+ config: ConfigMock()
+ )
+
+ viewModel.trackProfileVideoSettingsClicked()
+
+ Verify(analytics, 1, .profileVideoSettingsClicked())
+ }
+
+ func testTrackEmailSupportClicked() {
+ let interactor = ProfileInteractorProtocolMock()
+ let router = ProfileRouterMock()
+ let analytics = ProfileAnalyticsMock()
+ let coreAnalytics = CoreAnalyticsMock()
+
+ Given(
+ interactor,
+ .getSettings(
+ willReturn: UserSettings(
+ wifiOnly: true,
+ streamingQuality: .auto,
+ downloadQuality: .auto
+ )
+ )
+ )
+
+ let viewModel = SettingsViewModel(
+ interactor: interactor,
+ downloadManager: DownloadManagerMock(),
+ router: router,
+ analytics: analytics,
+ coreAnalytics: coreAnalytics,
+ config: ConfigMock()
+ )
+
+ viewModel.trackEmailSupportClicked()
+
+ Verify(analytics, 1, .emailSupportClicked())
+ }
+
+ func testTrackCookiePolicyClicked() {
+ let interactor = ProfileInteractorProtocolMock()
+ let router = ProfileRouterMock()
+ let analytics = ProfileAnalyticsMock()
+ let coreAnalytics = CoreAnalyticsMock()
+
+ Given(
+ interactor,
+ .getSettings(
+ willReturn: UserSettings(
+ wifiOnly: true,
+ streamingQuality: .auto,
+ downloadQuality: .auto
+ )
+ )
+ )
+
+ let viewModel = SettingsViewModel(
+ interactor: interactor,
+ downloadManager: DownloadManagerMock(),
+ router: router,
+ analytics: analytics,
+ coreAnalytics: coreAnalytics,
+ config: ConfigMock()
+ )
+
+ viewModel.trackCookiePolicyClicked()
+
+ Verify(analytics, 1, .cookiePolicyClicked())
+ }
+
+ func testTrackPrivacyPolicyClicked() {
+ let interactor = ProfileInteractorProtocolMock()
+ let router = ProfileRouterMock()
+ let analytics = ProfileAnalyticsMock()
+ let coreAnalytics = CoreAnalyticsMock()
+
+ Given(
+ interactor,
+ .getSettings(
+ willReturn: UserSettings(
+ wifiOnly: true,
+ streamingQuality: .auto,
+ downloadQuality: .auto
+ )
+ )
+ )
+
+ let viewModel = SettingsViewModel(
+ interactor: interactor,
+ downloadManager: DownloadManagerMock(),
+ router: router,
+ analytics: analytics,
+ coreAnalytics: coreAnalytics,
+ config: ConfigMock()
+ )
+
+ viewModel.trackPrivacyPolicyClicked()
+
+ Verify(analytics, 1, .privacyPolicyClicked())
+ }
+
+ func testTrackProfileEditClicked() {
+ let interactor = ProfileInteractorProtocolMock()
+ let router = ProfileRouterMock()
+ let analytics = ProfileAnalyticsMock()
+ let coreAnalytics = CoreAnalyticsMock()
+
+ Given(
+ interactor,
+ .getSettings(
+ willReturn: UserSettings(
+ wifiOnly: true,
+ streamingQuality: .auto,
+ downloadQuality: .auto
+ )
+ )
+ )
+
+ let viewModel = SettingsViewModel(
+ interactor: interactor,
+ downloadManager: DownloadManagerMock(),
+ router: router,
+ analytics: analytics,
+ coreAnalytics: coreAnalytics,
+ config: ConfigMock()
+ )
+
+ viewModel.trackProfileEditClicked()
+
+ Verify(analytics, 1, .profileEditClicked())
+ }
+}
diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift
index 30ec58bb5..8f927c355 100644
--- a/Profile/ProfileTests/ProfileMock.generated.swift
+++ b/Profile/ProfileTests/ProfileMock.generated.swift
@@ -3045,6 +3045,18 @@ open class ProfileRouterMock: ProfileRouter, Mock {
perform?()
}
+ open func showVideoSettings() {
+ addInvocation(.m_showVideoSettings)
+ let perform = methodPerformValue(.m_showVideoSettings) as? () -> Void
+ perform?()
+ }
+
+ open func showManageAccount() {
+ addInvocation(.m_showManageAccount)
+ let perform = methodPerformValue(.m_showManageAccount) as? () -> Void
+ perform?()
+ }
+
open func showVideoQualityView(viewModel: SettingsViewModel) {
addInvocation(.m_showVideoQualityView__viewModel_viewModel(Parameter.value(`viewModel`)))
let perform = methodPerformValue(.m_showVideoQualityView__viewModel_viewModel(Parameter.value(`viewModel`))) as? (SettingsViewModel) -> Void
@@ -3163,6 +3175,8 @@ open class ProfileRouterMock: ProfileRouter, Mock {
fileprivate enum MethodType {
case m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit(Parameter, Parameter, Parameter<((UserProfile?, UIImage?)) -> Void>)
case m_showSettings
+ case m_showVideoSettings
+ case m_showManageAccount
case m_showVideoQualityView__viewModel_viewModel(Parameter)
case m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelectanalytics_analytics(Parameter, Parameter<((DownloadQuality) -> Void)?>, Parameter)
case m_showDeleteProfileView
@@ -3194,6 +3208,10 @@ open class ProfileRouterMock: ProfileRouter, Mock {
case (.m_showSettings, .m_showSettings): return .match
+ case (.m_showVideoSettings, .m_showVideoSettings): return .match
+
+ case (.m_showManageAccount, .m_showManageAccount): return .match
+
case (.m_showVideoQualityView__viewModel_viewModel(let lhsViewmodel), .m_showVideoQualityView__viewModel_viewModel(let rhsViewmodel)):
var results: [Matcher.ParameterComparisonResult] = []
results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsViewmodel, rhs: rhsViewmodel, with: matcher), lhsViewmodel, rhsViewmodel, "viewModel"))
@@ -3304,6 +3322,8 @@ open class ProfileRouterMock: ProfileRouter, Mock {
switch self {
case let .m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue
case .m_showSettings: return 0
+ case .m_showVideoSettings: return 0
+ case .m_showManageAccount: return 0
case let .m_showVideoQualityView__viewModel_viewModel(p0): return p0.intValue
case let .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelectanalytics_analytics(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue
case .m_showDeleteProfileView: return 0
@@ -3329,6 +3349,8 @@ open class ProfileRouterMock: ProfileRouter, Mock {
switch self {
case .m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit: return ".showEditProfile(userModel:avatar:profileDidEdit:)"
case .m_showSettings: return ".showSettings()"
+ case .m_showVideoSettings: return ".showVideoSettings()"
+ case .m_showManageAccount: return ".showManageAccount()"
case .m_showVideoQualityView__viewModel_viewModel: return ".showVideoQualityView(viewModel:)"
case .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelectanalytics_analytics: return ".showVideoDownloadQualityView(downloadQuality:didSelect:analytics:)"
case .m_showDeleteProfileView: return ".showDeleteProfileView()"
@@ -3368,6 +3390,8 @@ 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 showVideoSettings() -> Verify { return Verify(method: .m_showVideoSettings)}
+ public static func showManageAccount() -> Verify { return Verify(method: .m_showManageAccount)}
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)?>, analytics: Parameter) -> Verify { return Verify(method: .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelectanalytics_analytics(`downloadQuality`, `didSelect`, `analytics`))}
public static func showDeleteProfileView() -> Verify { return Verify(method: .m_showDeleteProfileView)}
@@ -3399,6 +3423,12 @@ open class ProfileRouterMock: ProfileRouter, Mock {
public static func showSettings(perform: @escaping () -> Void) -> Perform {
return Perform(method: .m_showSettings, performs: perform)
}
+ public static func showVideoSettings(perform: @escaping () -> Void) -> Perform {
+ return Perform(method: .m_showVideoSettings, performs: perform)
+ }
+ public static func showManageAccount(perform: @escaping () -> Void) -> Perform {
+ return Perform(method: .m_showManageAccount, performs: perform)
+ }
public static func showVideoQualityView(viewModel: Parameter, perform: @escaping (SettingsViewModel) -> Void) -> Perform {
return Perform(method: .m_showVideoQualityView__viewModel_viewModel(`viewModel`), performs: perform)
}
diff --git a/Theme/Theme/Assets.xcassets/Auth/Contents.json b/Theme/Theme/Assets.xcassets/Auth/Contents.json
deleted file mode 100644
index 73c00596a..000000000
--- a/Theme/Theme/Assets.xcassets/Auth/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Contents.json b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Contents.json
similarity index 100%
rename from Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Contents.json
rename to Theme/Theme/Assets.xcassets/headerBackground.imageset/Contents.json
diff --git a/Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle-2.png b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle-2.png
similarity index 100%
rename from Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle-2.png
rename to Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle-2.png
diff --git a/Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle.png b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle.png
similarity index 100%
rename from Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle.png
rename to Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle.png
diff --git a/Theme/Theme/SwiftGen/ThemeAssets.swift b/Theme/Theme/SwiftGen/ThemeAssets.swift
index 733a02635..b2573c7f0 100644
--- a/Theme/Theme/SwiftGen/ThemeAssets.swift
+++ b/Theme/Theme/SwiftGen/ThemeAssets.swift
@@ -24,7 +24,6 @@ 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 accentXColor = ColorAsset(name: "AccentXColor")
@@ -81,6 +80,7 @@ public enum ThemeAssets {
public static let warningText = ColorAsset(name: "warningText")
public static let white = ColorAsset(name: "white")
public static let appLogo = ImageAsset(name: "appLogo")
+ public static let headerBackground = ImageAsset(name: "headerBackground")
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
From 2f99d755c7ab7d0ac37e29924e2073de3199f626 Mon Sep 17 00:00:00 2001
From: Anton Yarmolenko
Date: Tue, 7 May 2024 16:47:14 +0200
Subject: [PATCH 02/55] chore: added custom back button for downloads view
---
.../Downloads/DownloadsView.swift | 28 ++++++++++++++++++-
.../Downloads/DownloadsViewModel.swift | 4 +++
.../Outline/CourseOutlineView.swift | 2 +-
OpenEdX/Router.swift | 7 ++++-
4 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/Course/Course/Presentation/Downloads/DownloadsView.swift b/Course/Course/Presentation/Downloads/DownloadsView.swift
index 3c648ead4..791791204 100644
--- a/Course/Course/Presentation/Downloads/DownloadsView.swift
+++ b/Course/Course/Presentation/Downloads/DownloadsView.swift
@@ -15,12 +15,14 @@ public struct DownloadsView: View {
// MARK: - Properties
@Environment(\.dismiss) private var dismiss
+ @Environment (\.isHorizontal) private var isHorizontal
@StateObject private var viewModel: DownloadsViewModel
var isSheet: Bool = true
public init(
isSheet: Bool = true,
+ router: CourseRouter,
courseId: String? = nil,
downloads: [DownloadDataTask] = [],
manager: DownloadManagerProtocol
@@ -28,6 +30,7 @@ public struct DownloadsView: View {
self.isSheet = isSheet
self._viewModel = .init(
wrappedValue: .init(
+ router: router,
courseId: courseId,
downloads: downloads,
manager: manager
@@ -38,13 +41,36 @@ public struct DownloadsView: View {
// MARK: - Body
public var body: some View {
- ZStack {
+ ZStack(alignment: .top) {
Theme.Colors.background
.ignoresSafeArea()
+ if !isSheet {
+ HStack {
+ Text(CourseLocalization.Download.downloads)
+ .titleSettings(color: Theme.Colors.textPrimary)
+ .accessibilityIdentifier("downloads_text")
+ }
+ .padding(.top, isHorizontal ? 10 : 0)
+ VStack {
+ BackNavigationButton(
+ color: Theme.Colors.accentColor,
+ action: {
+ viewModel.router.back()
+ }
+ )
+ .backViewStyle()
+ .padding(.leading, 8)
+
+ }
+ .frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading)
+ .padding(.top, isHorizontal ? 23 : 13)
+
+ }
content
.sheetNavigation(isSheet: isSheet) {
dismiss()
}
+ .padding(.top, isSheet ? 0 : 40)
}
}
diff --git a/Course/Course/Presentation/Downloads/DownloadsViewModel.swift b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift
index 78c063778..709ecc402 100644
--- a/Course/Course/Presentation/Downloads/DownloadsViewModel.swift
+++ b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift
@@ -15,15 +15,19 @@ final class DownloadsViewModel: ObservableObject {
@Published private(set) var downloads: [DownloadDataTask] = []
private let courseId: String?
+
+ let router: CourseRouter
private let manager: DownloadManagerProtocol
private var cancellables = Set()
init(
+ router: CourseRouter,
courseId: String? = nil,
downloads: [DownloadDataTask] = [],
manager: DownloadManagerProtocol
) {
+ self.router = router
self.courseId = courseId
self.manager = manager
self.downloads = downloads
diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift
index 18575dc7a..90cf6e1d9 100644
--- a/Course/Course/Presentation/Outline/CourseOutlineView.swift
+++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift
@@ -216,7 +216,7 @@ public struct CourseOutlineView: View {
.ignoresSafeArea()
)
.sheet(isPresented: $showingDownloads) {
- DownloadsView(manager: viewModel.manager)
+ DownloadsView(router: viewModel.router, manager: viewModel.manager)
}
.sheet(isPresented: $showingVideoDownloadQuality) {
viewModel.storage.userSettings.map {
diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift
index 6c96146b3..347d721ef 100644
--- a/OpenEdX/Router.swift
+++ b/OpenEdX/Router.swift
@@ -537,7 +537,12 @@ public class Router: AuthorizationRouter,
downloads: [DownloadDataTask],
manager: DownloadManagerProtocol
) {
- let downloadsView = DownloadsView(isSheet: false, downloads: downloads, manager: manager)
+ let downloadsView = DownloadsView(
+ isSheet: false,
+ router: Container.shared.resolve(CourseRouter.self)!,
+ downloads: downloads,
+ manager: manager
+ )
let controller = UIHostingController(rootView: downloadsView)
navigationController.pushViewController(controller, animated: true)
}
From bd9f081729b2c254e2d82cb698a2677f828609b8 Mon Sep 17 00:00:00 2001
From: Shafqat Muneer
Date: Tue, 7 May 2024 21:08:24 +0500
Subject: [PATCH 03/55] feat: Alert for outdated course calendars on the course
home screen
---
Core/Core/Extensions/Notification.swift | 1 +
.../Container/CourseContainerView.swift | 41 ++++++++++++++++++-
.../Container/CourseContainerViewModel.swift | 1 +
.../Presentation/Dates/CourseDatesView.swift | 3 +-
.../Dates/CourseDatesViewModel.swift | 12 ++++++
Course/Course/Views/DatesSuccessView.swift | 7 +---
OpenEdX/Router.swift | 8 ++++
7 files changed, 64 insertions(+), 9 deletions(-)
diff --git a/Core/Core/Extensions/Notification.swift b/Core/Core/Extensions/Notification.swift
index ba9dfe70c..9f792fb2a 100644
--- a/Core/Core/Extensions/Notification.swift
+++ b/Core/Core/Extensions/Notification.swift
@@ -17,4 +17,5 @@ public extension Notification.Name {
static let onBlockCompletion = Notification.Name.init("onBlockCompletion")
static let shiftCourseDates = Notification.Name("shiftCourseDates")
static let profileUpdated = Notification.Name("profileUpdated")
+ static let getCourseDates = Notification.Name("getCourseDates")
}
diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift
index 322b37564..d79f12034 100644
--- a/Course/Course/Presentation/Container/CourseContainerView.swift
+++ b/Course/Course/Presentation/Container/CourseContainerView.swift
@@ -15,6 +15,8 @@ public struct CourseContainerView: View {
@ObservedObject
public var viewModel: CourseContainerViewModel
+ @ObservedObject
+ public var courseDatesViewModel: CourseDatesViewModel
@State private var isAnimatingForTap: Bool = false
public var courseID: String
private var title: String
@@ -39,6 +41,7 @@ public struct CourseContainerView: View {
public init(
viewModel: CourseContainerViewModel,
+ courseDatesViewModel: CourseDatesViewModel,
courseID: String,
title: String
) {
@@ -55,6 +58,7 @@ public struct CourseContainerView: View {
}
self.courseID = courseID
self.title = title
+ self.courseDatesViewModel = courseDatesViewModel
}
public var body: some View {
@@ -112,8 +116,32 @@ public struct CourseContainerView: View {
}
}
}
+
+ switch courseDatesViewModel.eventState {
+ case .removedCalendar:
+ showDatesSuccessView(
+ title: CourseLocalization.CourseDates.calendarEvents,
+ message: CourseLocalization.CourseDates.calendarEventsRemoved
+ )
+ case .updatedCalendar:
+ showDatesSuccessView(
+ title: CourseLocalization.CourseDates.calendarEvents,
+ message: CourseLocalization.CourseDates.calendarEventsUpdated
+ )
+ default:
+ EmptyView()
+ }
}
+ private func showDatesSuccessView(title: String, message: String) -> some View {
+ return DatesSuccessView(
+ title: title,
+ message: message
+ ) {
+ courseDatesViewModel.resetEventState()
+ }
+ }
+
private func backButton(containerWidth: CGFloat) -> some View {
ZStack(alignment: .topLeading) {
if !collapsed {
@@ -184,8 +212,7 @@ public struct CourseContainerView: View {
courseID: courseID,
coordinate: $coordinate,
collapsed: $collapsed,
- viewModel: Container.shared.resolve(CourseDatesViewModel.self,
- arguments: courseID, title)!
+ viewModel: courseDatesViewModel
)
.tabItem {
tab.image
@@ -313,6 +340,16 @@ struct CourseScreensView_Previews: PreviewProvider {
enrollmentEnd: nil,
coreAnalytics: CoreAnalyticsMock()
),
+ courseDatesViewModel: CourseDatesViewModel(
+ interactor: CourseInteractor.mock,
+ router: CourseRouterMock(),
+ cssInjector: CSSInjectorMock(),
+ connectivity: Connectivity(),
+ config: ConfigMock(),
+ courseID: "1",
+ courseName: "a",
+ analytics: CourseAnalyticsMock()
+ ),
courseID: "", title: "Title of Course")
}
}
diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift
index 82572b60e..e8ce69707 100644
--- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift
+++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift
@@ -140,6 +140,7 @@ public class CourseContainerViewModel: BaseCourseViewModel {
do {
if isInternetAvaliable {
courseStructure = try await interactor.getCourseBlocks(courseID: courseID)
+ NotificationCenter.default.post(name: .getCourseDates, object: courseID)
isShowProgress = false
isShowRefresh = false
if let courseStructure {
diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift
index 92dc86a6c..d4a58dfd7 100644
--- a/Course/Course/Presentation/Dates/CourseDatesView.swift
+++ b/Course/Course/Presentation/Dates/CourseDatesView.swift
@@ -114,8 +114,7 @@ public struct CourseDatesView: View {
} else {
return DatesSuccessView(
title: title,
- message: message,
- selectedTab: .dates
+ message: message
) {
viewModel.resetEventState()
}
diff --git a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift
index 2effb2678..8ee011025 100644
--- a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift
+++ b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift
@@ -195,6 +195,18 @@ extension CourseDatesViewModel {
selector: #selector(handleShiftDueDates),
name: .shiftCourseDates, object: nil
)
+
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(getCourseDates),
+ name: .getCourseDates, object: nil
+ )
+ }
+
+ @objc private func getCourseDates(_ notification: Notification) {
+ Task {
+ await getCourseDates(courseID: courseID)
+ }
}
@objc private func handleShiftDueDates(_ notification: Notification) {
diff --git a/Course/Course/Views/DatesSuccessView.swift b/Course/Course/Views/DatesSuccessView.swift
index 3adfe607c..2b9040683 100644
--- a/Course/Course/Views/DatesSuccessView.swift
+++ b/Course/Course/Views/DatesSuccessView.swift
@@ -18,7 +18,7 @@ public struct DatesSuccessView: View {
private var title: String
private var message: String
- var selectedTab: Tab
+ var selectedTab: Tab?
var courseDatesViewModel: CourseDatesViewModel?
var courseContainerViewModel: CourseContainerViewModel?
var action: () -> Void = {}
@@ -29,12 +29,10 @@ public struct DatesSuccessView: View {
init (
title: String,
message: String,
- selectedTab: Tab,
dismissAction: @escaping () -> Void
) {
self.title = title
self.message = message
- self.selectedTab = selectedTab
self.dismissAction = dismissAction
}
@@ -149,8 +147,7 @@ struct DatesSuccessView_Previews: PreviewProvider {
static var previews: some View {
DatesSuccessView(
title: CourseLocalization.CourseDates.toastSuccessTitle,
- message: CourseLocalization.CourseDates.toastSuccessMessage,
- selectedTab: .course
+ message: CourseLocalization.CourseDates.toastSuccessMessage
) {}
}
}
diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift
index 80fd75ad6..8e582a5f4 100644
--- a/OpenEdX/Router.swift
+++ b/OpenEdX/Router.swift
@@ -395,8 +395,16 @@ public class Router: AuthorizationRouter,
enrollmentStart,
enrollmentEnd
)!
+
+ let datesVm = Container.shared.resolve(
+ CourseDatesViewModel.self,
+ arguments: courseID,
+ title
+ )!
+
let screensView = CourseContainerView(
viewModel: vm,
+ courseDatesViewModel: datesVm,
courseID: courseID,
title: title
)
From 102e44c2af2b4225de8c329ef39bbc82a5906f5b Mon Sep 17 00:00:00 2001
From: Saeed Bashir
Date: Thu, 9 May 2024 10:40:34 +0500
Subject: [PATCH 04/55] chore: Sliding course menu theme enhancement
---
.../ScrollSlidingTabBar.swift | 2 +-
.../Contents.json | 38 +++++++++++++++++++
Theme/Theme/SwiftGen/ThemeAssets.swift | 1 +
Theme/Theme/Theme.swift | 1 +
4 files changed, 41 insertions(+), 1 deletion(-)
create mode 100644 Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingSelectedTextColor.colorset/Contents.json
diff --git a/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift
index 5f09777a7..bd558d197 100644
--- a/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift
+++ b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift
@@ -100,7 +100,7 @@ extension ScrollSlidingTabBar {
}
.accentColor(
isSelected(index: obj.offset)
- ? Theme.Colors.white
+ ? Theme.Colors.slidingSelectedTextColor
: Theme.Colors.slidingTextColor
)
}
diff --git a/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingSelectedTextColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingSelectedTextColor.colorset/Contents.json
new file mode 100644
index 000000000..22c4bb0a8
--- /dev/null
+++ b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingSelectedTextColor.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/SwiftGen/ThemeAssets.swift b/Theme/Theme/SwiftGen/ThemeAssets.swift
index b2573c7f0..aa00c67f5 100644
--- a/Theme/Theme/SwiftGen/ThemeAssets.swift
+++ b/Theme/Theme/SwiftGen/ThemeAssets.swift
@@ -56,6 +56,7 @@ public enum ThemeAssets {
public static let secondaryButtonBorderColor = ColorAsset(name: "SecondaryButtonBorderColor")
public static let secondaryButtonTextColor = ColorAsset(name: "SecondaryButtonTextColor")
public static let shadowColor = ColorAsset(name: "ShadowColor")
+ public static let slidingSelectedTextColor = ColorAsset(name: "slidingSelectedTextColor")
public static let slidingStrokeColor = ColorAsset(name: "slidingStrokeColor")
public static let slidingTextColor = ColorAsset(name: "slidingTextColor")
public static let snackbarErrorColor = ColorAsset(name: "SnackbarErrorColor")
diff --git a/Theme/Theme/Theme.swift b/Theme/Theme/Theme.swift
index 0fad87eb9..c985b752b 100644
--- a/Theme/Theme/Theme.swift
+++ b/Theme/Theme/Theme.swift
@@ -65,6 +65,7 @@ public struct Theme {
public private(set) static var infoColor = ThemeAssets.infoColor.swiftUIColor
public private(set) static var irreversibleAlert = ThemeAssets.irreversibleAlert.swiftUIColor
public private(set) static var slidingTextColor = ThemeAssets.slidingTextColor.swiftUIColor
+ public private(set) static var slidingSelectedTextColor = ThemeAssets.slidingSelectedTextColor.swiftUIColor
public private(set) static var slidingStrokeColor = ThemeAssets.slidingStrokeColor.swiftUIColor
public private(set) static var primaryHeaderColor = ThemeAssets.primaryHeaderColor.swiftUIColor
public private(set) static var secondaryHeaderColor = ThemeAssets.secondaryHeaderColor.swiftUIColor
From 5305a92e3d481b6293dc5204a30da2d0c6b80508 Mon Sep 17 00:00:00 2001
From: Volodymyr Chekyrta
<127732735+volodymyr-chekyrta@users.noreply.github.com>
Date: Thu, 9 May 2024 15:55:46 +0300
Subject: [PATCH 05/55] fix: discovery enabled by default (#433)
---
Core/Core/Configuration/Config/DiscoveryConfig.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Core/Core/Configuration/Config/DiscoveryConfig.swift b/Core/Core/Configuration/Config/DiscoveryConfig.swift
index 884800441..893ea0ca9 100644
--- a/Core/Core/Configuration/Config/DiscoveryConfig.swift
+++ b/Core/Core/Configuration/Config/DiscoveryConfig.swift
@@ -40,7 +40,7 @@ public class DiscoveryConfig: NSObject {
init(dictionary: [String: AnyObject]) {
type = (dictionary[DiscoveryKeys.discoveryType] as? String).flatMap {
DiscoveryConfigType(rawValue: $0)
- } ?? .none
+ } ?? .native
webview = DiscoveryWebviewConfig(dictionary: dictionary[DiscoveryKeys.webview] as? [String: AnyObject] ?? [:])
}
From 9e18a29c8d94671638088254382d62aff9a53177 Mon Sep 17 00:00:00 2001
From: Volodymyr Chekyrta
<127732735+volodymyr-chekyrta@users.noreply.github.com>
Date: Thu, 9 May 2024 15:56:55 +0300
Subject: [PATCH 06/55] fix: build warnings (#432)
---
.swiftlint.yml | 11 ++++++++-
.../Presentation/AuthorizationAnalytics.swift | 8 +++----
Core/Core/Extensions/DateExtension.swift | 1 -
Core/Core/Extensions/Dictionary+JSON.swift | 2 +-
Core/Core/Extensions/String+JSON.swift | 2 +-
Core/Core/Extensions/ViewExtension.swift | 2 +-
Core/Core/Network/DownloadManager.swift | 2 ++
.../SocialAuth/MicrosoftAuthProvider.swift | 2 +-
.../ThirdPartyMailClient.swift | 3 +++
.../View/Base/RefreshableScrollView.swift | 2 +-
Core/Core/View/Base/UnitButtonView.swift | 3 ++-
Core/Core/View/Base/Webview/WebView.swift | 4 ++--
Course/Course.xcodeproj/project.pbxproj | 16 ++++---------
.../Presentation/Dates/CourseDatesView.swift | 2 +-
.../Outline/CourseOutlineView.swift | 2 +-
.../Unit/CourseNavigationView.swift | 2 +-
.../Presentation/Video/SubtittlesView.swift | 2 +-
.../Video/YouTubeVideoPlayer.swift | 2 +-
.../Data/Network/DiscussionRepository.swift | 4 ++--
.../Discussion/Domain/Model/UserThread.swift | 2 +-
.../DiscussionSearchTopicsViewModel.swift | 2 +-
.../AnalyticsManager/AnalyticsManager.swift | 10 +++++---
.../Listeners/BrazeListener.swift | 5 ++--
Podfile | 2 +-
Podfile.lock | 12 +++++-----
.../EditProfile/EditProfileViewModel.swift | 23 +++++++++++--------
Theme/Theme/Theme.swift | 2 ++
27 files changed, 72 insertions(+), 58 deletions(-)
diff --git a/.swiftlint.yml b/.swiftlint.yml
index f6b96b50b..600160d85 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -24,8 +24,17 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
- Discovery/DiscoveryTests
- Discussion/DiscussionTests
- Profile/ProfileTests
+ - WhatsNew/WhatsNewTests
+ - Theme/ThemeTests
- vendor
-# - Source/ExcludedFolder
+ - Core/Core/SwiftGen
+ - Authorization/Authorization/SwiftGen
+ - Course/Course/SwiftGen
+ - Discovery/Discovery/SwiftGen
+ - Dashboard/Dashboard/SwiftGen
+ - Profile/Profile/SwiftGen
+ - WhatsNew/WhatsNew/SwiftGen
+ - Theme/Theme/SwiftGen
# - Source/ExcludedFile.swift
# - Source/*/ExcludedFile.swift # Exclude files with a wildcard
#analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
diff --git a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift
index b59ebd774..00cb384a5 100644
--- a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift
+++ b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift
@@ -22,10 +22,10 @@ public enum AuthMethod: Equatable {
}
public enum SocialAuthMethod: String {
- case facebook = "facebook"
- case google = "google"
- case microsoft = "microsoft"
- case apple = "apple"
+ case facebook
+ case google
+ case microsoft
+ case apple
}
//sourcery: AutoMockable
diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift
index 8a57079f4..bbdb6834b 100644
--- a/Core/Core/Extensions/DateExtension.swift
+++ b/Core/Core/Extensions/DateExtension.swift
@@ -7,7 +7,6 @@
import Foundation
-
public extension Date {
init(iso8601: String) {
let formats = ["yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"]
diff --git a/Core/Core/Extensions/Dictionary+JSON.swift b/Core/Core/Extensions/Dictionary+JSON.swift
index 398fc3676..938cef881 100644
--- a/Core/Core/Extensions/Dictionary+JSON.swift
+++ b/Core/Core/Extensions/Dictionary+JSON.swift
@@ -8,7 +8,7 @@
import Foundation
public extension Dictionary where Key == String, Value == String {
- public func toJson() -> String? {
+ func toJson() -> String? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: self, options: []) else {
return nil
}
diff --git a/Core/Core/Extensions/String+JSON.swift b/Core/Core/Extensions/String+JSON.swift
index ab171369e..6d801a886 100644
--- a/Core/Core/Extensions/String+JSON.swift
+++ b/Core/Core/Extensions/String+JSON.swift
@@ -8,7 +8,7 @@
import Foundation
public extension String {
- public func jsonStringToDictionary() -> [String: Any]? {
+ func jsonStringToDictionary() -> [String: Any]? {
guard let jsonData = self.data(using: .utf8) else {
return nil
}
diff --git a/Core/Core/Extensions/ViewExtension.swift b/Core/Core/Extensions/ViewExtension.swift
index 4d98df77d..71392ebd7 100644
--- a/Core/Core/Extensions/ViewExtension.swift
+++ b/Core/Core/Extensions/ViewExtension.swift
@@ -154,7 +154,7 @@ public extension View {
.offset(y: 2)
.foregroundColor(color)
self
- .offset(y: 2)
+ .offset(y: 2)
}
}
diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift
index 2f967597a..15ec5fd10 100644
--- a/Core/Core/Network/DownloadManager.swift
+++ b/Core/Core/Network/DownloadManager.swift
@@ -551,6 +551,7 @@ public final class BackgroundTaskProvider {
}
// Mark - For testing and SwiftUI preview
+// swiftlint:disable file_length
#if DEBUG
public class DownloadManagerMock: DownloadManagerProtocol {
@@ -639,3 +640,4 @@ public class DownloadManagerMock: DownloadManagerProtocol {
}
#endif
+// swiftlint:enable file_length
diff --git a/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift b/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift
index 16178b17c..2fd998579 100644
--- a/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift
+++ b/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift
@@ -49,7 +49,7 @@ public final class MicrosoftAuthProvider {
continuation.resume(
returning: .success(
SocialAuthResponse(
- name: account.accountClaims?["name"] as? String ?? "" ,
+ name: account.accountClaims?["name"] as? String ?? "",
email: account.accountClaims?["email"] as? String ?? "",
token: result.accessToken
)
diff --git a/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift b/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift
index 76d48270e..5fdf14d34 100644
--- a/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift
+++ b/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift
@@ -5,6 +5,8 @@
//
// Licensed under MIT License
+// swiftlint:disable all
+
import SwiftUI
/// A third-party mail client, offering a custom URL scheme.
@@ -145,3 +147,4 @@ public extension ThirdPartyMailClient {
}
}
}
+// swiftlint:enable all
diff --git a/Core/Core/View/Base/RefreshableScrollView.swift b/Core/Core/View/Base/RefreshableScrollView.swift
index 0905bdba6..d09148528 100644
--- a/Core/Core/View/Base/RefreshableScrollView.swift
+++ b/Core/Core/View/Base/RefreshableScrollView.swift
@@ -282,7 +282,7 @@ public extension List {
onRefresh: @escaping OnRefresh,
@ViewBuilder progress:
@escaping RefreshProgressBuilder