diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 80bc8fd5d..5e7252c90 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -118,7 +118,6 @@ 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE5E28D0B22C006D8A5D /* Strings.swift */; }; 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE6028D0B2CB006D8A5D /* Assets.swift */; }; 07DDFCBD29A780BB00572595 /* UINavigationController+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */; }; - BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */; }; BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA30427D2B20B299009B64B7 /* SocialAuthError.swift */; }; BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */; }; BA8B3A2F2AD546A700D25EF5 /* DebugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */; }; @@ -128,6 +127,8 @@ BA8FA66C2AD59BBC00EA029A /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */; }; BA8FA66E2AD59E7D00EA029A /* FacebookAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */; }; BA8FA6702AD59EA300EA029A /* MicrosoftAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */; }; + BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */; }; + BAD9CA422B2B140100DE790A /* AgreementConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */; }; BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */; }; BAF0D4CB2AD6AE14007AC334 /* FacebookLogin in Frameworks */ = {isa = PBXBuildFile; productRef = BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */; }; BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */; }; @@ -289,7 +290,6 @@ 3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = ""; }; 60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = ""; }; 9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = ""; }; - BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDisclosureGroup.swift; sourceTree = ""; }; BA30427D2B20B299009B64B7 /* SocialAuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocialAuthError.swift; sourceTree = ""; }; BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthResponse.swift; sourceTree = ""; }; BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLog.swift; sourceTree = ""; }; @@ -298,6 +298,8 @@ BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthProvider.swift; sourceTree = ""; }; BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthProvider.swift; sourceTree = ""; }; BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftAuthProvider.swift; sourceTree = ""; }; + BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDisclosureGroup.swift; sourceTree = ""; }; + BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementConfigTests.swift; sourceTree = ""; }; BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultExtension.swift; sourceTree = ""; }; BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookConfig.swift; sourceTree = ""; }; BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftConfig.swift; sourceTree = ""; }; @@ -718,6 +720,7 @@ isa = PBXGroup; children = ( E09179FC2B0F204D002AB695 /* ConfigTests.swift */, + BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */, ); path = Configuration; sourceTree = ""; @@ -902,6 +905,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BAD9CA422B2B140100DE790A /* AgreementConfigTests.swift in Sources */, E09179FD2B0F204E002AB695 /* ConfigTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Core/Core/Configuration/Config/AgreementConfig.swift b/Core/Core/Configuration/Config/AgreementConfig.swift index 46059500e..b790f2cca 100644 --- a/Core/Core/Configuration/Config/AgreementConfig.swift +++ b/Core/Core/Configuration/Config/AgreementConfig.swift @@ -10,16 +10,60 @@ import Foundation private enum AgreementKeys: String { case privacyPolicyURL = "PRIVACY_POLICY_URL" case tosURL = "TOS_URL" + case cookiePolicyURL = "COOKIE_POLICY_URL" + case dataSellContentURL = "DATA_SELL_CONSENT_URL" + case supportedLanguages = "SUPPORTED_LANGUAGES" } public class AgreementConfig: NSObject { public var privacyPolicyURL: URL? public var tosURL: URL? - + public var cookiePolicyURL: URL? + public var dataSellContentURL: URL? + public var supportedLanguages: [String]? + init(dictionary: [String: AnyObject]) { - privacyPolicyURL = (dictionary[AgreementKeys.privacyPolicyURL.rawValue] as? String).flatMap(URL.init) - tosURL = (dictionary[AgreementKeys.tosURL.rawValue] as? String).flatMap(URL.init) + supportedLanguages = dictionary[AgreementKeys.supportedLanguages.rawValue] as? [String] + cookiePolicyURL = (dictionary[AgreementKeys.cookiePolicyURL.rawValue] as? String).flatMap(URL.init) + dataSellContentURL = (dictionary[AgreementKeys.dataSellContentURL.rawValue] as? String).flatMap(URL.init) + super.init() + + if let tosURL = dictionary[AgreementKeys.tosURL.rawValue] as? String { + self.tosURL = URL(string: completePath(url: tosURL)) + } + + if let privacyPolicyURL = dictionary[AgreementKeys.privacyPolicyURL.rawValue] as? String { + self.privacyPolicyURL = URL(string: completePath(url: privacyPolicyURL)) + } + } + + private func completePath(url: String) -> String { + let langCode: String + if #available(iOS 16, *) { + langCode = Locale.current.language.languageCode?.identifier ?? "" + } else { + langCode = Locale.current.languageCode ?? "" + } + + if let supportedLanguages = supportedLanguages, + !supportedLanguages.contains(langCode) { + return url + } + + let URL = URL(string: url) + let host = URL?.host ?? "" + let components = url.components(separatedBy: host) + + if components.count != 2 { + return url + } + + if let firstComponent = components.first, let lastComponent = components.last { + return "\(firstComponent)\(host)/\(langCode)\(lastComponent)" + } + + return url } } diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift index a10438eb6..75c6dc7c4 100644 --- a/Core/Core/Configuration/Config/Config.swift +++ b/Core/Core/Configuration/Config/Config.swift @@ -13,6 +13,7 @@ public protocol ConfigProtocol { var tokenType: TokenType { get } var feedbackEmail: String { get } var appStoreLink: String { get } + var faq: URL? { get } var platformName: String { get } var agreement: AgreementConfig { get } var firebase: FirebaseConfig { get } @@ -38,6 +39,7 @@ private enum ConfigKeys: String { case platformName = "PLATFORM_NAME" case organizationCode = "ORGANIZATION_CODE" case appstoreID = "APP_STORE_ID" + case faq = "FAQ_URL" } public class Config { @@ -137,6 +139,14 @@ extension Config: ConfigProtocol { public var appStoreLink: String { "itms-apps://itunes.apple.com/app/id\(appStoreId)?mt=8" } + + public var faq: URL? { + guard let urlString = string(for: ConfigKeys.faq.rawValue), + let url = URL(string: urlString) else { + return nil + } + return url + } } // Mark - For testing and SwiftUI preview @@ -151,7 +161,10 @@ public class ConfigMock: Config { "WHATS_NEW_ENABLED": false, "AGREEMENT_URLS": [ "PRIVACY_POLICY_URL": "https://www.example.com/privacy", - "TOS_URL": "https://www.example.com/tos" + "TOS_URL": "https://www.example.com/tos", + "DATA_SELL_CONSENT_URL": "https://www.example.com/sell", + "COOKIE_POLICY_URL": "https://www.example.com/cookie", + "SUPPORTED_LANGUAGES": ["es"] ], "GOOGLE": [ "ENABLED": true, diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index ef04c50c5..dafd884fc 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -10,42 +10,56 @@ import WebKit import Theme public struct WebBrowser: View { - - var url: String - var pageTitle: String - @State private var isShowProgress: Bool = true + + @State private var isLoading: Bool = true @Environment(\.presentationMode) var presentationMode + + private var url: String + private var pageTitle: String + private var showProgress: Bool - public init(url: String, pageTitle: String) { + public init(url: String, pageTitle: String, showProgress: Bool = false) { self.url = url self.pageTitle = pageTitle + self.showProgress = showProgress } public var body: some View { - ZStack(alignment: .top) { - Theme.Colors.background.ignoresSafeArea() - // MARK: - Page name - VStack(alignment: .center) { - NavigationBar(title: pageTitle, - leftButtonAction: { presentationMode.wrappedValue.dismiss() }) - - // MARK: - Page Body - VStack { - ZStack(alignment: .top) { -// NavigationView { - WebView( - viewModel: .init(url: url, baseURL: ""), - isLoading: $isShowProgress, - refreshCookies: {} - ) - -// } - }.navigationBarTitle(Text("")) // Needed for hide navBar on ios 14, 15 - .navigationBarHidden(true) - .ignoresSafeArea() + GeometryReader { proxy in + ZStack(alignment: .center) { + Theme.Colors.background.ignoresSafeArea() + webView(proxy: proxy) + if isLoading, showProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(20) + } + .frame(maxWidth: .infinity) } } + .navigationBarTitle(Text("")) + .navigationBarHidden(true) + .ignoresSafeArea() + } + } + + private func webView(proxy: GeometryProxy) -> some View { + VStack(alignment: .center) { + NavigationBar( + title: pageTitle, + leftButtonAction: { presentationMode.wrappedValue.dismiss() } + ) + WebView( + viewModel: .init(url: url, baseURL: ""), + isLoading: $isLoading, + refreshCookies: {} + ) } + .padding(.top, proxy.safeAreaInsets.top) + .padding(.bottom, proxy.safeAreaInsets.bottom) } } diff --git a/Core/CoreTests/Configuration/AgreementConfigTests.swift b/Core/CoreTests/Configuration/AgreementConfigTests.swift new file mode 100644 index 000000000..9a30e84fb --- /dev/null +++ b/Core/CoreTests/Configuration/AgreementConfigTests.swift @@ -0,0 +1,38 @@ +// +// AgreementConfigTests.swift +// CoreTests +// +// Created by Eugene Yatsenko on 14.12.2023. +// + +import XCTest +@testable import Core + +class AgreementConfigTests: XCTestCase { + + private let privacy = "https://www.example.com/privacy" + private let tos = "https://www.example.com/tos" + private let dataSellContent = "https://www.example.com/sell" + private let cookie = "https://www.example.com/cookie" + private let supportedLanguages = ["es"] + + private lazy var properties: [String: Any] = [ + "AGREEMENT_URLS": [ + "PRIVACY_POLICY_URL": privacy, + "TOS_URL": tos, + "DATA_SELL_CONSENT_URL": dataSellContent, + "COOKIE_POLICY_URL": cookie, + "SUPPORTED_LANGUAGES": supportedLanguages + ] + ] + + func testAgreementConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertEqual(config.agreement.privacyPolicyURL, URL(string: privacy)) + XCTAssertEqual(config.agreement.tosURL, URL(string: tos)) + XCTAssertEqual(config.agreement.cookiePolicyURL, URL(string: cookie)) + XCTAssertEqual(config.agreement.dataSellContentURL, URL(string: dataSellContent)) + XCTAssertEqual(config.agreement.supportedLanguages, supportedLanguages) + } +} diff --git a/Core/CoreTests/Configuration/ConfigTests.swift b/Core/CoreTests/Configuration/ConfigTests.swift index 694ee6734..32638e484 100644 --- a/Core/CoreTests/Configuration/ConfigTests.swift +++ b/Core/CoreTests/Configuration/ConfigTests.swift @@ -18,7 +18,10 @@ class ConfigTests: XCTestCase { "WHATS_NEW_ENABLED": true, "AGREEMENT_URLS": [ "PRIVACY_POLICY_URL": "https://www.example.com/privacy", - "TOS_URL": "https://www.example.com/tos" + "TOS_URL": "https://www.example.com/tos", + "DATA_SELL_CONSENT_URL": "https://www.example.com/sell", + "COOKIE_POLICY_URL": "https://www.example.com/cookie", + "SUPPORTED_LANGUAGES": ["es"] ], "FIREBASE": [ "ENABLED": true, @@ -68,13 +71,6 @@ class ConfigTests: XCTestCase { XCTAssertTrue(config.features.whatNewEnabled) } - func testAgreementConfigInitialization() { - let config = Config(properties: properties) - - XCTAssertEqual(config.agreement.privacyPolicyURL, URL(string: "https://www.example.com/privacy")) - XCTAssertEqual(config.agreement.tosURL, URL(string: "https://www.example.com/tos")) - } - func testFirebaseConfigInitialization() { let config = Config(properties: properties) diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index 28dc8a604..f4ebc7e78 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 02F3BFE7292539850051930C /* ProfileRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFE6292539850051930C /* ProfileRouter.swift */; }; 0796C8C929B7905300444B05 /* ProfileBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0796C8C829B7905300444B05 /* ProfileBottomSheet.swift */; }; 25B36FF48C1307888A3890DA /* Pods_App_Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEA369C38362C1A91A012F70 /* Pods_App_Profile.framework */; }; + BAD9CA3F2B29BF5C00DE790A /* ProfileSupportInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA3E2B29BF5C00DE790A /* ProfileSupportInfoView.swift */; }; E8264C634DD8AD314ECE8905 /* Pods_App_Profile_ProfileTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C85ADF87135E03275A980E07 /* Pods_App_Profile_ProfileTests.framework */; }; /* End PBXBuildFile section */ @@ -94,6 +95,7 @@ 9D125F82E0EAC4B6C0CE280F /* Pods-App-Profile-ProfileTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile-ProfileTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Profile-ProfileTests/Pods-App-Profile-ProfileTests.releaseprod.xcconfig"; sourceTree = ""; }; A9F98CD65D1F657EB8F9EA59 /* Pods-App-Profile.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.releasedev.xcconfig"; sourceTree = ""; }; B3F05DC21379BD4FE1AFCCF1 /* Pods-App-Profile.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debugprod.xcconfig"; sourceTree = ""; }; + BAD9CA3E2B29BF5C00DE790A /* ProfileSupportInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSupportInfoView.swift; sourceTree = ""; }; BEA369C38362C1A91A012F70 /* Pods_App_Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C85ADF87135E03275A980E07 /* Pods_App_Profile_ProfileTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Profile_ProfileTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F52EFE7DC07BE68B9A302DAF /* Pods-App-Profile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debug.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debug.xcconfig"; sourceTree = ""; }; @@ -135,6 +137,7 @@ 0203DC3D29AE79F80017BD05 /* Profile */ = { isa = PBXGroup; children = ( + BAD9CA402B29D6CD00DE790A /* Subviews */, 02D0FD072AD695E10020D752 /* UserProfile */, 021D924528DC634300ACC565 /* ProfileView.swift */, 021D925128DC918D00ACC565 /* ProfileViewModel.swift */, @@ -342,6 +345,14 @@ path = ../Pods; sourceTree = ""; }; + BAD9CA402B29D6CD00DE790A /* Subviews */ = { + isa = PBXGroup; + children = ( + BAD9CA3E2B29BF5C00DE790A /* ProfileSupportInfoView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; C456081FB065DCEDAB8119E4 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -563,6 +574,7 @@ 021D924E28DC88BB00ACC565 /* ProfileRepository.swift in Sources */, 0796C8C929B7905300444B05 /* ProfileBottomSheet.swift in Sources */, 021D924C28DC884A00ACC565 /* ProfileEndpoint.swift in Sources */, + BAD9CA3F2B29BF5C00DE790A /* ProfileSupportInfoView.swift in Sources */, 020306C82932B13F000949EA /* EditProfileView.swift in Sources */, 0262149229AE57A1008BD75A /* DeleteAccountView.swift in Sources */, 02D0FD092AD698380020D752 /* UserProfileView.swift in Sources */, diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 431a94e12..0450da9c8 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -11,10 +11,10 @@ import Kingfisher import Theme public struct ProfileView: View { - + @StateObject private var viewModel: ProfileViewModel @Binding var settingsTapped: Bool - + public init(viewModel: ProfileViewModel, settingsTapped: Binding) { self._viewModel = StateObject(wrappedValue: { viewModel }()) self._settingsTapped = settingsTapped @@ -23,280 +23,53 @@ public struct ProfileView: View { public var body: some View { ZStack(alignment: .top) { // MARK: - Page Body - RefreshableScrollViewCompat(action: { - await viewModel.getMyProfile(withProgress: false) - }) { - VStack { - if viewModel.isShowProgress { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - .padding(.horizontal) - } else { - UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar) - .padding(.top, 30) - Text(viewModel.userModel?.name ?? "") - .font(Theme.Fonts.headlineSmall) - .padding(.top, 20) - - Text("@\(viewModel.userModel?.username ?? "")") - .font(Theme.Fonts.labelLarge) - .padding(.top, 4) - .foregroundColor(Theme.Colors.textSecondary) - .padding(.bottom, 10) - - // MARK: - Profile Info - if viewModel.userModel?.yearOfBirth != 0 || viewModel.userModel?.shortBiography != "" { - VStack(alignment: .leading, spacing: 14) { - Text(ProfileLocalization.info) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - - VStack(alignment: .leading, spacing: 16) { - if viewModel.userModel?.yearOfBirth != 0 { - HStack { - Text(ProfileLocalization.Edit.Fields.yearOfBirth) - .foregroundColor(Theme.Colors.textSecondary) - Text(String(viewModel.userModel?.yearOfBirth ?? 0)) - } - } - if let bio = viewModel.userModel?.shortBiography, bio != "" { - HStack(alignment: .top) { - Text(ProfileLocalization.bio + " ") - .foregroundColor(Theme.Colors.textSecondary) - + Text(bio) - } - } - } - .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) + RefreshableScrollViewCompat( + action: { + await viewModel.getMyProfile(withProgress: false) + }, + content: content + ) + .accessibilityAction {} + .frameLimit(sizePortrait: 420) + .padding(.top, 8) + .onChange(of: settingsTapped, perform: { _ in + let userModel = viewModel.userModel ?? UserProfile() + viewModel.trackProfileEditClicked() + viewModel.router.showEditProfile( + userModel: userModel, + avatar: viewModel.updatedAvatar, + profileDidEdit: { updatedProfile, updatedImage in + if let updatedProfile { + self.viewModel.userModel = updatedProfile } - - VStack(alignment: .leading, spacing: 14) { - // MARK: - Settings - Text(ProfileLocalization.settings) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - VStack(alignment: .leading, spacing: 27) { - Button(action: { - viewModel.trackProfileVideoSettingsClicked() - viewModel.router.showSettings() - }, label: { - HStack { - Text(ProfileLocalization.settingsVideo) - Spacer() - Image(systemName: "chevron.right") - } - }) - - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(ProfileLocalization.settingsVideo) - .cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - - // MARK: - Support info - Text(ProfileLocalization.supportInfo) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - VStack(alignment: .leading, spacing: 24) { - if let support = viewModel.contactSupport() { - Button(action: { - viewModel.trackEmailSupportClicked() - UIApplication.shared.open(support) - }, label: { - HStack { - Text(ProfileLocalization.contact) - Spacer() - Image(systemName: "chevron.right") - } - }) - .buttonStyle(PlainButtonStyle()) - .foregroundColor(.primary) - .accessibilityElement(children: .ignore) - .accessibilityLabel(ProfileLocalization.supportInfo) - Rectangle() - .frame(height: 1) - .foregroundColor(Theme.Colors.textSecondary) - } - - if let tos = viewModel.config.agreement.tosURL { - Button(action: { - viewModel.trackCookiePolicyClicked() - UIApplication.shared.open(tos) - }, label: { - HStack { - Text(ProfileLocalization.terms) - Spacer() - Image(systemName: "chevron.right") - } - }) - .buttonStyle(PlainButtonStyle()) - .foregroundColor(.primary) - .accessibilityElement(children: .ignore) - .accessibilityLabel(ProfileLocalization.terms) - Rectangle() - .frame(height: 1) - .foregroundColor(Theme.Colors.textSecondary) - } - - if let privacy = viewModel.config.agreement.privacyPolicyURL { - Button(action: { - viewModel.trackPrivacyPolicyClicked() - UIApplication.shared.open(privacy) - }, label: { - HStack { - Text(ProfileLocalization.privacy) - Spacer() - Image(systemName: "chevron.right") - } - }) - .buttonStyle(PlainButtonStyle()) - .foregroundColor(.primary) - .accessibilityElement(children: .ignore) - .accessibilityLabel(ProfileLocalization.privacy) - } - - // MARK: Version - Rectangle() - .frame(height: 1) - .foregroundColor(Theme.Colors.textSecondary) - Button(action: { - viewModel.openAppStore() - }, label: { - HStack { - VStack(alignment: .leading, spacing: 0) { - HStack { - if viewModel.versionState == .updateRequired { - CoreAssets.warningFilled.swiftUIImage - .resizable() - .frame(width: 24, height: 24) - } - Text("\(ProfileLocalization.Settings.version) \(viewModel.currentVersion)") - } - switch viewModel.versionState { - case .actual: - HStack { - CoreAssets.checkmark.swiftUIImage - .renderingMode(.template) - .foregroundColor(.green) - Text(ProfileLocalization.Settings.upToDate) - .font(Theme.Fonts.labelMedium) - .foregroundStyle(Theme.Colors.textSecondary) - } - case .updateNeeded: - Text("\(ProfileLocalization.Settings.tapToUpdate) \(viewModel.latestVersion)") - .font(Theme.Fonts.labelMedium) - .foregroundStyle(Theme.Colors.accentColor) - case .updateRequired: - Text(ProfileLocalization.Settings.tapToInstall) - .font(Theme.Fonts.labelMedium) - .foregroundStyle(Theme.Colors.accentColor) - } - } - Spacer() - if viewModel.versionState != .actual { - Image(systemName: "arrow.up.circle") - .resizable() - .frame(width: 24, height: 24) - .foregroundStyle(Theme.Colors.accentColor) - } - - } - }).disabled(viewModel.versionState == .actual) - - }.cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - - // MARK: - Log out - VStack { - Button(action: { - viewModel.router.presentView(transitionStyle: .crossDissolve) { - AlertView( - alertTitle: ProfileLocalization.LogoutAlert.title, - alertMessage: ProfileLocalization.LogoutAlert.text, - positiveAction: CoreLocalization.Alert.accept, - onCloseTapped: { - viewModel.router.dismiss(animated: true) - }, - okTapped: { - viewModel.router.dismiss(animated: true) - Task { - await viewModel.logOut() - } - }, type: .logOut - ) - } - }, label: { - HStack { - Text(ProfileLocalization.logout) - Spacer() - Image(systemName: "rectangle.portrait.and.arrow.right") - } - }) - .accessibilityElement(children: .ignore) - .accessibilityLabel(ProfileLocalization.logout) - } - .foregroundColor(Theme.Colors.alert) - .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear) - .padding(.top, 24) - .padding(.bottom, 60) + if let updatedImage { + self.viewModel.updatedAvatar = updatedImage } - Spacer() } - } - }.accessibilityAction {} - .frameLimit(sizePortrait: 420) - .padding(.top, 8) - .onChange(of: settingsTapped, perform: { _ in - let userModel = viewModel.userModel ?? UserProfile() - viewModel.trackProfileEditClicked() - viewModel.router.showEditProfile( - userModel: userModel, - avatar: viewModel.updatedAvatar, - profileDidEdit: { updatedProfile, updatedImage in - if let updatedProfile { - self.viewModel.userModel = updatedProfile - } - if let updatedImage { - self.viewModel.updatedAvatar = updatedImage - } - } - ) - }) - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - - // MARK: - Offline mode SnackBar - OfflineSnackBarView(connectivity: viewModel.connectivity, - reloadAction: { - await viewModel.getMyProfile(withProgress: false) + ) }) - + .navigationBarHidden(false) + .navigationBarBackButtonHidden(false) + + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getMyProfile(withProgress: false) + } + ) + // MARK: - Error Alert if viewModel.showError { VStack { Spacer() SnackBarView(message: viewModel.errorMessage) } - .padding(.bottom, viewModel.connectivity.isInternetAvaliable - ? 0 : OfflineSnackBarView.height) + .padding( + .bottom, + viewModel.connectivity.isInternetAvaliable + ? 0 : OfflineSnackBarView.height + ) .transition(.move(edge: .bottom)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { @@ -315,6 +88,149 @@ public struct ProfileView: View { .ignoresSafeArea() ) } + + private var progressBar: some View { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } + + private func content() -> some View { + VStack { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } else { + UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar) + .padding(.top, 30) + Text(viewModel.userModel?.name ?? "") + .font(Theme.Fonts.headlineSmall) + .padding(.top, 20) + Text("@\(viewModel.userModel?.username ?? "")") + .font(Theme.Fonts.labelLarge) + .padding(.top, 4) + .foregroundColor(Theme.Colors.textSecondary) + .padding(.bottom, 10) + profileInfo + VStack(alignment: .leading, spacing: 14) { + settings + ProfileSupportInfoView(viewModel: viewModel) + logOutButton + } + Spacer() + } + } + } + + // MARK: - Profile Info + + @ViewBuilder + private var profileInfo: some View { + if viewModel.userModel?.yearOfBirth != 0 || viewModel.userModel?.shortBiography != "" { + VStack(alignment: .leading, spacing: 14) { + Text(ProfileLocalization.info) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + + VStack(alignment: .leading, spacing: 16) { + if viewModel.userModel?.yearOfBirth != 0 { + HStack { + Text(ProfileLocalization.Edit.Fields.yearOfBirth) + .foregroundColor(Theme.Colors.textSecondary) + Text(String(viewModel.userModel?.yearOfBirth ?? 0)) + } + } + if let bio = viewModel.userModel?.shortBiography, bio != "" { + HStack(alignment: .top) { + Text(ProfileLocalization.bio + " ") + .foregroundColor(Theme.Colors.textSecondary) + + Text(bio) + } + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel( + (viewModel.userModel?.yearOfBirth != 0 ? + ProfileLocalization.Edit.Fields.yearOfBirth + String(viewModel.userModel?.yearOfBirth ?? 0) : + "") + + (viewModel.userModel?.shortBiography != nil ? + ProfileLocalization.bio + (viewModel.userModel?.shortBiography ?? "") : + "") + ) + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + }.padding(.bottom, 16) + } + } + + // MARK: - Settings + + @ViewBuilder + private var settings: some View { + Text(ProfileLocalization.settings) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + VStack(alignment: .leading, spacing: 27) { + Button(action: { + viewModel.trackProfileVideoSettingsClicked() + viewModel.router.showSettings() + }, label: { + HStack { + Text(ProfileLocalization.settingsVideo) + Spacer() + Image(systemName: "chevron.right") + } + }) + + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(ProfileLocalization.settingsVideo) + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + } + + // MARK: - Log out + + private var logOutButton: some View { + VStack { + Button(action: { + viewModel.router.presentView(transitionStyle: .crossDissolve) { + AlertView( + alertTitle: ProfileLocalization.LogoutAlert.title, + alertMessage: ProfileLocalization.LogoutAlert.text, + positiveAction: CoreLocalization.Alert.accept, + onCloseTapped: { + viewModel.router.dismiss(animated: true) + }, + okTapped: { + viewModel.router.dismiss(animated: true) + Task { + await viewModel.logOut() + } + }, type: .logOut + ) + } + }, label: { + HStack { + Text(ProfileLocalization.logout) + Spacer() + Image(systemName: "rectangle.portrait.and.arrow.right") + } + }) + .accessibilityElement(children: .ignore) + .accessibilityLabel(ProfileLocalization.logout) + } + .foregroundColor(Theme.Colors.alert) + .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear) + .padding(.top, 24) + .padding(.bottom, 60) + } } #if DEBUG @@ -326,12 +242,12 @@ struct ProfileView_Previews: PreviewProvider { analytics: ProfileAnalyticsMock(), config: ConfigMock(), connectivity: Connectivity()) - + ProfileView(viewModel: vm, settingsTapped: .constant(false)) .preferredColorScheme(.light) .previewDisplayName("DiscoveryView Light") .loadFonts() - + ProfileView(viewModel: vm, settingsTapped: .constant(false)) .preferredColorScheme(.dark) .previewDisplayName("DiscoveryView Dark") @@ -341,10 +257,10 @@ struct ProfileView_Previews: PreviewProvider { #endif struct UserAvatar: View { - + private var url: URL? @Binding private var image: UIImage? - + init(url: String, image: Binding) { if let rightUrl = URL(string: url) { self.url = rightUrl @@ -353,7 +269,7 @@ struct UserAvatar: View { } self._image = image } - + var body: some View { ZStack { Circle() diff --git a/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift new file mode 100644 index 000000000..39485ab72 --- /dev/null +++ b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift @@ -0,0 +1,195 @@ +// +// ProfileSupportInfo.swift +// Profile +// +// Created by Eugene Yatsenko on 13.12.2023. +// + +import SwiftUI +import Theme +import Core + +struct ProfileSupportInfoView: View { + + struct LinkViewModel { + let url: URL + let title: String + } + + @ObservedObject var viewModel: ProfileViewModel + + var body: some View { + Text(ProfileLocalization.supportInfo) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + VStack(alignment: .leading, spacing: 24) { + viewModel.contactSupport().map(supportInfo) + viewModel.config.agreement.tosURL.map(terms) + viewModel.config.agreement.privacyPolicyURL.map(privacy) + viewModel.config.agreement.cookiePolicyURL.map(cookiePolicy) + viewModel.config.agreement.dataSellContentURL.map(dataSellContent) + viewModel.config.faq.map(faq) + version + } + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + } + + private func supportInfo(url: URL) -> some View { + button( + linkViewModel: .init( + url: url, + title: ProfileLocalization.contact + ), + isEmailSupport: true + ) + + } + + private func terms(url: URL) -> some View { + navigationLink( + viewModel: .init( + url: url, + title: ProfileLocalization.terms + ) + ) + } + + private func privacy(url: URL) -> some View { + navigationLink( + viewModel: .init( + url: url, + title: ProfileLocalization.privacy + ) + ) + } + + private func cookiePolicy(url: URL) -> some View { + navigationLink( + viewModel: .init( + url: url, + title: ProfileLocalization.cookiePolicy + ) + ) + } + + private func dataSellContent(url: URL) -> some View { + navigationLink( + viewModel: .init( + url: url, + title: ProfileLocalization.doNotSellInformation + ) + ) + } + + private func faq(url: URL) -> some View { + button( + linkViewModel: .init( + url: url, + title: ProfileLocalization.faq + ) + ) + } + + @ViewBuilder + private func navigationLink(viewModel: LinkViewModel) -> some View { + NavigationLink { + WebBrowser( + url: viewModel.url.absoluteString, + pageTitle: viewModel.title, + showProgress: true + ) + } label: { + HStack { + Text(viewModel.title) + .multilineTextAlignment(.leading) + Spacer() + Image(systemName: "chevron.right") + } + } + .foregroundColor(.primary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(viewModel.title) + Rectangle() + .frame(height: 1) + .foregroundColor(Theme.Colors.textSecondary) + } + + @ViewBuilder + private func button(linkViewModel: LinkViewModel, isEmailSupport: Bool = false) -> some View { + Button { + guard UIApplication.shared.canOpenURL(linkViewModel.url) else { + viewModel.errorMessage = isEmailSupport ? + ProfileLocalization.Error.cannotSendEmail : + CoreLocalization.Error.unknownError + return + } + if isEmailSupport { + viewModel.trackEmailSupportClicked() + } + UIApplication.shared.open(linkViewModel.url) + } label: { + HStack { + Text(linkViewModel.title) + Spacer() + Image(systemName: "chevron.right") + } + } + .foregroundColor(.primary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(linkViewModel.title) + Rectangle() + .frame(height: 1) + .foregroundColor(Theme.Colors.textSecondary) + } + + @ViewBuilder + private var version: some View { + Button(action: { + viewModel.openAppStore() + }, label: { + HStack { + VStack(alignment: .leading, spacing: 0) { + HStack { + if viewModel.versionState == .updateRequired { + CoreAssets.warningFilled.swiftUIImage + .resizable() + .frame(width: 24, height: 24) + } + Text("\(ProfileLocalization.Settings.version) \(viewModel.currentVersion)") + } + switch viewModel.versionState { + case .actual: + HStack { + CoreAssets.checkmark.swiftUIImage + .renderingMode(.template) + .foregroundColor(.green) + Text(ProfileLocalization.Settings.upToDate) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textSecondary) + } + case .updateNeeded: + Text("\(ProfileLocalization.Settings.tapToUpdate) \(viewModel.latestVersion)") + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.accentColor) + case .updateRequired: + Text(ProfileLocalization.Settings.tapToInstall) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.accentColor) + } + } + Spacer() + if viewModel.versionState != .actual { + Image(systemName: "arrow.up.circle") + .resizable() + .frame(width: 24, height: 24) + .foregroundStyle(Theme.Colors.accentColor) + } + + } + }).disabled(viewModel.versionState == .actual) + } + +} diff --git a/Profile/Profile/SwiftGen/Strings.swift b/Profile/Profile/SwiftGen/Strings.swift index 8ff79c726..52019109a 100644 --- a/Profile/Profile/SwiftGen/Strings.swift +++ b/Profile/Profile/SwiftGen/Strings.swift @@ -14,8 +14,14 @@ public enum ProfileLocalization { public static let bio = ProfileLocalization.tr("Localizable", "BIO", fallback: "Bio:") /// Contact support public static let contact = ProfileLocalization.tr("Localizable", "CONTACT", fallback: "Contact support") + /// Cookie policy + public static let cookiePolicy = ProfileLocalization.tr("Localizable", "COOKIE_POLICY", fallback: "Cookie policy") + /// Do not sell my personal information + public static let doNotSellInformation = ProfileLocalization.tr("Localizable", "DO_NOT_SELL_INFORMATION", fallback: "Do not sell my personal information") /// Edit profile public static let editProfile = ProfileLocalization.tr("Localizable", "EDIT_PROFILE", fallback: "Edit profile") + /// FAQ + public static let faq = ProfileLocalization.tr("Localizable", "FAQ", fallback: "FAQ") /// full profile public static let fullProfile = ProfileLocalization.tr("Localizable", "FULL_PROFILE", fallback: "full profile") /// Profile info @@ -24,8 +30,8 @@ public enum ProfileLocalization { public static let limitedProfile = ProfileLocalization.tr("Localizable", "LIMITED_PROFILE", fallback: "limited profile") /// Log out public static let logout = ProfileLocalization.tr("Localizable", "LOGOUT", fallback: "Log out") - /// Privacy and policy - public static let privacy = ProfileLocalization.tr("Localizable", "PRIVACY", fallback: "Privacy and policy") + /// Privacy policy + public static let privacy = ProfileLocalization.tr("Localizable", "PRIVACY", fallback: "Privacy policy") /// Settings public static let settings = ProfileLocalization.tr("Localizable", "SETTINGS", fallback: "Settings") /// Video settings @@ -97,6 +103,10 @@ public enum ProfileLocalization { public static let yearOfBirth = ProfileLocalization.tr("Localizable", "EDIT.FIELDS.YEAR_OF_BIRTH", fallback: "Year of birth") } } + public enum Error { + /// Cannot send email. It seems your email client is not set up. + public static let cannotSendEmail = ProfileLocalization.tr("Localizable", "ERROR.CANNOT_SEND_EMAIL", fallback: "Cannot send email. It seems your email client is not set up.") + } public enum LogoutAlert { /// Are you sure you want to log out? public static let text = ProfileLocalization.tr("Localizable", "LOGOUT_ALERT.TEXT", fallback: "Are you sure you want to log out?") diff --git a/Profile/Profile/en.lproj/Localizable.strings b/Profile/Profile/en.lproj/Localizable.strings index c626255de..80feaed87 100644 --- a/Profile/Profile/en.lproj/Localizable.strings +++ b/Profile/Profile/en.lproj/Localizable.strings @@ -16,7 +16,11 @@ "SUPPORT_INFO" = "Support info"; "CONTACT" = "Contact support"; "TERMS" = "Terms of use"; -"PRIVACY" = "Privacy and policy"; +"PRIVACY" = "Privacy policy"; +"COOKIE_POLICY" = "Cookie policy"; +"DO_NOT_SELL_INFORMATION" = "Do not sell my personal information"; +"FAQ" = "FAQ"; + "LOGOUT" = "Log out"; "SWITCH_TO" = "Switch to"; "FULL_PROFILE" = "full profile"; @@ -73,3 +77,5 @@ "SETTINGS.UP_TO_DATE" = "Up-to-date"; "SETTINGS.TAP_TO_UPDATE" = "Tap to update to version"; "SETTINGS.TAP_TO_INSTALL" = "Tap to install required app update"; + +"ERROR.CANNOT_SEND_EMAIL" = "Cannot send email. It seems your email client is not set up."; diff --git a/Profile/Profile/uk.lproj/Localizable.strings b/Profile/Profile/uk.lproj/Localizable.strings index 8b5df15fd..b3f91c924 100644 --- a/Profile/Profile/uk.lproj/Localizable.strings +++ b/Profile/Profile/uk.lproj/Localizable.strings @@ -17,6 +17,9 @@ "CONTACT" = "Cлужби підтримки"; "TERMS" = "Умови використання"; "PRIVACY" = "Політика конфіденційності"; +"COOKIE_POLICY" = "Cookie policy"; +"DO_NOT_SELL_INFORMATION" = "Do not sell my personal information"; +"FAQ" = "FAQ"; "LOGOUT" = "Вийти"; "SWITCH_TO" = "Переключити на"; "FULL_PROFILE" = "повний профіль"; @@ -73,3 +76,5 @@ "SETTINGS.UP_TO_DATE" = "Оновлено"; "SETTINGS.TAP_TO_UPDATE" = "Клацніть, щоб оновити до версії"; "SETTINGS.TAP_TO_INSTALL" = "Клацніть, щоб встановити обов'язкове оновлення програми"; + +"ERROR.CANNOT_SEND_EMAIL" = "Cannot send email. It seems your email client is not set up.";