Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Alfie/Alfie/Views/BagView/BagView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Mocks
#endif

struct BagView<ViewModel: BagViewModelProtocol>: View {
@EnvironmentObject var coordinador: Coordinator
@EnvironmentObject var coordinator: Coordinator
@StateObject private var viewModel: ViewModel

init(viewModel: ViewModel) {
Expand All @@ -17,7 +17,7 @@ struct BagView<ViewModel: BagViewModelProtocol>: 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())
Expand Down Expand Up @@ -49,7 +49,7 @@ struct BagView<ViewModel: BagViewModelProtocol>: View {
BagView(
viewModel: MockBagViewModel(
products: [
.init(product: Product.fixture())
.init(selectedProduct: .init(product: Product.fixture()))
]
)
)
Expand Down
6 changes: 3 additions & 3 deletions Alfie/Alfie/Views/BagView/BagViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions Alfie/Alfie/Views/HomeView/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Mocks
#endif

struct HomeView<ViewModel: HomeViewModelProtocol>: View {
@EnvironmentObject var coordinador: Coordinator
@EnvironmentObject var coordinator: Coordinator
@StateObject private var viewModel: ViewModel
@State private var showSearchBar = true
@Namespace private var animation
Expand All @@ -19,7 +19,7 @@ struct HomeView<ViewModel: HomeViewModelProtocol>: View {

var body: some View {
VStack {
if !coordinador.navigationAdapter.isPresentingFullScreenOverlay {
if !coordinator.navigationAdapter.isPresentingFullScreenOverlay {
ThemedSearchBarView(
searchText: .constant(""),
placeholder: L10n.Home.SearchBar.placeholder,
Expand All @@ -30,7 +30,7 @@ struct HomeView<ViewModel: HomeViewModelProtocol>: View {
.matchedGeometryEffect(id: Constants.searchBarGeometryID, in: animation)
.disabled(true)
.onTapGesture {
coordinador.navigationAdapter.presentFullscreenOverlay(
coordinator.navigationAdapter.presentFullscreenOverlay(
with: .search(
transition: .matchedGeometryEffect(
id: Constants.searchBarGeometryID,
Expand Down
2 changes: 1 addition & 1 deletion Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ extension ProductDetailsView {
ThemedButton(
text: viewModel.productHasStock ? addToBagText : outOfStockText,
isDisabled: .init(
get: { !viewModel.productHasStock },
get: { !viewModel.isAddToBagEnabled },
set: { _ in }
),
isFullWidth: true
Expand Down
62 changes: 50 additions & 12 deletions Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -108,7 +119,7 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {

buildColorAndSizingSelectionConfigurations(
product: product,
selectedVariant: product.defaultVariant
selectedVariant: product.defaultVariantWithoutSize
)

case .selectedProduct(let selectedProduct):
Expand Down Expand Up @@ -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] {
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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? {
Expand Down
1 change: 1 addition & 0 deletions Alfie/Alfie/Views/ProductListing/ProductListingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ struct ProductListingView<ViewModel: ProductListingViewModelProtocol>: View {
viewModel: .init(
configuration: .init(
size: viewModel.style == .list ? .large : .medium,
hideSize: true,
hideAction: !coordinator.isWishlistEnabled
),
product: product
Expand Down
14 changes: 7 additions & 7 deletions Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
8 changes: 4 additions & 4 deletions Alfie/Alfie/Views/WishlistView/WishlistView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Mocks
#endif

struct WishlistView<ViewModel: WishlistViewModelProtocol>: View {
@EnvironmentObject var coordinador: Coordinator
@EnvironmentObject var coordinator: Coordinator
@StateObject private var viewModel: ViewModel

init(viewModel: ViewModel) {
Expand All @@ -24,7 +24,7 @@ struct WishlistView<ViewModel: WishlistViewModelProtocol>: View {
) {
ForEach(viewModel.products) { product in
Button(
action: { coordinador.openDetails(for: product) },
action: { coordinator.openDetails(for: product) },
label: {
VerticalProductCard(
viewModel: viewModel.productCardViewModel(for: product)
Expand All @@ -49,13 +49,13 @@ struct WishlistView<ViewModel: WishlistViewModelProtocol>: 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
}
Expand Down
33 changes: 14 additions & 19 deletions Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
)
}
}
14 changes: 7 additions & 7 deletions Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading