diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 9a85622bdc..d74e103be6 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -181,7 +181,6 @@ 3D15ACEE2155117000F725D5 /* MWMObjectsCategorySelectorDataSource.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D15ACED2155117000F725D5 /* MWMObjectsCategorySelectorDataSource.mm */; }; 3D2D79BA2C7C508E0062BC3D /* SingleEntityCoreDataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79B92C7C508E0062BC3D /* SingleEntityCoreDataController.swift */; }; 3D2D79BC2C7C5E300062BC3D /* ProfileRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79BB2C7C5E300062BC3D /* ProfileRepositoryImpl.swift */; }; - 3D2D79C12C7C7EA00062BC3D /* PersonalData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79BF2C7C7EA00062BC3D /* PersonalData.xcdatamodeld */; }; 3D2D79C32C7C80E60062BC3D /* PersonalDataPersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79C22C7C80E60062BC3D /* PersonalDataPersistenceController.swift */; }; 3D2D79CC2C7C8C350062BC3D /* ProfileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79CB2C7C8C350062BC3D /* ProfileService.swift */; }; 3D2D79D32C7CF4F70062BC3D /* PersonalDataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79D22C7CF4F70062BC3D /* PersonalDataViewController.swift */; }; @@ -191,7 +190,6 @@ 3D2D79DB2C7D15410062BC3D /* SecondaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79DA2C7D15410062BC3D /* SecondaryButton.swift */; }; 3D2D79DD2C7DE34B0062BC3D /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79DC2C7DE34B0062BC3D /* PhotoPickerView.swift */; }; 3D585BF42C760850005DF71F /* UIScreenExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D585BF32C760850005DF71F /* UIScreenExtensions.swift */; }; - 3D585BFA2C768C3A005DF71F /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D585BF92C768C3A005DF71F /* ToastView.swift */; }; 3DA3FC992C75ED2A0065E4D6 /* changeTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA3FC982C75ED2A0065E4D6 /* changeTheme.swift */; }; 3DBD7BE42425015C00ED9FE8 /* ParntersStyleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD7BE32425015C00ED9FE8 /* ParntersStyleSheet.swift */; }; 3DEE1AEB21F72CD300054A91 /* MWMPowerManagmentViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3DEE1AEA21F72CD300054A91 /* MWMPowerManagmentViewController.mm */; }; @@ -304,7 +302,9 @@ 527D5E822C60EFEE00736A85 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527D5E812C60EFEE00736A85 /* UIViewExtensions.swift */; }; 528D72A12C5BBBF700D53210 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 528D72A02C5BBBF700D53210 /* Colors.xcassets */; }; 5292123D2C7359FC007B97E1 /* CountryPickerView in Frameworks */ = {isa = PBXBuildFile; productRef = 5292123C2C7359FC007B97E1 /* CountryPickerView */; }; - 529212422C735A61007B97E1 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 529212412C735A61007B97E1 /* SDWebImageSwiftUI */; }; + 529A5F0A2C858F82004FE4A1 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 529A5F092C858F82004FE4A1 /* SDWebImageSwiftUI */; }; + 529A5F162C8595BB004FE4A1 /* PersonalData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F112C859535004FE4A1 /* PersonalData.xcdatamodeld */; }; + 529A5F192C85BFF0004FE4A1 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F182C85BFF0004FE4A1 /* ToastView.swift */; }; 52B573EC2C61E1C10047FAC9 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B573EB2C61E1C10047FAC9 /* SignInViewController.swift */; }; 52B573F02C61E4110047FAC9 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B573EF2C61E4110047FAC9 /* Constants.swift */; }; 52B573F22C61E8980047FAC9 /* SignUpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B573F12C61E8980047FAC9 /* SignUpViewController.swift */; }; @@ -1149,7 +1149,6 @@ 3D15ACEF2155118800F725D5 /* MWMObjectsCategorySelectorDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMObjectsCategorySelectorDataSource.h; sourceTree = ""; }; 3D2D79B92C7C508E0062BC3D /* SingleEntityCoreDataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleEntityCoreDataController.swift; sourceTree = ""; }; 3D2D79BB2C7C5E300062BC3D /* ProfileRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRepositoryImpl.swift; sourceTree = ""; }; - 3D2D79C02C7C7EA00062BC3D /* PersonalData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = PersonalData.xcdatamodel; sourceTree = ""; }; 3D2D79C22C7C80E60062BC3D /* PersonalDataPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalDataPersistenceController.swift; sourceTree = ""; }; 3D2D79CB2C7C8C350062BC3D /* ProfileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileService.swift; sourceTree = ""; }; 3D2D79D22C7CF4F70062BC3D /* PersonalDataViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalDataViewController.swift; sourceTree = ""; }; @@ -1159,7 +1158,6 @@ 3D2D79DA2C7D15410062BC3D /* SecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryButton.swift; sourceTree = ""; }; 3D2D79DC2C7DE34B0062BC3D /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = ""; }; 3D585BF32C760850005DF71F /* UIScreenExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScreenExtensions.swift; sourceTree = ""; }; - 3D585BF92C768C3A005DF71F /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ToastView.swift; path = ../../../../../../../../Documents/ToastView.swift; sourceTree = ""; }; 3DA3FC982C75ED2A0065E4D6 /* changeTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = changeTheme.swift; sourceTree = ""; }; 3DBD7BE32425015C00ED9FE8 /* ParntersStyleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParntersStyleSheet.swift; sourceTree = ""; }; 3DEE1AE921F72CD300054A91 /* MWMPowerManagmentViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMPowerManagmentViewController.h; sourceTree = ""; }; @@ -1303,6 +1301,8 @@ 527D5E7E2C60E69C00736A85 /* Layouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layouting.swift; sourceTree = ""; }; 527D5E812C60EFEE00736A85 /* UIViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; 528D72A02C5BBBF700D53210 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; + 529A5F122C859535004FE4A1 /* PersonalData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = PersonalData.xcdatamodel; sourceTree = ""; }; + 529A5F182C85BFF0004FE4A1 /* ToastView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; 52B573EB2C61E1C10047FAC9 /* SignInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; 52B573EF2C61E4110047FAC9 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 52B573F12C61E8980047FAC9 /* SignUpViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpViewController.swift; sourceTree = ""; }; @@ -1894,7 +1894,7 @@ FA853BDB26BC54CD0026D455 /* libtraffic.a in Frameworks */, FA853BD926BC54C80026D455 /* libexpat.a in Frameworks */, FA853BD726BC54650026D455 /* libpugixml.a in Frameworks */, - 529212422C735A61007B97E1 /* SDWebImageSwiftUI in Frameworks */, + 529A5F0A2C858F82004FE4A1 /* SDWebImageSwiftUI in Frameworks */, FA853BD526BC545D0026D455 /* libagg.a in Frameworks */, FA853BD326BC54530026D455 /* libtransit.a in Frameworks */, FA853BA926BC3B8A0026D455 /* libbase.a in Frameworks */, @@ -2718,14 +2718,6 @@ path = Buttons; sourceTree = ""; }; - 3D585BF82C768C2C005DF71F /* Toast */ = { - isa = PBXGroup; - children = ( - 3D585BF92C768C3A005DF71F /* ToastView.swift */, - ); - path = Toast; - sourceTree = ""; - }; 447DB4B72BA7826D000DF4C2 /* ReauthAlert */ = { isa = PBXGroup; children = ( @@ -3089,7 +3081,7 @@ 527D5E762C60D92900736A85 /* Components */ = { isa = PBXGroup; children = ( - 3D585BF82C768C2C005DF71F /* Toast */, + 529A5F172C85BF99004FE4A1 /* ToastView */, 3D585BF72C768BED005DF71F /* Buttons */, 52522F442C6DFD220015709C /* Nav */, 52B573F32C61F10B0047FAC9 /* TextFields */, @@ -3129,6 +3121,14 @@ path = Theme; sourceTree = ""; }; + 529A5F172C85BF99004FE4A1 /* ToastView */ = { + isa = PBXGroup; + children = ( + 529A5F182C85BFF0004FE4A1 /* ToastView.swift */, + ); + path = ToastView; + sourceTree = ""; + }; 52B189972C53B9E900B5B6F9 /* Home */ = { isa = PBXGroup; children = ( @@ -3213,8 +3213,8 @@ 52ED91A02C72007C000EE25B /* DataModels */ = { isa = PBXGroup; children = ( + 529A5F112C859535004FE4A1 /* PersonalData.xcdatamodeld */, 52ED91A12C7200C4000EE25B /* Currency.xcdatamodeld */, - 3D2D79BF2C7C7EA00062BC3D /* PersonalData.xcdatamodeld */, ); path = DataModels; sourceTree = ""; @@ -4373,7 +4373,7 @@ name = OMaps; packageProductDependencies = ( 5292123C2C7359FC007B97E1 /* CountryPickerView */, - 529212412C735A61007B97E1 /* SDWebImageSwiftUI */, + 529A5F092C858F82004FE4A1 /* SDWebImageSwiftUI */, ); productName = Maps; productReference = 6741AA5D1BF340DE002C974C /* Organic Maps (Debug).app */; @@ -4473,7 +4473,7 @@ mainGroup = 29B97314FDCFA39411CA2CEA /* Maps */; packageReferences = ( 5292123B2C7359FC007B97E1 /* XCRemoteSwiftPackageReference "CountryPickerView" */, - 529212402C735A61007B97E1 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, + 529A5F082C858F82004FE4A1 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, ); productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; @@ -4738,6 +4738,7 @@ 34AB665F1FC5AA330078E451 /* TransportTransitIntermediatePoint.swift in Sources */, 34B846A82029E8110081ECCD /* BMCDefaultViewModel.swift in Sources */, 993DF12123F6BDB100AC231A /* UIViewControllerRenderer.swift in Sources */, + 529A5F192C85BFF0004FE4A1 /* ToastView.swift in Sources */, 34D3AFF61E37A36A004100F9 /* UICollectionView+Cells.swift in Sources */, 4767CDA420AAF66B00BD8166 /* NSAttributedString+HTML.swift in Sources */, 6741A9A91BF340DE002C974C /* MWMDefaultAlert.mm in Sources */, @@ -4833,6 +4834,7 @@ ED79A5D42BDF8D6100952D1F /* MetadataItem.swift in Sources */, 34AB667D1FC5AA330078E451 /* MWMRoutePreview.mm in Sources */, 993DF11B23F6BDB100AC231A /* UIViewRenderer.swift in Sources */, + 529A5F162C8595BB004FE4A1 /* PersonalData.xcdatamodeld in Sources */, 99C964302428C27A00E41723 /* PlacePageHeaderView.swift in Sources */, 9989273A2449E60200260CE2 /* BottomMenuViewController.swift in Sources */, 47E3C72D2111E6A2008B3B27 /* FadeTransitioning.swift in Sources */, @@ -4917,7 +4919,6 @@ 990128562449A82500C72B10 /* BottomTabBarView.swift in Sources */, F6E2FD711E097BA00083EBEC /* MWMMapDownloaderTableViewCell.m in Sources */, F6E2FE4F1E097BA00083EBEC /* MWMActionBarButton.m in Sources */, - 3D2D79C12C7C7EA00062BC3D /* PersonalData.xcdatamodeld in Sources */, 47F86CFF20C936FC00FEE291 /* TabView.swift in Sources */, 5260D3CE2C64F60200C673B4 /* APIEndpoints.swift in Sources */, 34AB66741FC5AA330078E451 /* BaseRoutePreviewStatus.swift in Sources */, @@ -4974,7 +4975,6 @@ 3404F4992028A20D0090E401 /* BMCCategoryCell.swift in Sources */, F62607FD207B790300176C5A /* SpinnerAlert.swift in Sources */, 3444DFD21F17620C00E73099 /* MWMMapWidgetsHelper.mm in Sources */, - 3D585BFA2C768C3A005DF71F /* ToastView.swift in Sources */, 3472B5E1200F86C800DC6CD5 /* MWMEditorHelper.mm in Sources */, 3D2D79CC2C7C8C350062BC3D /* ProfileService.swift in Sources */, 99F3EB1123F418C900C713F8 /* PlacePageBuilder.swift in Sources */, @@ -5618,12 +5618,12 @@ minimumVersion = 3.0.0; }; }; - 529212402C735A61007B97E1 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { + 529A5F082C858F82004FE4A1 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 3.0.0; + kind = exactVersion; + version = 2.0.2; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -5634,22 +5634,21 @@ package = 5292123B2C7359FC007B97E1 /* XCRemoteSwiftPackageReference "CountryPickerView" */; productName = CountryPickerView; }; - 529212412C735A61007B97E1 /* SDWebImageSwiftUI */ = { + 529A5F092C858F82004FE4A1 /* SDWebImageSwiftUI */ = { isa = XCSwiftPackageProductDependency; - package = 529212402C735A61007B97E1 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; + package = 529A5F082C858F82004FE4A1 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; productName = SDWebImageSwiftUI; }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ - 3D2D79BF2C7C7EA00062BC3D /* PersonalData.xcdatamodeld */ = { + 529A5F112C859535004FE4A1 /* PersonalData.xcdatamodeld */ = { isa = XCVersionGroup; children = ( - 3D2D79C02C7C7EA00062BC3D /* PersonalData.xcdatamodel */, + 529A5F122C859535004FE4A1 /* PersonalData.xcdatamodel */, ); - currentVersion = 3D2D79C02C7C7EA00062BC3D /* PersonalData.xcdatamodel */; - name = PersonalData.xcdatamodeld; - path = /Users/llcrebus/Projects/Tourism/iphone/Maps/Tourism/Data/Db/DataModels/PersonalData.xcdatamodeld; + currentVersion = 529A5F122C859535004FE4A1 /* PersonalData.xcdatamodel */; + path = PersonalData.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; diff --git a/iphone/Maps/Tourism/Data/Db/DataModels/PersonalData.swift b/iphone/Maps/Tourism/Data/Db/DataModels/PersonalData.swift deleted file mode 100644 index e92d715f59..0000000000 --- a/iphone/Maps/Tourism/Data/Db/DataModels/PersonalData.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// PersonalData.swift -// OMaps -// -// Created by LLC Rebus on 26/08/24. -// Copyright © 2024 Organic Maps. All rights reserved. -// - -import Foundation diff --git a/iphone/Maps/Tourism/Data/Network/Services/ProfileService.swift b/iphone/Maps/Tourism/Data/Network/Services/ProfileService.swift index 3b68a9d89d..50e26f33af 100644 --- a/iphone/Maps/Tourism/Data/Network/Services/ProfileService.swift +++ b/iphone/Maps/Tourism/Data/Network/Services/ProfileService.swift @@ -8,7 +8,7 @@ protocol ProfileService { func updateProfile( fullName: String, country: String, - email: String, + email: String?, pfpUrl: UIImage? ) -> AnyPublisher @@ -18,6 +18,12 @@ protocol ProfileService { } class ProfileServiceImpl: ProfileService { + let userPreferences: UserPreferences + + init(userPreferences: UserPreferences) { + self.userPreferences = userPreferences + } + func getPersonalData() -> AnyPublisher { return CombineNetworkHelper.get(path: APIEndpoints.getUserUrl) } @@ -25,17 +31,87 @@ class ProfileServiceImpl: ProfileService { func updateProfile( fullName: String, country: String, - email: String, + email: String?, pfpUrl: UIImage? ) -> AnyPublisher { - let body = createMultipartFormData(fullName: fullName, country: country, email: email, pfpUrl: pfpUrl) + var parameters = [ + [ + "key": "full_name", + "value": fullName, + "type": "text" + ], + [ + "key": "country", + "value": country, + "type": "text" + ], + [ + "key": "_method", + "value": "PUT", + "type": "text" + ]] as [[String: Any]] - let boundary = UUID().uuidString - let headers = ["Content-Type": "multipart/form-data; boundary=\(boundary)"] + if let newEmail = email { + parameters.append([ + "key": "email", + "value": newEmail, + "type": "text"]) + } - return CombineNetworkHelper.post(path: APIEndpoints.updateProfileUrl, body: body, headers: headers) + let theme = userPreferences.getTheme() + parameters.append([ + "key": "theme", + "value": theme?.code ?? "light", + "type": "text"]) + + let language = userPreferences.getLanguage() + parameters.append([ + "key": "language", + "value": language?.code ?? "ru", + "type": "text"]) + + let boundary = "Boundary-\(UUID().uuidString)" + var body = Data() + + for param in parameters { + let paramName = param["key"] as! String + body += Data("--\(boundary)\r\n".utf8) + body += Data("Content-Disposition: form-data; name=\"\(paramName)\"\r\n\r\n".utf8) + body += Data("\(param["value"] as! String)\r\n".utf8) + } + + // Add image file data if it exists + if let image = pfpUrl, let imageData = image.jpegData(compressionQuality: 0.01) { + body += Data("--\(boundary)\r\n".utf8) + body += Data("Content-Disposition: form-data; name=\"avatar\"; filename=\"avatar.jpg\"\r\n".utf8) + body += Data("Content-Type: image/jpeg\r\n\r\n".utf8) + body += imageData + body += Data("\r\n".utf8) + } + + body += Data("--\(boundary)--\r\n".utf8) + + var request = URLRequest(url: URL(string: "https://product.rebus.tj/api/profile")!, timeoutInterval: Double.infinity) + request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + if let token = userPreferences.getToken() { + request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + + request.httpMethod = "POST" + request.httpBody = body + + return URLSession.shared.dataTaskPublisher(for: request) + .tryMap { data, response in + try CombineNetworkHelper.handleResponse(data: data, response: response) + } + .mapError { error in + CombineNetworkHelper.handleMappingError(error) + } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() } - + func updateLanguage(code: String) { // TODO: cmon } @@ -44,44 +120,3 @@ class ProfileServiceImpl: ProfileService { // TODO: cmon } } - - -func createMultipartFormData(fullName: String, country: String, email: String?, pfpUrl: UIImage?) -> Data { - let boundary = UUID().uuidString - var body = Data() - - let boundaryPrefix = "--\(boundary)\r\n" - - body.appendString(boundaryPrefix) - body.appendString("Content-Disposition: form-data; name=\"fullName\"\r\n\r\n") - body.appendString("\(fullName)\r\n") - - body.appendString(boundaryPrefix) - body.appendString("Content-Disposition: form-data; name=\"country\"\r\n\r\n") - body.appendString("\(country)\r\n") - - if let email = email { - body.appendString(boundaryPrefix) - body.appendString("Content-Disposition: form-data; name=\"email\"\r\n\r\n") - body.appendString("\(email)\r\n") - } - - if let image = pfpUrl, let imageData = image.jpegData(compressionQuality: 0.5) { - body.appendString(boundaryPrefix) - body.appendString("Content-Disposition: form-data; name=\"pfpUrl\"; filename=\"profile.jpg\"\r\n") - body.appendString("Content-Type: image/jpeg\r\n\r\n") - body.append(imageData) - body.appendString("\r\n") - } - - body.appendString("--\(boundary)--\r\n") - return body -} - -extension Data { - mutating func appendString(_ string: String) { - if let data = string.data(using: .utf8) { - append(data) - } - } -} diff --git a/iphone/Maps/Tourism/Data/Network/Utils/CombineNetworkHelper.swift b/iphone/Maps/Tourism/Data/Network/Utils/CombineNetworkHelper.swift index e477453832..b08471f8c8 100644 --- a/iphone/Maps/Tourism/Data/Network/Utils/CombineNetworkHelper.swift +++ b/iphone/Maps/Tourism/Data/Network/Utils/CombineNetworkHelper.swift @@ -1,9 +1,6 @@ import Foundation import Combine -// EminoFire is a kind of "library" for the abstraction of http code. -// It is named after the inventor of this piece - Emin - class CombineNetworkHelper { // MARK: - Lower level code static func createRequest(url: URL, method: String, headers: [String: String] = [:], body: Data? = nil) -> URLRequest { @@ -102,6 +99,14 @@ class CombineNetworkHelper { return Fail(error: ResourceError.other(message: "Encoding error: \(error)")).eraseToAnyPublisher() } } + + static func postt(path: String, body: Data, headers: [String: String] = [:], decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher { + guard let url = URL(string: path) else { + print("Invalid url") + return Fail(error: ResourceError.other(message: "Invalid url")).eraseToAnyPublisher() + } + return performRequest(url: url, method: "POST", body: body, headers: headers, decoder: decoder) + } static func postWithoutBody(path: String, headers: [String: String] = [:], decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher { guard let url = URL(string: path) else { diff --git a/iphone/Maps/Tourism/Data/Repositories/ProfileRepositoryImpl.swift b/iphone/Maps/Tourism/Data/Repositories/ProfileRepositoryImpl.swift index 2142f72338..7d757cbc58 100644 --- a/iphone/Maps/Tourism/Data/Repositories/ProfileRepositoryImpl.swift +++ b/iphone/Maps/Tourism/Data/Repositories/ProfileRepositoryImpl.swift @@ -57,7 +57,7 @@ class ProfileRepositoryImpl: ProfileRepository { func updateProfile( fullName: String, country: String, - email: String, + email: String?, pfpUrl: UIImage? ) -> AnyPublisher { return profileService.updateProfile( diff --git a/iphone/Maps/Tourism/Domain/Repositories/ProfileRepository.swift b/iphone/Maps/Tourism/Domain/Repositories/ProfileRepository.swift index 6ffa35248c..6cc60120f9 100644 --- a/iphone/Maps/Tourism/Domain/Repositories/ProfileRepository.swift +++ b/iphone/Maps/Tourism/Domain/Repositories/ProfileRepository.swift @@ -9,7 +9,7 @@ protocol ProfileRepository { func updateProfile( fullName: String, country: String, - email: String, + email: String?, pfpUrl: UIImage? ) -> AnyPublisher diff --git a/iphone/Maps/Tourism/Presentation/Auth/Screens/SignInViewController.swift b/iphone/Maps/Tourism/Presentation/Auth/Screens/SignInViewController.swift index 7790ae3da9..9d760ada47 100644 --- a/iphone/Maps/Tourism/Presentation/Auth/Screens/SignInViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Auth/Screens/SignInViewController.swift @@ -101,7 +101,7 @@ class SignInViewController: UIViewController { // Back Button backButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), - backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), // Container View containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), diff --git a/iphone/Maps/Tourism/Presentation/Auth/Screens/SignUpViewController.swift b/iphone/Maps/Tourism/Presentation/Auth/Screens/SignUpViewController.swift index 04e46a73e3..849f20de7c 100644 --- a/iphone/Maps/Tourism/Presentation/Auth/Screens/SignUpViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Auth/Screens/SignUpViewController.swift @@ -56,7 +56,11 @@ class SignUpViewController: UIViewController { return textField }() - private let cpv: CountryPickerView = getCountryPickerView() + private let cpv: CountryPickerView = { + let cpv = getCountryPickerView() + cpv.textColor = .white + return cpv + }() private let underline: UIView = { let underline = UIView() @@ -131,12 +135,12 @@ class SignUpViewController: UIViewController { // Back Button backButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), - backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), // Container View containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -60), + containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -32), // Blur View blurView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), diff --git a/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift b/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift index 58c7ae83e1..a56e770b58 100644 --- a/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift +++ b/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift @@ -9,9 +9,8 @@ struct LoadImageView: View { var body: some View { if let urlString = url { let errorImage = Image(systemName: "error_centered") - WebImage(url: URL(string: urlString)) { image in - image.image?.resizable() - } + WebImage(url: URL(string: urlString)) + .resizable() .onSuccess(perform: { image, data, cacheType in isError = false }) diff --git a/iphone/Maps/Tourism/Presentation/Components/ToastView/ToastView.swift b/iphone/Maps/Tourism/Presentation/Components/ToastView/ToastView.swift new file mode 100644 index 0000000000..4309b9092d --- /dev/null +++ b/iphone/Maps/Tourism/Presentation/Components/ToastView/ToastView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct ToastView: View { + let message: String + @Binding var isPresented: Bool + + var body: some View { + VStack { + Text(message) + .padding() + .foregroundColor(Color.onSurface) + .background(Color.surface) + .cornerRadius(10) + .shadow(radius: 5) + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation { + isPresented = false + } + } + } + } +} diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PersonalDataViewController.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PersonalDataViewController.swift index 63ecd91670..0fba997593 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PersonalDataViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PersonalDataViewController.swift @@ -27,7 +27,7 @@ struct PersonalDataScreen: View { @ObservedObject var profileVM: ProfileViewModel @Environment(\.presentationMode) var presentationMode: Binding - @State private var showSheet = false + @State private var showPhotoPicker = false var body: some View { ScrollView { @@ -55,7 +55,7 @@ struct PersonalDataScreen: View { Spacer().frame(width: 32) - // upload photo button + // photo picker Group { Image(systemName: "photo.badge.arrow.down") .foregroundColor(Color.onBackground) @@ -65,7 +65,7 @@ struct PersonalDataScreen: View { .textStyle(TextStyle.h4) } .onTapGesture { - showSheet = true + showPhotoPicker = true } } @@ -97,15 +97,24 @@ struct PersonalDataScreen: View { PrimaryButton( label: L("save"), onClick: { - + profileVM.save() } ) } .padding() - .sheet(isPresented: $showSheet) { + .sheet(isPresented: $showPhotoPicker) { ImagePicker(sourceType: .photoLibrary, selectedImage: $profileVM.pfpToUpload) } } + .overlay( + Group { + if profileVM.shouldShowMessage { + ToastView(message: profileVM.messageToShow, isPresented: $profileVM.shouldShowMessage) + .padding(.bottom) + } + }, + alignment: .bottom + ) .background(Color.background) } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewController.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewController.swift index 5d2264b33f..043354fcef 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewController.swift @@ -11,7 +11,7 @@ class ProfileViewController: UIViewController { currencyPersistenceController: CurrencyPersistenceController.shared ), profileRepository: ProfileRepositoryImpl( - personalDataService: ProfileServiceImpl(), + personalDataService: ProfileServiceImpl(userPreferences: UserPreferences.shared), personalDataPersistenceController: PersonalDataPersistenceController.shared ), authRepository: AuthRepositoryImpl(authService: AuthServiceImpl()), @@ -37,7 +37,6 @@ struct ProfileScreen: View { @State private var signOutLoading = false @State private var navigateToPersonalData = false - @State private var showToast = false func onLanguageClick () { navigateToLanguageSettings() @@ -58,51 +57,41 @@ struct ProfileScreen: View { } VerticalSpace(height: 32) - if let currencyRates = profileVM.currencyRates { - CurrencyRatesView(currencyRates: currencyRates) - VerticalSpace(height: 20) + VStack(spacing: 20) { + if let currencyRates = profileVM.currencyRates { + CurrencyRatesView(currencyRates: currencyRates) + } + + GenericProfileItem( + label: L("personal_data"), + icon: "person.circle", + onClick: { + onPersonalDataClick() + } + ) + + GenericProfileItem( + label: L("language"), + icon: "globe", + onClick: { + onLanguageClick() + } + ) + + ThemeSwitch(themeViewModel: themeVM) + + GenericProfileItem( + label: L("sign_out"), + icon: "rectangle.portrait.and.arrow.right", + isLoading: signOutLoading, + onClick: { + isSheetOpen = true + } + ) } - - GenericProfileItem( - label: L("personal_data"), - icon: "person.circle", - onClick: { - onPersonalDataClick() - } - ) - VerticalSpace(height: 20) - - GenericProfileItem( - label: L("language"), - icon: "globe", - onClick: { - onLanguageClick() - } - ) - VerticalSpace(height: 20) - ThemeSwitch(themeViewModel: themeVM) - VerticalSpace(height: 20) - - GenericProfileItem( - label: L("sign_out"), - icon: "rectangle.portrait.and.arrow.right", - isLoading: signOutLoading, - onClick: { - isSheetOpen = true - } - ) } .padding(16) } - .overlay( - Group { - if showToast { - ToastView(message: "This is a toast message", isPresented: $showToast) - .padding(.bottom) - } - }, - alignment: .bottom - ) .sheet(isPresented: $isSheetOpen) { SignOutWarning( onSignOutClick: { diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewModel.swift index 479662892a..1f16db2d5d 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/ProfileViewModel.swift @@ -7,14 +7,17 @@ class ProfileViewModel: ObservableObject { private let profileRepository: ProfileRepository private let authRepository: AuthRepository private let userPreferences: UserPreferences - var onMessageToUserRequested: ((String) -> Void)? = nil var onSignOutCompleted: (() -> Void)? = nil + @Published var messageToShow = "" + @Published var shouldShowMessage = false + @Published var pfpFromRemote: URL? = nil @Published var pfpToUpload = UIImage() @Published var isImagePickerUsed: Bool = false @Published var fullName: String = "" + var currentEmail: String = "" // it changes only when confirmed by server @Published var email: String = "" @Published var countryCodeName: String? = nil @Published var personalData: PersonalData? = nil @@ -65,7 +68,7 @@ class ProfileViewModel: ObservableObject { profileRepository.personalDataPassThroughSubject .sink { completion in if case let .failure(error) = completion { - self.onMessageToUserRequested?(error.errorDescription) + self.showMessage(error.errorDescription) } } receiveValue: { resource in self.personalData = resource @@ -73,6 +76,7 @@ class ProfileViewModel: ObservableObject { self.pfpFromRemote = URL(string: pfpUrl) } self.fullName = resource.fullName + self.currentEmail = resource.email self.email = resource.email self.countryCodeName = resource.country } @@ -86,20 +90,21 @@ class ProfileViewModel: ObservableObject { profileRepository.updateProfile( fullName: fullName, country: countryCodeName!, - email: email, + // We shouldn't send email field if there's no change + email: email == currentEmail ? nil : email, pfpUrl: pfpToUpload ) .sink { completion in if case let .failure(error) = completion { - self.onMessageToUserRequested?(error.errorDescription) + self.showMessage(error.errorDescription) } } receiveValue: { resource in self.updatePersonalDataInMemory(personalData: resource) - self.onMessageToUserRequested?(L("saved")) + self.showMessage(L("saved")) } .store(in: &cancellables) } else { - self.onMessageToUserRequested?(L("please_fill_all_fields")) + self.showMessage(L("please_fill_all_fields")) } } @@ -113,7 +118,7 @@ class ProfileViewModel: ObservableObject { currencyRepository.currencyPassThroughSubject .sink { completion in if case let .failure(error) = completion { - self.onMessageToUserRequested?(error.errorDescription) + self.showMessage(error.errorDescription) } } receiveValue: { resource in self.currencyRates = resource @@ -127,14 +132,19 @@ class ProfileViewModel: ObservableObject { authRepository.signOut() .sink { completion in if case let .failure(error) = completion { - self.onMessageToUserRequested?(error.errorDescription) + self.showMessage(error.errorDescription) } } receiveValue: { response in self.signOutResponse = response self.userPreferences.setToken(value: nil) self.onSignOutCompleted?() - self.onMessageToUserRequested?(response.message) + self.showMessage(response.message) } .store(in: &cancellables) } + + func showMessage(_ message: String) { + messageToShow = message + shouldShowMessage = true + } } diff --git a/iphone/Maps/Tourism/Presentation/Theme/Font.swift b/iphone/Maps/Tourism/Presentation/Theme/Font.swift index 8b82cf21cd..66dc77bdcd 100644 --- a/iphone/Maps/Tourism/Presentation/Theme/Font.swift +++ b/iphone/Maps/Tourism/Presentation/Theme/Font.swift @@ -97,9 +97,10 @@ struct TextStyle { extension Text { func textStyle(_ style: TextStyle) -> some View { - self + let calibrationFactor: CGFloat = 0.2 + return self .font(style.font) - .lineSpacing(style.lineHeight) + .lineSpacing(style.lineHeight * calibrationFactor) } }