diff --git a/Alfie/Alfie.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Alfie/Alfie.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 34b547f5..c12a5c9f 100644 --- a/Alfie/Alfie.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Alfie/Alfie.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -230,8 +230,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", - "version" : "601.0.1" + "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", + "version" : "510.0.3" } }, { diff --git a/Alfie/Alfie/Views/BagView/BagView.swift b/Alfie/Alfie/Views/BagView/BagView.swift index 4e839f6f..ebc2ae64 100644 --- a/Alfie/Alfie/Views/BagView/BagView.swift +++ b/Alfie/Alfie/Views/BagView/BagView.swift @@ -6,7 +6,7 @@ import Mocks #endif struct BagView: View { - @EnvironmentObject var coordinador: Coordinator + @EnvironmentObject var coordinator: Coordinator @StateObject private var viewModel: ViewModel init(viewModel: ViewModel) { @@ -17,7 +17,7 @@ struct BagView: View { List { ForEach(viewModel.products) { product in Button( - action: { coordinador.openDetails(for: product) }, + action: { coordinator.openDetails(for: product) }, label: { HorizontalProductCard(viewModel: viewModel.productCardViewModel(for: product)) .contentShape(Rectangle()) @@ -49,7 +49,7 @@ struct BagView: View { BagView( viewModel: MockBagViewModel( products: [ - .init(product: Product.fixture()) + .init(selectedProduct: .init(product: Product.fixture())) ] ) ) diff --git a/Alfie/Alfie/Views/BagView/BagViewModel.swift b/Alfie/Alfie/Views/BagView/BagViewModel.swift index 676a39e2..bbb6d01f 100644 --- a/Alfie/Alfie/Views/BagView/BagViewModel.swift +++ b/Alfie/Alfie/Views/BagView/BagViewModel.swift @@ -3,7 +3,7 @@ import Models import SharedUI final class BagViewModel: BagViewModelProtocol { - @Published private(set) var products: [SelectedProduct] + @Published private(set) var products: [BagProduct] private let dependencies: BagDependencyContainer @@ -18,13 +18,13 @@ final class BagViewModel: BagViewModelProtocol { products = dependencies.bagService.getBagContent() } - func didSelectDelete(for selectedProduct: SelectedProduct) { + func didSelectDelete(for selectedProduct: BagProduct) { dependencies.bagService.removeProduct(selectedProduct) products = dependencies.bagService.getBagContent() dependencies.analytics.trackRemoveFromBag(productID: selectedProduct.product.id) } - func productCardViewModel(for selectedProduct: SelectedProduct) -> HorizontalProductCardViewModel { + func productCardViewModel(for selectedProduct: BagProduct) -> HorizontalProductCardViewModel { .init( image: selectedProduct.media.first?.asImage?.url, designer: selectedProduct.brand.name, diff --git a/Alfie/Alfie/Views/HomeView/HomeView.swift b/Alfie/Alfie/Views/HomeView/HomeView.swift index 7ca32470..e2f2a35e 100644 --- a/Alfie/Alfie/Views/HomeView/HomeView.swift +++ b/Alfie/Alfie/Views/HomeView/HomeView.swift @@ -8,7 +8,7 @@ import Mocks #endif struct HomeView: View { - @EnvironmentObject var coordinador: Coordinator + @EnvironmentObject var coordinator: Coordinator @StateObject private var viewModel: ViewModel @State private var showSearchBar = true @Namespace private var animation @@ -19,7 +19,7 @@ struct HomeView: View { var body: some View { VStack { - if !coordinador.navigationAdapter.isPresentingFullScreenOverlay { + if !coordinator.navigationAdapter.isPresentingFullScreenOverlay { ThemedSearchBarView( searchText: .constant(""), placeholder: L10n.Home.SearchBar.placeholder, @@ -30,7 +30,7 @@ struct HomeView: View { .matchedGeometryEffect(id: Constants.searchBarGeometryID, in: animation) .disabled(true) .onTapGesture { - coordinador.navigationAdapter.presentFullscreenOverlay( + coordinator.navigationAdapter.presentFullscreenOverlay( with: .search( transition: .matchedGeometryEffect( id: Constants.searchBarGeometryID, diff --git a/Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift b/Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift index fd6e76a5..3882c7ef 100644 --- a/Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift +++ b/Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift @@ -490,7 +490,7 @@ extension ProductDetailsView { ThemedButton( text: viewModel.productHasStock ? addToBagText : outOfStockText, isDisabled: .init( - get: { !viewModel.productHasStock }, + get: { !viewModel.isAddToBagEnabled }, set: { _ in } ), isFullWidth: true diff --git a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift index f3a89ba6..ef6a9f49 100644 --- a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift +++ b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift @@ -5,6 +5,7 @@ import Models import SharedUI import StyleGuide +// swiftlint:disable file_length final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { private let dependencies: ProductDetailsDependencyContainer // In case we already have a full or partial product to show while fetching @@ -28,7 +29,7 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { private var selectedVariant: Product.Variant? { guard case .success(let model) = state else { - return initialSelectedProduct?.selectedVariant ?? baseProduct?.defaultVariant + return initialSelectedProduct?.selectedVariant ?? baseProduct?.defaultVariantWithoutSize } return model.selectedVariant @@ -89,6 +90,16 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { product?.priceType } + var isAddToBagEnabled: Bool { productHasStock && hasColorSelected && hasSizeSelected } + + private var hasColorSelected: Bool { + !colorSelectionConfiguration.items.isEmpty ? selectedVariant?.colour != nil : true + } + + private var hasSizeSelected: Bool { + !sizingSelectionConfiguration.items.isEmpty ? selectedVariant?.size != nil : true + } + init( configuration: ProductDetailsConfiguration, dependencies: ProductDetailsDependencyContainer @@ -108,7 +119,7 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { buildColorAndSizingSelectionConfigurations( product: product, - selectedVariant: product.defaultVariant + selectedVariant: product.defaultVariantWithoutSize ) case .selectedProduct(let selectedProduct): @@ -185,14 +196,16 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { func didTapAddToBag() { guard let selectedProduct else { return } - dependencies.bagService.addProduct(selectedProduct) - dependencies.analytics.trackAddToBag(productID: selectedProduct.id) + let bagProduct = BagProduct(selectedProduct: selectedProduct) + dependencies.bagService.addProduct(bagProduct) + dependencies.analytics.trackAddToBag(productID: bagProduct.id) } func didTapAddToWishlist() { guard let selectedProduct else { return } - dependencies.wishlistService.addProduct(selectedProduct) - dependencies.analytics.trackAddToWishlist(productID: selectedProduct.id) + let wishlistProduct = WishlistProduct(selectedProduct: selectedProduct) + dependencies.wishlistService.addProduct(wishlistProduct) + dependencies.analytics.trackAddToWishlist(productID: wishlistProduct.id) } func colorSwatches(filteredBy searchTerm: String) -> [ColorSwatch] { @@ -227,7 +240,7 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { return } - let selectedVariant = initialSelectedProduct?.selectedVariant ?? product.defaultVariant + let selectedVariant = initialSelectedProduct?.selectedVariant ?? product.defaultVariantWithoutSize buildColorAndSizingSelectionConfigurations(product: product, selectedVariant: selectedVariant) state = .success(.init(product: product, selectedVariant: selectedVariant)) } @@ -307,7 +320,8 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { sizingSelectionConfiguration = .init( selectedTitle: L10n.Product.Size.title + ":", items: sizingSwatches, - selectedItem: selectedSwatch + selectedItem: selectedSwatch, + noItemSelectedTitle: L10n.Product.Size.NoSelection.title ) sizingSelectionSubscription = sizingSelectionConfiguration.$selectedItem .receive(on: dependencies.scheduler) @@ -360,14 +374,26 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { } guard let variant = product.variants.first( - where: { $0.colour?.id == colorSwatch.id && $0.size?.id == selectedVariant?.size?.id } + where: { + $0.colour?.id == colorSwatch.id + && (selectedVariant?.size?.id == nil || $0.size?.id == selectedVariant?.size?.id) + } ) else { log.debug("Unexpected data inconsistency: tried to select color \(colorSwatch.id) on product \(productId) but no variant exists with that color, ignoring selection") return } - state = .success(.init(product: product, selectedVariant: variant)) + let updatedVariant = Product.Variant( + sku: variant.sku, + size: selectedVariant?.size, // Making sure if no size selected, it will not auto select + colour: variant.colour, + attributes: variant.attributes, + stock: variant.stock, + price: variant.price + ) + + state = .success(.init(product: product, selectedVariant: updatedVariant)) } private func didSelect(sizingSwatch: SizingSwatch) { @@ -377,14 +403,26 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { } guard let variant = product.variants.first( - where: { $0.size?.id == sizingSwatch.id && $0.colour?.id == selectedVariant?.colour?.id } + where: { + $0.size?.id == sizingSwatch.id + && (selectedVariant?.colour?.id == nil || $0.colour?.id == selectedVariant?.colour?.id) + } ) else { log.debug("Unexpected data inconsistency: tried to select size \(sizingSwatch.id) on product \(productId) but no variant exists with that size, ignoring selection") return } - state = .success(.init(product: product, selectedVariant: variant)) + let updatedVariant = Product.Variant( + sku: variant.sku, + size: variant.size, + colour: selectedVariant?.colour, // Making sure if no color selected, it will not auto select + attributes: variant.attributes, + stock: variant.stock, + price: variant.price + ) + + state = .success(.init(product: product, selectedVariant: updatedVariant)) } private var selectedProduct: SelectedProduct? { diff --git a/Alfie/Alfie/Views/ProductListing/ProductListingView.swift b/Alfie/Alfie/Views/ProductListing/ProductListingView.swift index a8e4a390..ffba17d0 100644 --- a/Alfie/Alfie/Views/ProductListing/ProductListingView.swift +++ b/Alfie/Alfie/Views/ProductListing/ProductListingView.swift @@ -72,6 +72,7 @@ struct ProductListingView: View { viewModel: .init( configuration: .init( size: viewModel.style == .list ? .large : .medium, + hideSize: true, hideAction: !coordinator.isWishlistEnabled ), product: product diff --git a/Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift b/Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift index 7a7ebccb..1de03bee 100644 --- a/Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift +++ b/Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift @@ -79,18 +79,18 @@ final class ProductListingViewModel: ProductListingViewModelProtocol { func didSelect(_: Product) {} func isFavoriteState(for product: Product) -> Bool { - wishlistContent.contains { $0.product.defaultVariant.sku == product.defaultVariant.sku } + wishlistContent.contains { $0.product.id == product.id } } func didTapAddToWishlist(for product: Product, isFavorite: Bool) { if !isFavorite { - let selectedProduct = SelectedProduct(product: product) - dependencies.wishlistService.addProduct(selectedProduct) - dependencies.analytics.trackAddToWishlist(productID: product.id) + let wishlistProduct = WishlistProduct(product: product) + dependencies.wishlistService.addProduct(wishlistProduct) + dependencies.analytics.trackAddToWishlist(productID: wishlistProduct.id) } else { - let selectedProduct = SelectedProduct(product: product) - dependencies.wishlistService.removeProduct(selectedProduct) - dependencies.analytics.trackRemoveFromWishlist(productID: product.id) + let wishlistProduct = WishlistProduct(product: product) + dependencies.wishlistService.removeProductVariants(wishlistProduct) + dependencies.analytics.trackRemoveFromWishlist(productID: wishlistProduct.id) } wishlistContent = dependencies.wishlistService.getWishlistContent() } diff --git a/Alfie/Alfie/Views/WishlistView/WishlistView.swift b/Alfie/Alfie/Views/WishlistView/WishlistView.swift index 8948df21..e2a3dcaf 100644 --- a/Alfie/Alfie/Views/WishlistView/WishlistView.swift +++ b/Alfie/Alfie/Views/WishlistView/WishlistView.swift @@ -6,7 +6,7 @@ import Mocks #endif struct WishlistView: View { - @EnvironmentObject var coordinador: Coordinator + @EnvironmentObject var coordinator: Coordinator @StateObject private var viewModel: ViewModel init(viewModel: ViewModel) { @@ -24,7 +24,7 @@ struct WishlistView: View { ) { ForEach(viewModel.products) { product in Button( - action: { coordinador.openDetails(for: product) }, + action: { coordinator.openDetails(for: product) }, label: { VerticalProductCard( viewModel: viewModel.productCardViewModel(for: product) @@ -49,13 +49,13 @@ struct WishlistView: View { // MARK: - Private Methods private extension WishlistView { - func handleUserAction(forProduct product: SelectedProduct, actionType: VerticalProductCard.ProductUserActionType) { + func handleUserAction(forProduct product: WishlistProduct, actionType: VerticalProductCard.ProductUserActionType) { // swiftlint:disable vertical_whitespace_between_cases switch actionType { case .remove: viewModel.didSelectDelete(for: product) case .addToBag: - viewModel.didTapAddToBag(for: product) + coordinator.openDetails(for: product) case .wishlist: return } diff --git a/Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift b/Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift index cbbbef72..4591040d 100644 --- a/Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift +++ b/Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift @@ -3,7 +3,7 @@ import Models import SharedUI final class WishlistViewModel: WishlistViewModelProtocol { - @Published private(set) var products: [SelectedProduct] + @Published private(set) var products: [WishlistProduct] private let dependencies: WishlistDependencyContainer @@ -18,32 +18,27 @@ final class WishlistViewModel: WishlistViewModelProtocol { products = dependencies.wishlistService.getWishlistContent() } - func didSelectDelete(for selectedProduct: SelectedProduct) { - dependencies.wishlistService.removeProduct(selectedProduct) - dependencies.analytics.trackRemoveFromWishlist(productID: selectedProduct.product.id) + func didSelectDelete(for wishlistProduct: WishlistProduct) { + dependencies.wishlistService.removeProduct(wishlistProduct) + dependencies.analytics.trackRemoveFromWishlist(productID: wishlistProduct.product.id) products = dependencies.wishlistService.getWishlistContent() } - func didTapAddToBag(for selectedProduct: SelectedProduct) { - dependencies.bagService.addProduct(selectedProduct) - dependencies.analytics.trackAddToBag(productID: selectedProduct.product.id) - } - - func productCardViewModel(for selectedProduct: SelectedProduct) -> VerticalProductCardViewModel { + func productCardViewModel(for wishlistProduct: WishlistProduct) -> VerticalProductCardViewModel { .init( - configuration: .init(size: .medium, hideDetails: false, actionType: .remove), - productId: selectedProduct.id, - image: selectedProduct.media.first?.asImage?.url, - designer: selectedProduct.brand.name, - name: selectedProduct.name, - priceType: selectedProduct.priceType, + configuration: .init(size: .medium, hideSize: true, actionType: .remove), + productId: wishlistProduct.id, + image: wishlistProduct.media.first?.asImage?.url, + designer: wishlistProduct.brand.name, + name: wishlistProduct.name, + priceType: wishlistProduct.priceType, colorTitle: L10n.Product.Color.title + ":", - color: selectedProduct.colour?.name ?? "", + color: wishlistProduct.colour?.name ?? "", sizeTitle: L10n.Product.Size.title + ":", - size: selectedProduct.size == nil ? L10n.Product.OneSize.title : selectedProduct.sizeText, + size: wishlistProduct.size == nil ? L10n.Product.OneSize.title : wishlistProduct.sizeText, addToBagTitle: L10n.Product.AddToBag.Button.cta, outOfStockTitle: L10n.Product.OutOfStock.Button.cta, - isAddToBagDisabled: selectedProduct.stock == .zero + isAddToBagDisabled: wishlistProduct.stock == .zero ) } } diff --git a/Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift b/Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift index 22501648..9cb0b32c 100644 --- a/Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift +++ b/Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift @@ -3,21 +3,21 @@ import Models // TODO: Update with an actual implementation with storage public final class BagService: BagServiceProtocol { - private var products: [SelectedProduct] = [] + private var products: [BagProduct] = [] public init() { } - public func addProduct(_ product: SelectedProduct) { - guard !products.contains(where: { $0.id == product.id }) else { return } + public func addProduct(_ bagProduct: BagProduct) { + guard !products.contains(where: { $0.id == bagProduct.id }) else { return } - products.append(product) + products.append(bagProduct) } - public func removeProduct(_ product: SelectedProduct) { - products = products.filter { $0.id != product.id } + public func removeProduct(_ bagProduct: BagProduct) { + products = products.filter { $0.id != bagProduct.id } } - public func getBagContent() -> [SelectedProduct] { + public func getBagContent() -> [BagProduct] { products } } diff --git a/Alfie/AlfieKit/Sources/Core/Services/Wishlist/WishlistService.swift b/Alfie/AlfieKit/Sources/Core/Services/Wishlist/WishlistService.swift index efa00e2e..27f3ea98 100644 --- a/Alfie/AlfieKit/Sources/Core/Services/Wishlist/WishlistService.swift +++ b/Alfie/AlfieKit/Sources/Core/Services/Wishlist/WishlistService.swift @@ -3,21 +3,25 @@ import Models // TODO: Update with an actual implementation with storage public final class WishlistService: WishlistServiceProtocol { - private var products: [SelectedProduct] = [] + private var products: [WishlistProduct] = [] public init() { } - public func addProduct(_ product: SelectedProduct) { - guard !products.contains(where: { $0.id == product.id }) else { return } + public func addProduct(_ wishlistProduct: WishlistProduct) { + guard !products.contains(where: { $0.id == wishlistProduct.id }) else { return } - products.append(product) + products.append(wishlistProduct) } - public func removeProduct(_ product: SelectedProduct) { - products = products.filter { $0.id != product.id } + public func removeProduct(_ wishlistProduct: WishlistProduct) { + products = products.filter { $0.id != wishlistProduct.id } } - public func getWishlistContent() -> [SelectedProduct] { + public func removeProductVariants(_ wishlistProduct: WishlistProduct) { + products = products.filter { $0.product.id != wishlistProduct.product.id } + } + + public func getWishlistContent() -> [WishlistProduct] { products } } diff --git a/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockBagViewModel.swift b/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockBagViewModel.swift index b3a95e52..34d3f1b5 100644 --- a/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockBagViewModel.swift +++ b/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockBagViewModel.swift @@ -1,9 +1,9 @@ import Models public class MockBagViewModel: BagViewModelProtocol { - public var products: [SelectedProduct] + public var products: [BagProduct] - public init(products: [SelectedProduct] = []) { + public init(products: [BagProduct] = []) { self.products = products } @@ -12,13 +12,13 @@ public class MockBagViewModel: BagViewModelProtocol { onViewDidAppearCalled?() } - public var onDidSelectDeleteCalled: ((SelectedProduct) -> Void)? - public func didSelectDelete(for selectedProduct: SelectedProduct) { + public var onDidSelectDeleteCalled: ((BagProduct) -> Void)? + public func didSelectDelete(for selectedProduct: BagProduct) { onDidSelectDeleteCalled?(selectedProduct) } - public var onProductCardViewModelCalled: ((SelectedProduct) -> HorizontalProductCardViewModel)? - public func productCardViewModel(for selectedProduct: SelectedProduct) -> HorizontalProductCardViewModel { + public var onProductCardViewModelCalled: ((BagProduct) -> HorizontalProductCardViewModel)? + public func productCardViewModel(for selectedProduct: BagProduct) -> HorizontalProductCardViewModel { onProductCardViewModelCalled?(selectedProduct) ?? .init( image: nil, designer: "Yves Saint Laurent", diff --git a/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockProductDetailsViewModel.swift b/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockProductDetailsViewModel.swift index 383a12eb..61f4c9e9 100644 --- a/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockProductDetailsViewModel.swift +++ b/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockProductDetailsViewModel.swift @@ -18,6 +18,7 @@ public class MockProductDetailsViewModel: ProductDetailsViewModelProtocol { public var shouldShowMediaPaginatedControl = true public var hasSingleImage: Bool = false public var priceType: PriceType? = nil + public var isAddToBagEnabled: Bool = true public init(state: ViewState = .loading, productId: String = "", diff --git a/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockBagService.swift b/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockBagService.swift index e760f454..ead71e2b 100644 --- a/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockBagService.swift +++ b/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockBagService.swift @@ -2,21 +2,21 @@ import Foundation import Models public final class MockBagService: BagServiceProtocol { - private var products: [SelectedProduct] = [] + private var products: [BagProduct] = [] public init() { } - public func addProduct(_ product: SelectedProduct) { - guard !products.contains(where: { $0.id == product.id }) else { return } + public func addProduct(_ bagProduct: BagProduct) { + guard !products.contains(where: { $0.id == bagProduct.id }) else { return } - products.append(product) + products.append(bagProduct) } - public func removeProduct(_ product: SelectedProduct) { - products = products.filter { $0.id != product.id } + public func removeProduct(_ bagProduct: BagProduct) { + products = products.filter { $0.id != bagProduct.id } } - public func getBagContent() -> [SelectedProduct] { + public func getBagContent() -> [BagProduct] { products } } diff --git a/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockWishlistService.swift b/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockWishlistService.swift index dc255971..e3e0ddfa 100644 --- a/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockWishlistService.swift +++ b/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockWishlistService.swift @@ -2,21 +2,25 @@ import Foundation import Models public final class MockWishlistService: WishlistServiceProtocol { - private var products: [SelectedProduct] = [] + private var products: [WishlistProduct] = [] public init() { } - public func addProduct(_ product: SelectedProduct) { - guard !products.contains(where: { $0.id == product.id }) else { return } + public func addProduct(_ wishlistProduct: WishlistProduct) { + guard !products.contains(where: { $0.id == wishlistProduct.id }) else { return } - products.append(product) + products.append(wishlistProduct) } - public func removeProduct(_ product: SelectedProduct) { - products = products.filter { $0.id != product.id } + public func removeProduct(_ wishlistProduct: WishlistProduct) { + products = products.filter { $0.id != wishlistProduct.id } } - public func getWishlistContent() -> [SelectedProduct] { + public func removeProductVariants(_ wishlistProduct: WishlistProduct) { + products = products.filter { $0.product.id != wishlistProduct.product.id } + } + + public func getWishlistContent() -> [WishlistProduct] { products } } diff --git a/Alfie/AlfieKit/Sources/Models/Features/BagViewModelProtocol.swift b/Alfie/AlfieKit/Sources/Models/Features/BagViewModelProtocol.swift index 41fdb4a1..d26ec992 100644 --- a/Alfie/AlfieKit/Sources/Models/Features/BagViewModelProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Features/BagViewModelProtocol.swift @@ -1,9 +1,9 @@ import Foundation public protocol BagViewModelProtocol: ObservableObject { - var products: [SelectedProduct] { get } + var products: [BagProduct] { get } func viewDidAppear() - func didSelectDelete(for selectedProduct: SelectedProduct) - func productCardViewModel(for selectedProduct: SelectedProduct) -> HorizontalProductCardViewModel + func didSelectDelete(for selectedProduct: BagProduct) + func productCardViewModel(for selectedProduct: BagProduct) -> HorizontalProductCardViewModel } diff --git a/Alfie/AlfieKit/Sources/Models/Features/ProductDetailsViewModelProtocol.swift b/Alfie/AlfieKit/Sources/Models/Features/ProductDetailsViewModelProtocol.swift index 0b4f116e..2ed3591e 100644 --- a/Alfie/AlfieKit/Sources/Models/Features/ProductDetailsViewModelProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Features/ProductDetailsViewModelProtocol.swift @@ -68,6 +68,7 @@ public protocol ProductDetailsViewModelProtocol: ObservableObject { var shouldShowMediaPaginatedControl: Bool { get } var hasSingleImage: Bool { get } var priceType: PriceType? { get } + var isAddToBagEnabled: Bool { get } func viewDidAppear() func shouldShow(section: ProductDetailsSection) -> Bool diff --git a/Alfie/AlfieKit/Sources/Models/Features/WishlistViewModelProtocol.swift b/Alfie/AlfieKit/Sources/Models/Features/WishlistViewModelProtocol.swift index c69cabcf..09022e74 100644 --- a/Alfie/AlfieKit/Sources/Models/Features/WishlistViewModelProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Features/WishlistViewModelProtocol.swift @@ -1,10 +1,9 @@ import Foundation public protocol WishlistViewModelProtocol: ObservableObject { - var products: [SelectedProduct] { get } + var products: [WishlistProduct] { get } func viewDidAppear() - func didSelectDelete(for selectedProduct: SelectedProduct) - func didTapAddToBag(for selectedProduct: SelectedProduct) - func productCardViewModel(for selectedProduct: SelectedProduct) -> VerticalProductCardViewModel + func didSelectDelete(for wishlistProduct: WishlistProduct) + func productCardViewModel(for wishlistProduct: WishlistProduct) -> VerticalProductCardViewModel } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift deleted file mode 100644 index a8ece74d..00000000 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation - -public struct SelectedProduct { - public let product: Product - public let selectedVariant: Product.Variant - - public var name: String { - product.name - } - - public var brand: Brand { - product.brand - } - - public var size: Product.ProductSize? { - selectedVariant.size - } - - public var colour: Product.Colour? { - selectedVariant.colour - } - - public var stock: Int { - selectedVariant.stock - } - - public var price: Price { - selectedVariant.price - } - - public var media: [Media] { - colour?.media ?? [] - } - - public init(product: Product, selectedVariant: Product.Variant? = nil) { - self.product = product - self.selectedVariant = selectedVariant ?? product.defaultVariant - } -} - -extension SelectedProduct: Identifiable, Hashable { - public var id: String { - "\(product.id)-\(selectedVariant.sku)" - } -} diff --git a/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift b/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift index 99e7905f..f1ae4969 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift @@ -27,6 +27,21 @@ public struct Product: Identifiable, Hashable { /// Colour objects also include the Product media for each color. public let colours: [Colour]? + /// The 'default' variant but without a size. + /// + /// ⚠️ The default variant should only have a color associated, not a size, but given the current API model that isn't happening, so + /// we enforce an unnatural way of handling product variants. This should be refactored once the new backend model is available. + public var defaultVariantWithoutSize: Variant { + .init( + sku: defaultVariant.sku, + size: nil, + colour: defaultVariant.colour, + attributes: defaultVariant.attributes, + stock: defaultVariant.stock, + price: defaultVariant.price + ) + } + public init( id: String, styleNumber: String, diff --git a/Alfie/AlfieKit/Sources/Models/Models/Product/SelectedProduct/BagProduct.swift b/Alfie/AlfieKit/Sources/Models/Models/Product/SelectedProduct/BagProduct.swift new file mode 100644 index 00000000..e2623f1f --- /dev/null +++ b/Alfie/AlfieKit/Sources/Models/Models/Product/SelectedProduct/BagProduct.swift @@ -0,0 +1,7 @@ +import Foundation + +public class BagProduct: SelectedProduct { + public init(selectedProduct: SelectedProduct) { + super.init(product: selectedProduct.product, selectedVariant: selectedProduct.selectedVariant) + } +} diff --git a/Alfie/AlfieKit/Sources/Models/Models/Product/SelectedProduct/SelectedProduct.swift b/Alfie/AlfieKit/Sources/Models/Models/Product/SelectedProduct/SelectedProduct.swift new file mode 100644 index 00000000..bc0d3910 --- /dev/null +++ b/Alfie/AlfieKit/Sources/Models/Models/Product/SelectedProduct/SelectedProduct.swift @@ -0,0 +1,28 @@ +import Foundation + +public class SelectedProduct: Identifiable, Hashable { + public let product: Product + public let selectedVariant: Product.Variant + public var id: String { "\(product.id)-\(selectedVariant.sku)" } + public var name: String { product.name } + public var brand: Brand { product.brand } + public var size: Product.ProductSize? { selectedVariant.size } + public var colour: Product.Colour? { selectedVariant.colour } + public var stock: Int { selectedVariant.stock } + public var price: Price { selectedVariant.price } + public var media: [Media] { colour?.media ?? [] } + + public init(product: Product, selectedVariant: Product.Variant? = nil) { + self.product = product + self.selectedVariant = selectedVariant ?? product.defaultVariant + } + + public static func == (lhs: SelectedProduct, rhs: SelectedProduct) -> Bool { + lhs.product == rhs.product && lhs.selectedVariant == rhs.selectedVariant + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(product) + hasher.combine(selectedVariant) + } +} diff --git a/Alfie/AlfieKit/Sources/Models/Models/Product/SelectedProduct/WishlistProduct.swift b/Alfie/AlfieKit/Sources/Models/Models/Product/SelectedProduct/WishlistProduct.swift new file mode 100644 index 00000000..d9db60fd --- /dev/null +++ b/Alfie/AlfieKit/Sources/Models/Models/Product/SelectedProduct/WishlistProduct.swift @@ -0,0 +1,31 @@ +import Foundation + +public class WishlistProduct: SelectedProduct { + override public var id: String { "\(product.id)-\(colour?.id ?? "no colour")" } + + public init(selectedProduct: SelectedProduct) { + let updatedVariant = Product.Variant( + sku: selectedProduct.selectedVariant.sku, + size: nil, // Forcing size to not be included + colour: selectedProduct.selectedVariant.colour, + attributes: selectedProduct.selectedVariant.attributes, + stock: selectedProduct.selectedVariant.stock, + price: selectedProduct.selectedVariant.price + ) + + super.init(product: selectedProduct.product, selectedVariant: updatedVariant) + } + + public init(product: Product) { + let updatedVariant = Product.Variant( + sku: product.defaultVariant.sku, + size: nil, // Forcing size to not be included + colour: product.defaultVariant.colour, + attributes: product.defaultVariant.attributes, + stock: product.defaultVariant.stock, + price: product.defaultVariant.price + ) + + super.init(product: product, selectedVariant: updatedVariant) + } +} diff --git a/Alfie/AlfieKit/Sources/Models/Services/Bag/BagServiceProtocol.swift b/Alfie/AlfieKit/Sources/Models/Services/Bag/BagServiceProtocol.swift index 05912e09..02c36deb 100644 --- a/Alfie/AlfieKit/Sources/Models/Services/Bag/BagServiceProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Services/Bag/BagServiceProtocol.swift @@ -1,7 +1,7 @@ import Foundation public protocol BagServiceProtocol { - func addProduct(_ product: SelectedProduct) - func removeProduct(_ product: SelectedProduct) - func getBagContent() -> [SelectedProduct] + func addProduct(_ bagProduct: BagProduct) + func removeProduct(_ bagProduct: BagProduct) + func getBagContent() -> [BagProduct] } diff --git a/Alfie/AlfieKit/Sources/Models/Services/Wishlist/WishlistServiceProtocol.swift b/Alfie/AlfieKit/Sources/Models/Services/Wishlist/WishlistServiceProtocol.swift index 6b8bc58d..c6caae77 100644 --- a/Alfie/AlfieKit/Sources/Models/Services/Wishlist/WishlistServiceProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Services/Wishlist/WishlistServiceProtocol.swift @@ -1,7 +1,8 @@ import Foundation public protocol WishlistServiceProtocol { - func addProduct(_ product: SelectedProduct) - func removeProduct(_ product: SelectedProduct) - func getWishlistContent() -> [SelectedProduct] + func addProduct(_ wishlistProduct: WishlistProduct) + func removeProduct(_ wishlistProduct: WishlistProduct) + func removeProductVariants(_ wishlistProduct: WishlistProduct) + func getWishlistContent() -> [WishlistProduct] } diff --git a/Alfie/AlfieKit/Sources/Models/UI/ColorAndSizingSelectorConfiguration.swift b/Alfie/AlfieKit/Sources/Models/UI/ColorAndSizingSelectorConfiguration.swift index a467ebe0..943ca52b 100644 --- a/Alfie/AlfieKit/Sources/Models/UI/ColorAndSizingSelectorConfiguration.swift +++ b/Alfie/AlfieKit/Sources/Models/UI/ColorAndSizingSelectorConfiguration.swift @@ -7,10 +7,18 @@ public class ColorAndSizingSelectorConfiguration