diff --git a/PresentationLayer/Constants/ColorEnum.swift b/PresentationLayer/Constants/ColorEnum.swift index 4e1673aa3..41ea18420 100644 --- a/PresentationLayer/Constants/ColorEnum.swift +++ b/PresentationLayer/Constants/ColorEnum.swift @@ -53,7 +53,7 @@ public enum ColorEnum: String { case stationChipOnlineStateText case favoriteHeart case mapPin - case wxmWhite + case textWhite case darkBg case infoIndication case trasnparentButtonBg diff --git a/PresentationLayer/UIComponents/Screens/ClaimDevice/ClaimDeviceContainer/ClaimDeviceContainerViewModel.swift b/PresentationLayer/UIComponents/Screens/ClaimDevice/ClaimDeviceContainer/ClaimDeviceContainerViewModel.swift index 75a0dda03..ccdaf1084 100644 --- a/PresentationLayer/UIComponents/Screens/ClaimDevice/ClaimDeviceContainer/ClaimDeviceContainerViewModel.swift +++ b/PresentationLayer/UIComponents/Screens/ClaimDevice/ClaimDeviceContainer/ClaimDeviceContainerViewModel.swift @@ -196,7 +196,7 @@ extension ClaimDeviceContainerViewModel { let continueToPhotoVerificationAction: VoidCallback = { [weak self] in self?.dismissAndNavigate(device: nil) - let route = PhotoIntroViewModel.getInitialRoute() + let route = PhotoIntroViewModel.getInitialRoute(images: [], isNewPhotoVerification: true) DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { // The only way found to avoid errors with navigation stack Router.shared.navigateTo(route) } diff --git a/PresentationLayer/UIComponents/Screens/DailyRewards/Components/BoostsView.swift b/PresentationLayer/UIComponents/Screens/DailyRewards/Components/BoostsView.swift index 94e37f342..5f3d06ce8 100644 --- a/PresentationLayer/UIComponents/Screens/DailyRewards/Components/BoostsView.swift +++ b/PresentationLayer/UIComponents/Screens/DailyRewards/Components/BoostsView.swift @@ -19,18 +19,18 @@ struct BoostsView: View { HStack { Text(title) .font(.system(size: CGFloat(.normalFontSize), weight: .bold)) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Spacer() Text("+ \(rewards.toWXMTokenPrecisionString) \(StringConstants.wxmCurrency)") .font(.system(size: CGFloat(.caption), weight: .medium)) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) } HStack { Text(description) .font(.system(size: CGFloat(.caption))) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Spacer() } diff --git a/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoFactory.swift b/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoFactory.swift index 040478983..8a15ac62d 100644 --- a/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoFactory.swift +++ b/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoFactory.swift @@ -20,31 +20,34 @@ extension DeviceInfoViewModel.Field { } } - func titleFor(devie: DeviceDetails) -> String { + func titleFor(devie: DeviceDetails) -> (title: String, badge: String?) { switch self { case .name: - return LocalizableString.DeviceInfo.stationName.localized + return (LocalizableString.DeviceInfo.stationName.localized, nil) case .frequency: - return LocalizableString.DeviceInfo.stationFrequency.localized + return (LocalizableString.DeviceInfo.stationFrequency.localized, nil) case .reboot: - return LocalizableString.DeviceInfo.stationReboot.localized + return (LocalizableString.DeviceInfo.stationReboot.localized, nil) case .maintenance: #warning("TODO: Format when info is ready") - return LocalizableString.DeviceInfo.stationMaintenance("").localized + return (LocalizableString.DeviceInfo.stationMaintenance("").localized, nil) case .remove: - return LocalizableString.DeviceInfo.stationRemove.localized + return (LocalizableString.DeviceInfo.stationRemove.localized, nil) case .reconfigureWifi: - return LocalizableString.DeviceInfo.stationReconfigureWifi.localized + return (LocalizableString.DeviceInfo.stationReconfigureWifi.localized, nil) case .stationLocation: - return LocalizableString.DeviceInfo.stationLocation.localized + return (LocalizableString.DeviceInfo.stationLocation.localized, nil) case .rewardSplit: - return LocalizableString.RewardDetails.rewardSplit.localized + return (LocalizableString.RewardDetails.rewardSplit.localized, nil) case .photos: - return LocalizableString.PhotoVerification.uploadPhotos.localized + return (LocalizableString.PhotoVerification.photoVerificationIntroTitle.localized, LocalizableString.new.localized) } } - func descriptionFor(device: DeviceDetails, for followState: UserDeviceFollowState?, deviceInfo: NetworkDevicesInfoResponse?) -> String { + func descriptionFor(device: DeviceDetails, + for followState: UserDeviceFollowState?, + deviceInfo: NetworkDevicesInfoResponse?, + photoVerificationState: PhotoVerificationStateView.State?) -> String { switch self { case .name: return device.displayName @@ -74,7 +77,20 @@ extension DeviceInfoViewModel.Field { let count = rewardSplit.count return LocalizableString.RewardDetails.rewardSplitDescription(count).localized case .photos: - return LocalizableString.PhotoVerification.uploadPhotosDescription.localized + guard let photoVerificationState else { + return "" + } + + switch photoVerificationState { + case .uploading: + return LocalizableString.DeviceInfo.photoVerificationUploadingDescription.localized + case .content(let photos, _): + if photos.isEmpty { + return LocalizableString.DeviceInfo.photoVerificationEmptyText.localized + } + + return LocalizableString.DeviceInfo.photoVerificationWithPhotosDescription.localized + } } } @@ -116,7 +132,9 @@ extension DeviceInfoViewModel.Field { } @MainActor - func buttonInfoFor(devie: DeviceDetails, followState: UserDeviceFollowState?) -> DeviceInfoButtonInfo? { + func buttonInfoFor(devie: DeviceDetails, + followState: UserDeviceFollowState?, + photoVerificationState: PhotoVerificationStateView.State?) -> DeviceInfoButtonInfo? { switch self { case .name: return .init(icon: nil, title: LocalizableString.DeviceInfo.buttonChangeName.localized) @@ -140,25 +158,23 @@ extension DeviceInfoViewModel.Field { case .rewardSplit: return nil case .photos: - return .init(icon: nil, title: LocalizableString.PhotoVerification.letsTakeTheFirstPhoto.localized) - } - } - - @MainActor - func customViewFor(deviceInfo: NetworkDevicesInfoResponse?) -> AnyView? { - switch self { - case .rewardSplit: - guard let rewardsplit = deviceInfo?.rewardSplit, rewardsplit.count > 0 else { + guard let photoVerificationState else { return nil } - let items = rewardsplit.map { split in - let userWallet = MainScreenViewModel.shared.userInfo?.wallet?.address - let isUserWallet = split.wallet == userWallet - return split.toSplitViewItem(showReward: false, isUserWallet: isUserWallet) + + switch photoVerificationState { + case .content(let photos, _): + if photos.isEmpty { + return .init(icon: nil, + title: LocalizableString.DeviceInfo.photoVerificationStartButtonTitle.localized, + buttonStyle: .filled()) + + } + + fallthrough + default: + return nil } - return RewardsSplitView.WalletsListView(items: items).toAnyView - default: - return nil } } } diff --git a/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoRowView.swift b/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoRowView.swift index 31643dc91..f0a6302b9 100644 --- a/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoRowView.swift +++ b/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoRowView.swift @@ -13,12 +13,24 @@ struct DeviceInfoRowView: View { var body: some View { VStack(spacing: CGFloat(.smallToMediumSpacing)) { - VStack(spacing: CGFloat(.minimumSpacing)) { + VStack(spacing: CGFloat(.smallSpacing)) { HStack { Text(row.title) .foregroundColor(Color(colorEnum: .darkestBlue)) .font(.system(size: CGFloat(.mediumFontSize), weight: .bold)) - Spacer() + + Spacer() + + if let badge = row.badge { + Text(badge.uppercased()) + .foregroundColor(Color(colorEnum: .wxmPrimary)) + .font(.system(size: CGFloat(.littleCaption), weight: .bold)) + .padding(.horizontal, CGFloat(.smallSidePadding)) + .padding(.vertical, CGFloat(.minimumPadding)) + .background { + Capsule().fill(Color(colorEnum: .layer2)) + } + } } HStack { @@ -71,7 +83,6 @@ struct DeviceInfoRowView: View { if let customView = row.customView { customView - .padding(.top, CGFloat(.defaultSidePadding)) } } } @@ -82,6 +93,7 @@ extension DeviceInfoRowView { struct Row: Equatable { static func == (lhs: DeviceInfoRowView.Row, rhs: DeviceInfoRowView.Row) -> Bool { lhs.title == rhs.title && + lhs.badge == rhs.badge && lhs.description == rhs.description && lhs.imageUrl == rhs.imageUrl && lhs.buttonInfo == rhs.buttonInfo && @@ -89,6 +101,7 @@ extension DeviceInfoRowView { } let title: String + var badge: String? let description: AttributedString let imageUrl: URL? let buttonInfo: DeviceInfoButtonInfo? @@ -154,6 +167,7 @@ private extension DeviceInfoRowView { struct DeviceInfoRowView_Previews: PreviewProvider { static var previews: some View { DeviceInfoRowView(row: DeviceInfoRowView.Row(title: "TItle", + badge: "New", description: "This is a **desription**".attributedMarkdown!, imageUrl: URL(string: "https://i0.wp.com/weatherxm.com/wp-content/uploads/2023/12/Home-header-image-1200-x-1200-px-5.png?w=1200&ssl=1"), buttonInfo: .init(icon: nil, title: "Button title"), diff --git a/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoViewModel+Content.swift b/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoViewModel+Content.swift index 7f72cd5ab..d95cb3b95 100644 --- a/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoViewModel+Content.swift +++ b/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoViewModel+Content.swift @@ -38,7 +38,8 @@ extension DeviceInfoViewModel { static func heliumSections(for followState: UserDeviceFollowState?) -> [[Field]] { if followState?.state == .owned { - return [[.name, .frequency, .reboot, .photos], + return [[.photos], + [.name, .frequency, .reboot], [.stationLocation]] } @@ -47,7 +48,8 @@ extension DeviceInfoViewModel { static func wifiSections(for followState: UserDeviceFollowState?) -> [[Field]] { if followState?.state == .owned { - return [[.name, .photos], + return [[.name], + [.photos], [.stationLocation]] } diff --git a/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoViewModel.swift b/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoViewModel.swift index 1dd71bf18..47721b71e 100644 --- a/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoViewModel.swift +++ b/PresentationLayer/UIComponents/Screens/DeviceInfo/DeviceInfoViewModel.swift @@ -10,48 +10,59 @@ import DomainLayer import Combine import Toolkit import UIKit +import SwiftUI @MainActor class DeviceInfoViewModel: ObservableObject { let mainVM: MainScreenViewModel = .shared - var sections: [[DeviceInfoRowView.Row]] { - var fields: [[Field]] = [] + var sections: [[DeviceInfoRowView.Row]] { + var fields: [[Field]] = [] if device.isHelium { fields = Field.heliumSections(for: followState) } else { fields = Field.wifiSections(for: followState) - } + } - let rows: [[DeviceInfoRowView.Row]] = fields.map { $0.map { field in - DeviceInfoRowView.Row(title: field.titleFor(devie: device), - description: field.descriptionFor(device: device, - for: followState, - deviceInfo: deviceInfo).attributedMarkdown ?? "", - imageUrl: field.imageUrlFor(device: device, followState: followState), - buttonInfo: field.buttonInfoFor(devie: device, followState: followState), - warning: field.warning, - customView: field.customViewFor(deviceInfo: deviceInfo), - buttonAction: { [weak self] in self?.handleButtonTap(field: field) }) - } - } + let rows: [[DeviceInfoRowView.Row]] = fields.map { $0.map { field in + let title = field.titleFor(devie: device) + return DeviceInfoRowView.Row(title: title.title, + badge: title.badge, + description: field.descriptionFor(device: device, + for: followState, + deviceInfo: deviceInfo, + photoVerificationState: photoVerificationState).attributedMarkdown ?? "", + imageUrl: field.imageUrlFor(device: device, followState: followState), + buttonInfo: field.buttonInfoFor(devie: device, + followState: followState, + photoVerificationState: photoStateViewModel?.state), + warning: field.warning, + customView: customViewFor(field: field), + buttonAction: { [weak self] in self?.handleButtonTap(field: field) }) + } + } - return rows - } + return rows + } var bottomSections: [[DeviceInfoRowView.Row]] { let fields = Field.bottomSections(for: followState, deviceInfo: deviceInfo) let rows: [[DeviceInfoRowView.Row]] = fields.map { $0.map { field in - DeviceInfoRowView.Row(title: field.titleFor(devie: device), - description: field.descriptionFor(device: device, - for: followState, - deviceInfo: deviceInfo).attributedMarkdown ?? "", - imageUrl: field.imageUrlFor(device: device, followState: followState), - buttonInfo: field.buttonInfoFor(devie: device, followState: followState), - warning: field.warning, - customView: field.customViewFor(deviceInfo: deviceInfo), - buttonAction: { [weak self] in self?.handleButtonTap(field: field) }) - } + let title = field.titleFor(devie: device) + return DeviceInfoRowView.Row(title: title.title, + badge: title.badge, + description: field.descriptionFor(device: device, + for: followState, + deviceInfo: deviceInfo, + photoVerificationState: photoVerificationState).attributedMarkdown ?? "", + imageUrl: field.imageUrlFor(device: device, followState: followState), + buttonInfo: field.buttonInfoFor(devie: device, + followState: followState, + photoVerificationState: photoStateViewModel?.state), + warning: field.warning, + customView: customViewFor(field: field), + buttonAction: { [weak self] in self?.handleButtonTap(field: field) }) + } } return rows @@ -82,6 +93,7 @@ class DeviceInfoViewModel: ObservableObject { followState: followState) } } + @Published private(set) var photoVerificationState: PhotoVerificationStateView.State? let followState: UserDeviceFollowState? @Published var showRebootStation = false var rebootStationViewModel: RebootStationViewModel { @@ -113,11 +125,23 @@ class DeviceInfoViewModel: ObservableObject { private let deviceInfoUseCase: DeviceInfoUseCase? private var cancellable: Set = [] private let friendlyNameRegex = "^\\S.{0,64}$" + private let photoStateViewModel: PhotoVerificationStateViewModel? init(device: DeviceDetails, followState: UserDeviceFollowState?) { self.device = device self.followState = followState self.deviceInfoUseCase = SwinjectHelper.shared.getContainerForSwinject().resolve(DeviceInfoUseCase.self) + + if let deviceId = device.id { + self.photoStateViewModel = ViewModelsFactory.getPhotoVerificationStateViewModel(deviceId: deviceId) + } else { + self.photoStateViewModel = nil + } + + photoStateViewModel?.$state.sink { state in + self.photoVerificationState = state + }.store(in: &cancellable) + refresh { [weak self] in self?.trackRewardSplitViewEvent() } @@ -148,25 +172,28 @@ class DeviceInfoViewModel: ObservableObject { return } - do { - try deviceInfoUseCase?.getDeviceInfo(deviceId: deviceId).sink { [weak self] response in - self?.isLoading = false - if let error = response.error { - self?.failObj = error.uiInfo.defaultFailObject(type: .deviceInfo) { - self?.isFailed = false - self?.isLoading = true - self?.refresh() - } - self?.isFailed = true - } - self?.deviceInfo = response.value + Task { @MainActor [weak self] in + let photosError = await self?.photoStateViewModel?.refresh() + + do { + let response = try await self?.deviceInfoUseCase?.getDeviceInfo(deviceId: deviceId).toAsync() + self?.isLoading = false + if let error = response?.error ?? photosError { + self?.failObj = error.uiInfo.defaultFailObject(type: .deviceInfo) { + self?.isFailed = false + self?.isLoading = true + self?.refresh() + } + self?.isFailed = true + } + self?.deviceInfo = response?.value completion?() - }.store(in: &self.cancellable) - } catch { - isLoading = false - print(error) - completion?() - } + } catch { + self?.isLoading = false + print(error) + completion?() + } + } } } @@ -234,7 +261,7 @@ private extension DeviceInfoViewModel { case .rewardSplit: break case .photos: - let route = PhotoIntroViewModel.getInitialRoute() + let route = PhotoIntroViewModel.getInitialRoute(images: [], isNewPhotoVerification: true) Router.shared.navigateTo(route) } } @@ -315,7 +342,7 @@ private extension DeviceInfoViewModel { let alertObject = AlertHelper.AlertObject(title: LocalizableString.DeviceInfo.editNameAlertTitle.localized, message: LocalizableString.DeviceInfo.editNameAlertMessage.localized, - textFieldPlaceholder: Field.name.titleFor(devie: device), + textFieldPlaceholder: Field.name.titleFor(devie: device).title, textFieldValue: device.displayName, textFieldDelegate: AlertTexFieldDelegate(), cancelAction: { @@ -407,6 +434,28 @@ private extension DeviceInfoViewModel { .contentId: .changeStationNameResultContentId, .success: .custom(isSuccessful ? "1" : "0")]) } + + func customViewFor(field: Field) -> AnyView? { + switch field { + case .rewardSplit: + guard let rewardsplit = deviceInfo?.rewardSplit, rewardsplit.count > 0 else { + return nil + } + let items = rewardsplit.map { split in + let userWallet = MainScreenViewModel.shared.userInfo?.wallet?.address + let isUserWallet = split.wallet == userWallet + return split.toSplitViewItem(showReward: false, isUserWallet: isUserWallet) + } + return RewardsSplitView.WalletsListView(items: items).toAnyView + case .photos: + guard let photoStateViewModel else { + return nil + } + return PhotoVerificationStateView(viewModel: photoStateViewModel).toAnyView + default: + return nil + } + } } private extension DeviceInfoViewModel { diff --git a/PresentationLayer/UIComponents/Screens/DeviceInfo/PhotoVerificationState/PhotoVerificationStateView.swift b/PresentationLayer/UIComponents/Screens/DeviceInfo/PhotoVerificationState/PhotoVerificationStateView.swift new file mode 100644 index 000000000..5107cff31 --- /dev/null +++ b/PresentationLayer/UIComponents/Screens/DeviceInfo/PhotoVerificationState/PhotoVerificationStateView.swift @@ -0,0 +1,124 @@ +// +// PhotoVerificationStateView.swift +// wxm-ios +// +// Created by Pantelis Giazitsis on 18/12/24. +// + +import SwiftUI +import NukeUI + +struct PhotoVerificationStateView: View { + @StateObject var viewModel: PhotoVerificationStateViewModel + + var body: some View { + switch viewModel.state { + case .content(let photos, let isFailed): + photosView(photos: photos, isFailed: isFailed) + case .uploading(let progress): + uploadingView(progress: progress) + } + } +} + +extension PhotoVerificationStateView { + enum State { + case content(photos: [URL], isFailed: Bool) + case uploading(progress: CGFloat) + } + + @ViewBuilder + func photosView(photos: [URL], isFailed: Bool) -> some View { + VStack(spacing: CGFloat(.smallSpacing)) { + let last = photos.last + LazyVGrid(columns: [.init(spacing: CGFloat(.smallSpacing)), .init()]) { + ForEach(photos, id: \.self) { url in + Button { + viewModel.handleImageTap() + } label: { + Color.clear + .aspectRatio(1.0, contentMode: .fit) + .overlay { + LazyImage(url: url) { state in + if let image = state.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + .clipped() + } else { + ProgressView() + } + } + } + .overlay { + let isLast = url == last + if isLast, viewModel.morePhotosCount > 0 { + ZStack { + Color.black.opacity(0.6) + + Text("+\(viewModel.morePhotosCount)") + .font(.system(size: CGFloat(.XLTitleFontSize), weight: .bold)) + .foregroundStyle(Color(colorEnum: .textWhite)) + + } + } + } + } + .cornerRadius(CGFloat(.buttonCornerRadius)) + } + } + + if isFailed { + CardWarningView(configuration: .init(type: .error, + message: LocalizableString.PhotoVerification.uploadErrorDescription.localized, + closeAction: nil)) { + HStack { + Button { + + } label: { + Text(LocalizableString.PhotoVerification.retryUpload.localized) + .font(.system(size: CGFloat(.caption), weight: .bold)) + .foregroundStyle(Color(colorEnum: .wxmPrimary)) + } + + Spacer() + } + } + } + } + } + + @ViewBuilder + func uploadingView(progress: CGFloat) -> some View { + VStack(spacing: CGFloat(.mediumSpacing)) { + VStack(spacing: CGFloat(.smallSpacing)) { + HStack(alignment: .bottom, spacing: CGFloat(.smallSpacing)) { + Text(LocalizableString.percentage(Float(progress)).localized) + .font(.system(size: CGFloat(.largeTitleFontSize), weight: .bold)) + .foregroundStyle(Color(colorEnum: .text)) + + Text(LocalizableString.PhotoVerification.uploading.localized) + .font(.system(size: CGFloat(.normalFontSize))) + .foregroundStyle(Color(colorEnum: .text)) + + Spacer() + } + + ProgressView(value: progress, total: 100.0) + .tint(Color(colorEnum: .wxmPrimary)) + .animation(.easeOut(duration: 0.3), value: progress) + } + + Button { + viewModel.handleCancelUploadTap() + } label: { + Text(LocalizableString.PhotoVerification.cancelUpload.localized) + } + .buttonStyle(WXMButtonStyle()) + } + } +} + +#Preview { + PhotoVerificationStateView(viewModel: ViewModelsFactory.getPhotoVerificationStateViewModel(deviceId: "")) +} diff --git a/PresentationLayer/UIComponents/Screens/DeviceInfo/PhotoVerificationState/PhotoVerificationStateViewModel.swift b/PresentationLayer/UIComponents/Screens/DeviceInfo/PhotoVerificationState/PhotoVerificationStateViewModel.swift new file mode 100644 index 000000000..58419f22f --- /dev/null +++ b/PresentationLayer/UIComponents/Screens/DeviceInfo/PhotoVerificationState/PhotoVerificationStateViewModel.swift @@ -0,0 +1,102 @@ +// +// PhotoVerificationStateViewModel.swift +// wxm-ios +// +// Created by Pantelis Giazitsis on 18/12/24. +// + +import Foundation +import DomainLayer +import Combine + +@MainActor +class PhotoVerificationStateViewModel: ObservableObject { + @Published private(set) var state: PhotoVerificationStateView.State = .content(photos: [], isFailed: false) + private var allPhotos: [NetworkDevicePhotosResponse] = [] + @Published private(set) var morePhotosCount: Int = 0 + private var cancellable: Set = [] + private let deviceInfoUseCase: DeviceInfoUseCase? + private let deviceId: String + + init(deviceId: String, deviceInfoUseCase: DeviceInfoUseCase?) { + self.deviceId = deviceId + self.deviceInfoUseCase = deviceInfoUseCase + } + + func handleCancelUploadTap() { + let yesAction: AlertHelper.AlertObject.Action = (LocalizableString.PhotoVerification.yesCancel.localized, { _ in }) + let alertObject = AlertHelper.AlertObject(title: LocalizableString.PhotoVerification.cancelUpload.localized, + message: LocalizableString.PhotoVerification.cancelUploadAlertMessage.localized, + cancelActionTitle: LocalizableString.back.localized, + cancelAction: {}, + okAction: yesAction) + + AlertHelper().showAlert(alertObject) + + } + + func handleImageTap() { + let route = PhotoIntroViewModel.getInitialRoute(images: allPhotos.compactMap { $0.url }, isNewPhotoVerification: false) + Router.shared.navigateTo(route) + } + + func refresh() async -> NetworkErrorResponse? { + // Comment the following line and ucomment one of the rest to test each ui case along with the + // return nil at the bottom + await fetchPhotos() + + // ---Uploading state--- + //state = .uploading(progress: 63) + + // ---Empty--- + //state = .content(photos: [], isFailed: false) + + //---Empty with error--- + //state = .content(photos: [], isFailed: true) + + // ---Photos with error--- +// state = .content(photos: [URL(string: "https://i0.wp.com/weatherxm.com/wp-content/uploads/2023/09/5-5.png")!, +// URL(string: "https://docs.weatherxm.com/img/wxm-devices/deployments/good-example-1.jpg")!], isFailed: true) + + //return nil + } +} + +private extension PhotoVerificationStateViewModel { + @MainActor + func fetchPhotos() async -> NetworkErrorResponse? { + do { + guard let result = try await deviceInfoUseCase?.getDevicePhotos(deviceId: deviceId).toAsync().result else { + return nil + } + + switch result { + case .success(let response): + let urls: [URL]? = response.compactMap { photo in + guard let url = photo.url else { + return nil + } + return URL(string: url) + } + + self.allPhotos = response + + if let urls { + let urlsToShow = urls.prefix(2) + let remainingCount = urls.dropFirst(2).count + self.morePhotosCount = remainingCount + self.state = .content(photos: Array(urlsToShow), isFailed: false) + } else { + self.state = .content(photos: [], isFailed: false) + } + case .failure(let error): + return error + } + } catch { + state = .content(photos: [], isFailed: false) + print(error) + } + + return nil + } +} diff --git a/PresentationLayer/UIComponents/Screens/PhotoVerification/GalleryView/GalleryView.swift b/PresentationLayer/UIComponents/Screens/PhotoVerification/GalleryView/GalleryView.swift index 443ca0f14..ce0926a2b 100644 --- a/PresentationLayer/UIComponents/Screens/PhotoVerification/GalleryView/GalleryView.swift +++ b/PresentationLayer/UIComponents/Screens/PhotoVerification/GalleryView/GalleryView.swift @@ -21,7 +21,7 @@ struct GalleryView: View { Button { viewModel.handleBackButtonTap(dismissAction: dismiss) } label: { - Text(FontIcon.xmark.rawValue) + Text(viewModel.backButtonIcon.rawValue) .font(.fontAwesome(font: .FAProSolid, size: CGFloat(.smallTitleFontSize))) .foregroundStyle(.text) } @@ -233,5 +233,6 @@ private extension GalleryView { } #Preview { - GalleryView(viewModel: ViewModelsFactory.getGalleryViewModel()) + GalleryView(viewModel: ViewModelsFactory.getGalleryViewModel(images: [], + isNewVerification: true)) } diff --git a/PresentationLayer/UIComponents/Screens/PhotoVerification/GalleryView/GalleryViewModel.swift b/PresentationLayer/UIComponents/Screens/PhotoVerification/GalleryView/GalleryViewModel.swift index 66fc00007..bd5f6c83a 100644 --- a/PresentationLayer/UIComponents/Screens/PhotoVerification/GalleryView/GalleryViewModel.swift +++ b/PresentationLayer/UIComponents/Screens/PhotoVerification/GalleryView/GalleryViewModel.swift @@ -34,9 +34,17 @@ class GalleryViewModel: ObservableObject { var isUploadButtonEnabled: Bool { images.count >= minPhotosCount } + var backButtonIcon: FontIcon { + if isNewPhotoVerification { + return .xmark + } + + return .arrowLeft + } private let minPhotosCount = 2 private let maxPhotosCount = 6 private let useCase: PhotoGalleryUseCase + private let isNewPhotoVerification: Bool private lazy var imagePickerDelegate = { let picker = ImagePickerDelegate(useCase: useCase) picker.imageCallback = { [weak self] imageUrl in @@ -46,8 +54,10 @@ class GalleryViewModel: ObservableObject { return picker }() - init(photoGalleryUseCase: PhotoGalleryUseCase) { + init(images: [String], photoGalleryUseCase: PhotoGalleryUseCase, isNewPhotoVerification: Bool) { self.useCase = photoGalleryUseCase + self.images = images + self.isNewPhotoVerification = isNewPhotoVerification selectedImage = images.last updateSubtitle() updateCameraPermissionState() @@ -93,15 +103,22 @@ class GalleryViewModel: ObservableObject { } func handleBackButtonTap(dismissAction: DismissAction) { - let exitAction: AlertHelper.AlertObject.Action = (LocalizableString.exit.localized, { _ in dismissAction() }) - let alertObject = AlertHelper.AlertObject(title: LocalizableString.PhotoVerification.exitPhotoVerification.localized, - message: LocalizableString.PhotoVerification.exitPhotoVerificationText.localized, - cancelActionTitle: LocalizableString.back.localized, - cancelAction: { - }, - okAction: exitAction) + if isNewPhotoVerification { + showExitAlert(message: LocalizableString.PhotoVerification.exitPhotoVerificationText.localized, + dismissAction: dismissAction) - AlertHelper().showAlert(alertObject) + return + } + + let remoteImageCount = images.compactMap { URL(string: $0) }.filter { $0.isHttp }.count + if remoteImageCount < minPhotosCount { + showExitAlert(message: LocalizableString.PhotoVerification.exitPhotoVerificationMinimumPhotosText.localized, + dismissAction: dismissAction) + + return + } + + dismissAction() } } @@ -180,4 +197,15 @@ private extension GalleryViewModel { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: openPikerCallback) } } + + func showExitAlert(message: String, dismissAction: DismissAction) { + let exitAction: AlertHelper.AlertObject.Action = (LocalizableString.exit.localized, { _ in dismissAction() }) + let alertObject = AlertHelper.AlertObject(title: LocalizableString.PhotoVerification.exitPhotoVerification.localized, + message: message, + cancelActionTitle: LocalizableString.back.localized, + cancelAction: {}, + okAction: exitAction) + + AlertHelper().showAlert(alertObject) + } } diff --git a/PresentationLayer/UIComponents/Screens/PhotoVerification/Intro/PhotoIntroViewModel.swift b/PresentationLayer/UIComponents/Screens/PhotoVerification/Intro/PhotoIntroViewModel.swift index 0fa1fb259..2d33261d3 100644 --- a/PresentationLayer/UIComponents/Screens/PhotoVerification/Intro/PhotoIntroViewModel.swift +++ b/PresentationLayer/UIComponents/Screens/PhotoVerification/Intro/PhotoIntroViewModel.swift @@ -46,7 +46,7 @@ class PhotoIntroViewModel: ObservableObject { } func handleBeginButtonTap() { - let viewModel = ViewModelsFactory.getGalleryViewModel() + let viewModel = ViewModelsFactory.getGalleryViewModel(images: [], isNewVerification: true) Router.shared.navigateTo(.photoGallery(viewModel)) } } @@ -55,12 +55,12 @@ extension PhotoIntroViewModel: HashableViewModel { nonisolated func hash(into hasher: inout Hasher) { } - static func getInitialRoute() -> Route { + static func getInitialRoute(images: [String], isNewPhotoVerification: Bool) -> Route { let useCase = SwinjectHelper.shared.getContainerForSwinject().resolve(PhotoGalleryUseCase.self)! let areTermsAccepted = useCase.areTermsAccepted if areTermsAccepted { - let viewModel = ViewModelsFactory.getGalleryViewModel() + let viewModel = ViewModelsFactory.getGalleryViewModel(images: images, isNewVerification: isNewPhotoVerification) return .photoGallery(viewModel) } diff --git a/PresentationLayer/UIComponents/Screens/RewardBoosts/Components/BoostCardView.swift b/PresentationLayer/UIComponents/Screens/RewardBoosts/Components/BoostCardView.swift index 8a158576d..d90c2a551 100644 --- a/PresentationLayer/UIComponents/Screens/RewardBoosts/Components/BoostCardView.swift +++ b/PresentationLayer/UIComponents/Screens/RewardBoosts/Components/BoostCardView.swift @@ -16,7 +16,7 @@ struct BoostCardView: View { HStack { Text(boost.title) .font(.system(size: CGFloat(.titleFontSize), weight: .bold)) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Spacer() } @@ -24,7 +24,7 @@ struct BoostCardView: View { HStack { Text("+ \(boost.reward.toWXMTokenPrecisionString) \(StringConstants.wxmCurrency)") .font(.system(size: CGFloat(.XLTitleFontSize), weight: .bold)) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Spacer() } @@ -34,7 +34,7 @@ struct BoostCardView: View { timezone: .UTCTimezone, showTimeZoneIndication: true).capitalizedSentence).localized) .font(.system(size: CGFloat(.normalFontSize))) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Spacer() } } @@ -72,13 +72,13 @@ private extension BoostCardView { HStack { Text(LocalizableString.Boosts.dailyBoostScore.localized) .font(.system(size: CGFloat(.normalFontSize))) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Spacer() Text(LocalizableString.percentage(Float(score)).localized) .font(.system(size: CGFloat(.normalFontSize))) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) } Divider().overlay(Color(colorEnum: .lightLayer2)) @@ -86,7 +86,7 @@ private extension BoostCardView { HStack { Text(lostRewardString) .font(.system(size: CGFloat(.normalFontSize))) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Spacer() } } diff --git a/PresentationLayer/UIComponents/Screens/WeatherStations/StationDetails/Rewards/Components/AnnouncementCardView.swift b/PresentationLayer/UIComponents/Screens/WeatherStations/StationDetails/Rewards/Components/AnnouncementCardView.swift index 9d7f0fe6a..19d69fe23 100644 --- a/PresentationLayer/UIComponents/Screens/WeatherStations/StationDetails/Rewards/Components/AnnouncementCardView.swift +++ b/PresentationLayer/UIComponents/Screens/WeatherStations/StationDetails/Rewards/Components/AnnouncementCardView.swift @@ -16,7 +16,7 @@ struct AnnouncementCardView: View { HStack(alignment: .top, spacing: CGFloat(.smallSpacing)) { Text(configuration.title) .font(.system(size: CGFloat(.mediumFontSize), weight: .bold)) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Spacer() @@ -25,7 +25,7 @@ struct AnnouncementCardView: View { Text(FontIcon.close.rawValue) .font(.fontAwesome(font: .FAProSolid, size: CGFloat(.caption))) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) } } } @@ -35,7 +35,7 @@ struct AnnouncementCardView: View { HStack { Text(configuration.description) .font(.system(size: CGFloat(.normalFontSize))) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Spacer() } @@ -47,18 +47,18 @@ struct AnnouncementCardView: View { HStack(spacing: CGFloat(.minimumSpacing)) { Text(actionTitle) .font(.system(size: CGFloat(.caption), weight: .bold)) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) Text(FontIcon.externalLink.rawValue) .font(.fontAwesome(font: .FAProSolid, size: CGFloat(.caption))) - .foregroundColor(Color(colorEnum: .wxmWhite)) + .foregroundColor(Color(colorEnum: .textWhite)) } .padding(.horizontal, CGFloat(.mediumSidePadding)) .padding(.vertical, CGFloat(.smallSidePadding)) .background { - Capsule().foregroundStyle(Color(colorEnum: .wxmWhite).opacity(0.2)) + Capsule().foregroundStyle(Color(colorEnum: .textWhite).opacity(0.2)) } } diff --git a/PresentationLayer/UIComponents/ViewModelsFactory.swift b/PresentationLayer/UIComponents/ViewModelsFactory.swift index e6889745b..7652124c8 100644 --- a/PresentationLayer/UIComponents/ViewModelsFactory.swift +++ b/PresentationLayer/UIComponents/ViewModelsFactory.swift @@ -271,8 +271,13 @@ enum ViewModelsFactory { return PhotoInstructionsViewModel(photoGalleryUseCase: useCase) } - static func getGalleryViewModel() -> GalleryViewModel { + static func getGalleryViewModel(images: [String], isNewVerification: Bool) -> GalleryViewModel { let useCase = SwinjectHelper.shared.getContainerForSwinject().resolve(PhotoGalleryUseCase.self)! - return GalleryViewModel(photoGalleryUseCase: useCase) + return GalleryViewModel(images: images, photoGalleryUseCase: useCase, isNewPhotoVerification: isNewVerification) + } + + static func getPhotoVerificationStateViewModel(deviceId: String) -> PhotoVerificationStateViewModel { + let useCase = SwinjectHelper.shared.getContainerForSwinject().resolve(DeviceInfoUseCase.self)! + return PhotoVerificationStateViewModel(deviceId: deviceId, deviceInfoUseCase: useCase) } } diff --git a/wxm-ios.xcodeproj/project.pbxproj b/wxm-ios.xcodeproj/project.pbxproj index e8a731459..6933bb1f6 100644 --- a/wxm-ios.xcodeproj/project.pbxproj +++ b/wxm-ios.xcodeproj/project.pbxproj @@ -531,6 +531,8 @@ 26F6F73F2D070E3A00FB256B /* WXMCapsuleButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F6F73D2D070E3A00FB256B /* WXMCapsuleButtonStyle.swift */; }; 26F6F7412D0725FF00FB256B /* WXMButtonOpacityStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F6F7402D0725FF00FB256B /* WXMButtonOpacityStyle.swift */; }; 26F6F7422D0725FF00FB256B /* WXMButtonOpacityStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F6F7402D0725FF00FB256B /* WXMButtonOpacityStyle.swift */; }; + 26FC21C52D12F7DC00FC79B1 /* PhotoVerificationStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FC21C42D12F7DC00FC79B1 /* PhotoVerificationStateView.swift */; }; + 26FC21C72D12F7F000FC79B1 /* PhotoVerificationStateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FC21C62D12F7F000FC79B1 /* PhotoVerificationStateViewModel.swift */; }; 26FFAD682B8DF9C200BF0A6B /* LazyLoadingPager in Frameworks */ = {isa = PBXBuildFile; productRef = 26FFAD672B8DF9C200BF0A6B /* LazyLoadingPager */; }; 26FFAD792B8E12DC00BF0A6B /* RewardFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FFAD782B8E12DC00BF0A6B /* RewardFieldView.swift */; }; 26FFADA72B8F3D2700BF0A6B /* NetworkDeviceRewardDetailsResponse+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FFADA62B8F3D2700BF0A6B /* NetworkDeviceRewardDetailsResponse+.swift */; }; @@ -1091,6 +1093,8 @@ 26F6F73A2D07069E00FB256B /* GalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewModel.swift; sourceTree = ""; }; 26F6F73D2D070E3A00FB256B /* WXMCapsuleButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WXMCapsuleButtonStyle.swift; sourceTree = ""; }; 26F6F7402D0725FF00FB256B /* WXMButtonOpacityStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WXMButtonOpacityStyle.swift; sourceTree = ""; }; + 26FC21C42D12F7DC00FC79B1 /* PhotoVerificationStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoVerificationStateView.swift; sourceTree = ""; }; + 26FC21C62D12F7F000FC79B1 /* PhotoVerificationStateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoVerificationStateViewModel.swift; sourceTree = ""; }; 26FFAD782B8E12DC00BF0A6B /* RewardFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardFieldView.swift; sourceTree = ""; }; 26FFADA62B8F3D2700BF0A6B /* NetworkDeviceRewardDetailsResponse+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkDeviceRewardDetailsResponse+.swift"; sourceTree = ""; }; 26FFADD32B8F684C00BF0A6B /* RewardAnnotaionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardAnnotaionsView.swift; sourceTree = ""; }; @@ -2131,6 +2135,7 @@ 26A4116C29B744FC00A2C10B /* DeviceInfoViewModel.swift */, 26A4117029B78CAC00A2C10B /* StationInfoView.swift */, 2698AE9B2CB6667A00DDB04D /* DeviceInfoFactory.swift */, + 26FC21C32D12F79E00FC79B1 /* PhotoVerificationState */, ); path = DeviceInfo; sourceTree = ""; @@ -2568,6 +2573,15 @@ path = ButtonStyles; sourceTree = ""; }; + 26FC21C32D12F79E00FC79B1 /* PhotoVerificationState */ = { + isa = PBXGroup; + children = ( + 26FC21C42D12F7DC00FC79B1 /* PhotoVerificationStateView.swift */, + 26FC21C62D12F7F000FC79B1 /* PhotoVerificationStateViewModel.swift */, + ); + path = PhotoVerificationState; + sourceTree = ""; + }; 26FFAD772B8E125900BF0A6B /* Components */ = { isa = PBXGroup; children = ( @@ -3289,6 +3303,7 @@ 26AF276F2A0B815B0067A1B8 /* WeatherChartModels.swift in Sources */, 26AA21E92BF4FA3B00B91A7C /* SelectLocationMapView.swift in Sources */, 26A3B3DC2B18D2480002F35F /* TabBarVisibilityHandler.swift in Sources */, + 26FC21C72D12F7F000FC79B1 /* PhotoVerificationStateViewModel.swift in Sources */, 2672C77A2AA9BBEB00E1FDCC /* HistoryContainerView.swift in Sources */, 26A308F92C2EE481007A029A /* ScannerView.swift in Sources */, 2608A7902C09D3AC00452E40 /* ClaimDeviceSetFrequencyView.swift in Sources */, @@ -3523,6 +3538,7 @@ 267A378A2AEBFF2F00469126 /* DeviceRewardsOverview+.swift in Sources */, 2698AE9C2CB6667A00DDB04D /* DeviceInfoFactory.swift in Sources */, 26E5B0C029F8078C00834899 /* WXMDivider.swift in Sources */, + 26FC21C52D12F7DC00FC79B1 /* PhotoVerificationStateView.swift in Sources */, 2674017E2A38C2AE00E54E35 /* WXMEmptyView.swift in Sources */, 266DC01B2A55786F00EC9E1F /* WXMPopover.swift in Sources */, 2671E5D02CBD537700EE0FF1 /* StationHealthInfoView.swift in Sources */, diff --git a/wxm-ios/DataLayer/DataLayer.xcodeproj/project.pbxproj b/wxm-ios/DataLayer/DataLayer.xcodeproj/project.pbxproj index db0ddc14f..e30594ef0 100644 --- a/wxm-ios/DataLayer/DataLayer.xcodeproj/project.pbxproj +++ b/wxm-ios/DataLayer/DataLayer.xcodeproj/project.pbxproj @@ -75,6 +75,7 @@ 26F6F7542D0855FE00FB256B /* PhotosRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F6F7532D0855FE00FB256B /* PhotosRepositoryImpl.swift */; }; 26FC21B62D119C0E00FC79B1 /* claim_device.json in Resources */ = {isa = PBXBuildFile; fileRef = 26FC21B52D119C0E00FC79B1 /* claim_device.json */; }; 26FC21B82D11A46A00FC79B1 /* claim_device_helium.json in Resources */ = {isa = PBXBuildFile; fileRef = 26FC21B72D11A45700FC79B1 /* claim_device_helium.json */; }; + 26FC21C22D12E80400FC79B1 /* get_user_device_photos.json in Resources */ = {isa = PBXBuildFile; fileRef = 26FC21C12D12E7FD00FC79B1 /* get_user_device_photos.json */; }; 26FFAD932B8E2A5500BF0A6B /* get_reward_details.json in Resources */ = {isa = PBXBuildFile; fileRef = 26FFAD922B8E2A5500BF0A6B /* get_reward_details.json */; }; 4D1C95D028FC7EB1001F66C9 /* LocationRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D1C95CF28FC7EB1001F66C9 /* LocationRepositoryImpl.swift */; }; 4D49D90A294DE3B300987DE6 /* MapboxSearch in Frameworks */ = {isa = PBXBuildFile; productRef = 4D49D909294DE3B300987DE6 /* MapboxSearch */; }; @@ -170,6 +171,7 @@ 26F6F7532D0855FE00FB256B /* PhotosRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosRepositoryImpl.swift; sourceTree = ""; }; 26FC21B52D119C0E00FC79B1 /* claim_device.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = claim_device.json; sourceTree = ""; }; 26FC21B72D11A45700FC79B1 /* claim_device_helium.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = claim_device_helium.json; sourceTree = ""; }; + 26FC21C12D12E7FD00FC79B1 /* get_user_device_photos.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = get_user_device_photos.json; sourceTree = ""; }; 26FFAD922B8E2A5500BF0A6B /* get_reward_details.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = get_reward_details.json; sourceTree = ""; }; 4D1C95CF28FC7EB1001F66C9 /* LocationRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRepositoryImpl.swift; sourceTree = ""; }; 4D5AE32F28EA49C7006F2EBA /* BluetoothDevicesRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDevicesRepositoryImpl.swift; sourceTree = ""; }; @@ -255,6 +257,7 @@ 26AD3E9B2C8F3F4C0040AB09 /* get_device_rewards_analytics_7d.json */, 26AD3E9A2C8F3F4C0040AB09 /* get_device_rewards_analytics_year.json */, 26DD42D129AFAF26008E277E /* get_user_devices.json */, + 26FC21C12D12E7FD00FC79B1 /* get_user_device_photos.json */, 26B3D2B329E43D14006913E0 /* get_user_wallet.json */, 267CA6EE2C1314E200ABE599 /* get_user.json */, 26FC21B52D119C0E00FC79B1 /* claim_device.json */, @@ -552,6 +555,7 @@ 267A37872AEBEBBA00469126 /* get_user_device_rewards.json in Resources */, 26AD3EA62C90748C0040AB09 /* get_devices_rewards_analytics_year.json in Resources */, 26FC21B62D119C0E00FC79B1 /* claim_device.json in Resources */, + 26FC21C22D12E80400FC79B1 /* get_user_device_photos.json in Resources */, 26AD3EA72C90748C0040AB09 /* get_devices_rewards_analytics_7d.json in Resources */, 26A3B3E02B18E7A50002F35F /* get_user_rewards.json in Resources */, 2646FCBB2995018800EBB61B /* get_transactions.json in Resources */, diff --git a/wxm-ios/DataLayer/DataLayer/Networking/ApiRequestBuilders/MeApiRequestBuilder.swift b/wxm-ios/DataLayer/DataLayer/Networking/ApiRequestBuilders/MeApiRequestBuilder.swift index fd4d728fa..052dfe26f 100644 --- a/wxm-ios/DataLayer/DataLayer/Networking/ApiRequestBuilders/MeApiRequestBuilder.swift +++ b/wxm-ios/DataLayer/DataLayer/Networking/ApiRequestBuilders/MeApiRequestBuilder.swift @@ -48,6 +48,7 @@ enum MeApiRequestBuilder: URLRequestConvertible { case getUserDeviceForecastById(deviceId: String, fromDate: String, toDate: String, exclude: String) case getUserDeviceRewards(deviceId: String, mode: String) case getUserDevicesRewards(mode: String) + case getUserDevicePhotos(deviceId: String) case getDeviceFirmwareById(deviceId: String) case setFriendlyName(deviceId: String, name: String) case deleteFriendlyName(deviceId: String) @@ -64,7 +65,7 @@ enum MeApiRequestBuilder: URLRequestConvertible { switch self { case .getUser, .getUserWallet, .getDevices, .getFirmwares, .getUserDeviceById, .getUserDeviceHistoryById, .getUserDeviceForecastById, .getUserDeviceRewards, - .getUserDevicesRewards, .getDeviceFirmwareById, .getUserDeviceInfoById: + .getUserDevicesRewards, .getUserDevicePhotos, .getDeviceFirmwareById, .getUserDeviceInfoById: return .get case .saveUserWallet, .claimDevice, .setFriendlyName, .disclaimDevice, .follow, .setDeviceLocation, .setFCMToken: return .post @@ -115,6 +116,8 @@ enum MeApiRequestBuilder: URLRequestConvertible { return "me/devices/\(deviceId)/rewards" case .getUserDevicesRewards: return "me/devices/rewards" + case let .getUserDevicePhotos(deviceId): + return "me/devices/\(deviceId)/photos" case let .getDeviceFirmwareById(deviceId: deviceId): return "me/devices/\(deviceId)/firmware" case let .setFriendlyName(deviceId, _): @@ -206,6 +209,8 @@ extension MeApiRequestBuilder: MockResponseBuilder { default: return "get_device_rewards_analytics" } + case .getUserDevicePhotos: + return "get_user_device_photos" case .getUserDevicesRewards(let mode): switch mode { case DeviceRewardsMode.week.rawValue: diff --git a/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_device_info_helium.json b/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_device_info_helium.json index c6aa1aeb1..ad39ecf1a 100644 --- a/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_device_info_helium.json +++ b/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_device_info_helium.json @@ -18,5 +18,17 @@ "hw_version" : "1.1.0", "last_hs_name" : "warm-fuchsia-sheep", "last_hs_name_last_activity" : "2024-06-19T17:38:46+03:00" - } + }, + "reward_split": [ + { + "wallet": "0xc4E253863371fdeD8e414731DB951F4C17Bc645e", + "stake": 90, + "reward": 275.82664632199965 + }, + { + "wallet": "0xc4E253863371fdeD8e414731DB951F4C17Bc643e", + "stake": 10, + "reward": 27.82664632199965 + } + ] } diff --git a/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_device_info_m5.json b/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_device_info_m5.json index ba22fe306..b096f7b46 100644 --- a/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_device_info_m5.json +++ b/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_device_info_m5.json @@ -20,5 +20,17 @@ "bat_state": "low", "station_rssi" : -87, "station_rssi_last_activity" : "2024-10-08T13:29:30+03:00" - } + }, + "reward_split": [ + { + "wallet": "0xc4E253863371fdeD8e414731DB951F4C17Bc645e", + "stake": 90, + "reward": 275.82664632199965 + }, + { + "wallet": "0xc4E253863371fdeD8e414731DB951F4C17Bc643e", + "stake": 10, + "reward": 27.82664632199965 + } + ] } diff --git a/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_user_device_photos.json b/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_user_device_photos.json new file mode 100644 index 000000000..55687e728 --- /dev/null +++ b/wxm-ios/DataLayer/DataLayer/Networking/Mock/Jsons/get_user_device_photos.json @@ -0,0 +1,11 @@ +[ + { + "url": "https://i0.wp.com/weatherxm.com/wp-content/uploads/2023/09/5-5.png" + }, + { + "url": "https://docs.weatherxm.com/img/wxm-devices/deployments/good-example-1.jpg" + }, + { + "url": "https://docs.weatherxm.com/img/wxm-devices/deployments/good-example-2.jpg" + } +] diff --git a/wxm-ios/DataLayer/DataLayer/RepositoryImplementations/DeviceInfoRepositoryImpl.swift b/wxm-ios/DataLayer/DataLayer/RepositoryImplementations/DeviceInfoRepositoryImpl.swift index ddaffdfb9..0ead22ef6 100644 --- a/wxm-ios/DataLayer/DataLayer/RepositoryImplementations/DeviceInfoRepositoryImpl.swift +++ b/wxm-ios/DataLayer/DataLayer/RepositoryImplementations/DeviceInfoRepositoryImpl.swift @@ -79,6 +79,12 @@ extension DeviceInfoRepositoryImpl { return valueSubject.eraseToAnyPublisher() } + + public func getDevicePhotos(deviceId: String) throws -> AnyPublisher, Never> { + let builder = MeApiRequestBuilder.getUserDevicePhotos(deviceId: deviceId) + let urlRequest = try builder.asURLRequest() + return ApiClient.shared.requestCodableAuthorized(urlRequest, mockFileName: builder.mockFileName) + } } extension BTActionWrapper.ActionError { diff --git a/wxm-ios/DataLayer/DataLayer/RepositoryImplementations/PhotosRepositoryImpl.swift b/wxm-ios/DataLayer/DataLayer/RepositoryImplementations/PhotosRepositoryImpl.swift index d592417d0..51324db63 100644 --- a/wxm-ios/DataLayer/DataLayer/RepositoryImplementations/PhotosRepositoryImpl.swift +++ b/wxm-ios/DataLayer/DataLayer/RepositoryImplementations/PhotosRepositoryImpl.swift @@ -52,6 +52,7 @@ public struct PhotosRepositoryImpl: PhotosRepository { return } else if url.isHttp { // Delete from backend + return } throw PhotosError.failedToDeleteImage diff --git a/wxm-ios/DomainLayer/DomainLayer.xcodeproj/project.pbxproj b/wxm-ios/DomainLayer/DomainLayer.xcodeproj/project.pbxproj index 08bb18253..e10cd199a 100644 --- a/wxm-ios/DomainLayer/DomainLayer.xcodeproj/project.pbxproj +++ b/wxm-ios/DomainLayer/DomainLayer.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ 26DF3D2E2C85EA24005F6FC1 /* NetworkDeviceRewardsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DF3D2D2C85EA24005F6FC1 /* NetworkDeviceRewardsResponse.swift */; }; 26F6F7502D08540900FB256B /* PhotoGalleryUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F6F74F2D08540900FB256B /* PhotoGalleryUseCase.swift */; }; 26F6F7522D08559800FB256B /* PhotosRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F6F7512D08559800FB256B /* PhotosRepository.swift */; }; + 26FC21C02D12E5AB00FC79B1 /* NetworkDevicePhotosResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FC21BF2D12E5AB00FC79B1 /* NetworkDevicePhotosResponse.swift */; }; 26FFAD762B8E08CB00BF0A6B /* NetworkDeviceRewardDetailsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FFAD752B8E08CB00BF0A6B /* NetworkDeviceRewardDetailsResponse.swift */; }; 4D1C95CA28FC766D001F66C9 /* DeviceLocationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D1C95C928FC766D001F66C9 /* DeviceLocationRepository.swift */; }; 4D1C95D528FCB248001F66C9 /* DeviceLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D1C95D428FCB248001F66C9 /* DeviceLocation.swift */; }; @@ -137,6 +138,7 @@ 26DF3D2D2C85EA24005F6FC1 /* NetworkDeviceRewardsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDeviceRewardsResponse.swift; sourceTree = ""; }; 26F6F74F2D08540900FB256B /* PhotoGalleryUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGalleryUseCase.swift; sourceTree = ""; }; 26F6F7512D08559800FB256B /* PhotosRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosRepository.swift; sourceTree = ""; }; + 26FC21BF2D12E5AB00FC79B1 /* NetworkDevicePhotosResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDevicePhotosResponse.swift; sourceTree = ""; }; 26FFAD752B8E08CB00BF0A6B /* NetworkDeviceRewardDetailsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDeviceRewardDetailsResponse.swift; sourceTree = ""; }; 4D1C95C928FC766D001F66C9 /* DeviceLocationRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLocationRepository.swift; sourceTree = ""; }; 4D1C95D428FCB248001F66C9 /* DeviceLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLocation.swift; sourceTree = ""; }; @@ -380,6 +382,7 @@ B5611E7E2834D5FC0092F204 /* NetworkFirmwareResponse.swift */, B5667D6A2833D37000C08B57 /* NetworkUserInfoResponse.swift */, 26A3B3DD2B18E41F0002F35F /* NetworkUserRewardsResponse.swift */, + 26FC21BF2D12E5AB00FC79B1 /* NetworkDevicePhotosResponse.swift */, ); path = Network; sourceTree = ""; @@ -616,6 +619,7 @@ 264CC75B2B986AC90060DEA4 /* NetworkDeviceRewardBoostsResponse.swift in Sources */, 995A0CEE28C0B93C0015DAA1 /* UserDefaults+Constants.swift in Sources */, 995A0CD728C0A4BB0015DAA1 /* UserDefaultsRepository.swift in Sources */, + 26FC21C02D12E5AB00FC79B1 /* NetworkDevicePhotosResponse.swift in Sources */, 26A86BC02C7C97F800F1C2CF /* RemoteConfigRepository.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/wxm-ios/DomainLayer/DomainLayer/DomainRepositoryInterfaces/DeviceInfoRepository.swift b/wxm-ios/DomainLayer/DomainLayer/DomainRepositoryInterfaces/DeviceInfoRepository.swift index 8cf8e4ab3..f165b1982 100644 --- a/wxm-ios/DomainLayer/DomainLayer/DomainRepositoryInterfaces/DeviceInfoRepository.swift +++ b/wxm-ios/DomainLayer/DomainLayer/DomainRepositoryInterfaces/DeviceInfoRepository.swift @@ -15,6 +15,7 @@ public protocol DeviceInfoRepository { func disclaimDevice(serialNumber: String) throws -> AnyPublisher, Never> func rebootStation(device: DeviceDetails) -> AnyPublisher func changeFrequency(device: DeviceDetails, frequency: Frequency) -> AnyPublisher + func getDevicePhotos(deviceId: String) throws -> AnyPublisher, Never> } public enum RebootStationState: Sendable { diff --git a/wxm-ios/DomainLayer/DomainLayer/Entities/Codables/Me/Network/NetworkDevicePhotosResponse.swift b/wxm-ios/DomainLayer/DomainLayer/Entities/Codables/Me/Network/NetworkDevicePhotosResponse.swift new file mode 100644 index 000000000..550819f40 --- /dev/null +++ b/wxm-ios/DomainLayer/DomainLayer/Entities/Codables/Me/Network/NetworkDevicePhotosResponse.swift @@ -0,0 +1,12 @@ +// +// NetworkDevicePhotosResponse.swift +// DomainLayer +// +// Created by Pantelis Giazitsis on 18/12/24. +// + +import Foundation + +public struct NetworkDevicePhotosResponse: Sendable, Codable { + public let url: String? +} diff --git a/wxm-ios/DomainLayer/DomainLayer/UseCases/DeviceInfoUseCase.swift b/wxm-ios/DomainLayer/DomainLayer/UseCases/DeviceInfoUseCase.swift index 3d21ea93e..f35324b08 100644 --- a/wxm-ios/DomainLayer/DomainLayer/UseCases/DeviceInfoUseCase.swift +++ b/wxm-ios/DomainLayer/DomainLayer/UseCases/DeviceInfoUseCase.swift @@ -9,7 +9,7 @@ import Foundation import Alamofire import Combine -public struct DeviceInfoUseCase { +public struct DeviceInfoUseCase: @unchecked Sendable { private let repository: DeviceInfoRepository public init(repository: DeviceInfoRepository) { @@ -39,4 +39,8 @@ public struct DeviceInfoUseCase { public func changeFrequency(device: DeviceDetails, frequency: Frequency) -> AnyPublisher { repository.changeFrequency(device: device, frequency: frequency) } + + public func getDevicePhotos(deviceId: String) throws -> AnyPublisher, Never> { + try repository.getDevicePhotos(deviceId: deviceId) + } } diff --git a/wxm-ios/Resources/Colors.xcassets/wxmWhite.colorset/Contents.json b/wxm-ios/Resources/Colors.xcassets/textWhite.colorset/Contents.json similarity index 91% rename from wxm-ios/Resources/Colors.xcassets/wxmWhite.colorset/Contents.json rename to wxm-ios/Resources/Colors.xcassets/textWhite.colorset/Contents.json index a61d481a7..8cdcd1496 100644 --- a/wxm-ios/Resources/Colors.xcassets/wxmWhite.colorset/Contents.json +++ b/wxm-ios/Resources/Colors.xcassets/textWhite.colorset/Contents.json @@ -6,7 +6,7 @@ "components" : { "alpha" : "1.000", "blue" : "0xFF", - "green" : "0xFF", + "green" : "0xFB", "red" : "0xFE" } }, diff --git a/wxm-ios/Resources/Localizable/Localizable.xcstrings b/wxm-ios/Resources/Localizable/Localizable.xcstrings index cd1a14cfa..2678fa1c9 100644 --- a/wxm-ios/Resources/Localizable/Localizable.xcstrings +++ b/wxm-ios/Resources/Localizable/Localizable.xcstrings @@ -52,6 +52,9 @@ } } } + }, + "+%@" : { + }, "about" : { "extractionState" : "manual", @@ -2726,6 +2729,50 @@ } } }, + "device_info_photo_verification_empty_text" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "To ensure optimal performance and rewards, take a few quick photos of your station’s installation!" + } + } + } + }, + "device_info_photo_verification_start_button_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start Photo Verification" + } + } + } + }, + "device_info_photo_verification_uploading_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Once completed, the photos will appear here!" + } + } + } + }, + "device_info_photo_verification_with_photos_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thanks for helping us build a healthier network! Here you can see your photos." + } + } + } + }, "device_info_remove_station_account_confirmation_markdown" : { "extractionState" : "manual", "localizations" : { @@ -4970,12 +5017,6 @@ } } }, - "Key" : { - "extractionState" : "manual" - }, - "Key 1" : { - "extractionState" : "manual" - }, "last_name" : { "extractionState" : "manual", "localizations" : { @@ -5471,6 +5512,17 @@ } } }, + "new" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New" + } + } + } + }, "no" : { "extractionState" : "manual", "localizations" : { @@ -5636,6 +5688,28 @@ } } }, + "photo_verification_cancel_upload" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel upload" + } + } + } + }, + "photo_verification_cancel_upload_alert_message" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to cancel the upload and lose the photos?\nYou will need to retake them." + } + } + } + }, "photo_verification_check_clear_view" : { "extractionState" : "manual", "localizations" : { @@ -5757,6 +5831,17 @@ } } }, + "photo_verification_exit_photo_verification_minimum_photos_text" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "At least 2 photos are required.\nExiting now means starting over next time." + } + } + } + }, "photo_verification_exit_photo_verification_text" : { "extractionState" : "manual", "localizations" : { @@ -6010,6 +6095,17 @@ } } }, + "photo_verification_retry_upload" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry upload" + } + } + } + }, "photo_verification_rotate_instruction" : { "extractionState" : "manual", "localizations" : { @@ -6065,6 +6161,17 @@ } } }, + "photo_verification_upload_error_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "We faced an unexpected error while uploading your photos." + } + } + } + }, "photo_verification_upload_photos" : { "extractionState" : "manual", "localizations" : { @@ -6087,6 +6194,28 @@ } } }, + "photo_verification_uploading" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uploading…" + } + } + } + }, + "photo_verification_yes_cancel" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yes, cancel" + } + } + } + }, "photo_verification_your_camera_will_open" : { "extractionState" : "manual", "localizations" : { diff --git a/wxm-ios/Resources/Localizable/LocalizableConstants.swift b/wxm-ios/Resources/Localizable/LocalizableConstants.swift index d24761d53..21316db93 100644 --- a/wxm-ios/Resources/Localizable/LocalizableConstants.swift +++ b/wxm-ios/Resources/Localizable/LocalizableConstants.swift @@ -14,6 +14,7 @@ protocol WXMLocalizable { enum LocalizableString: WXMLocalizable { case url(String, String) + case new case notAvailable case confirm case email @@ -212,6 +213,8 @@ extension LocalizableString { switch self { case .url: return "url_format" + case .new: + return "new" case .notAvailable: return "not_available" case .confirm: diff --git a/wxm-ios/Resources/Localizable/LocalizableString+DeviceInfo.swift b/wxm-ios/Resources/Localizable/LocalizableString+DeviceInfo.swift index 6e73e4e7d..4dc1eb3a6 100644 --- a/wxm-ios/Resources/Localizable/LocalizableString+DeviceInfo.swift +++ b/wxm-ios/Resources/Localizable/LocalizableString+DeviceInfo.swift @@ -70,6 +70,10 @@ extension LocalizableString { case stationRssi case stationRssiWarning case stationRssiError + case photoVerificationEmptyText + case photoVerificationStartButtonTitle + case photoVerificationUploadingDescription + case photoVerificationWithPhotosDescription } } @@ -218,6 +222,14 @@ extension LocalizableString.DeviceInfo: WXMLocalizable { return "device_info_station_rssi_warning" case .stationRssiError: return "device_info_station_rssi_error" + case .photoVerificationEmptyText: + return "device_info_photo_verification_empty_text" + case .photoVerificationStartButtonTitle: + return "device_info_photo_verification_start_button_title" + case .photoVerificationUploadingDescription: + return "device_info_photo_verification_uploading_description" + case .photoVerificationWithPhotosDescription: + return "device_info_photo_verification_with_photos_description" } } } diff --git a/wxm-ios/Resources/Localizable/LocalizableString+PhotoVerification.swift b/wxm-ios/Resources/Localizable/LocalizableString+PhotoVerification.swift index 7f2f0ba5a..d0c5ee1c5 100644 --- a/wxm-ios/Resources/Localizable/LocalizableString+PhotoVerification.swift +++ b/wxm-ios/Resources/Localizable/LocalizableString+PhotoVerification.swift @@ -54,6 +54,13 @@ extension LocalizableString { case openSettings case exitPhotoVerification case exitPhotoVerificationText + case exitPhotoVerificationMinimumPhotosText + case uploading + case cancelUpload + case cancelUploadAlertMessage + case yesCancel + case retryUpload + case uploadErrorDescription } } @@ -162,6 +169,20 @@ extension LocalizableString.PhotoVerification: WXMLocalizable { "photo_verification_exit_photo_verification" case .exitPhotoVerificationText: "photo_verification_exit_photo_verification_text" + case .exitPhotoVerificationMinimumPhotosText: + "photo_verification_exit_photo_verification_minimum_photos_text" + case .uploading: + "photo_verification_uploading" + case .cancelUpload: + "photo_verification_cancel_upload" + case .cancelUploadAlertMessage: + "photo_verification_cancel_upload_alert_message" + case .yesCancel: + "photo_verification_yes_cancel" + case .retryUpload: + "photo_verification_retry_upload" + case .uploadErrorDescription: + "photo_verification_upload_error_description" } } }