Skip to content
Draft
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
3 changes: 0 additions & 3 deletions Alfie/Alfie/Views/BagView/BagView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ struct BagView<ViewModel: BagViewModelProtocol>: View {
.listRowSpacing(Spacing.space200)
.padding(.vertical, Spacing.space200)
.withToolbar(for: .tab(.bag))
.onAppear {
viewModel.viewDidAppear()
}
}
}

Expand Down
22 changes: 14 additions & 8 deletions Alfie/Alfie/Views/BagView/BagViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import Combine
import Foundation
import Models
import SharedUI

final class BagViewModel: BagViewModelProtocol {
@Published private(set) var products: [BagProduct]

@Published private(set) var products: [BagProduct] = []
private var subscriptions = Set<AnyCancellable>()
private let dependencies: BagDependencyContainer

init(dependencies: BagDependencyContainer) {
self.dependencies = dependencies
products = dependencies.bagService.getBagContent()
}

// MARK: - BagViewModelProtocol
setupBindigs()
}

func viewDidAppear() {
products = dependencies.bagService.getBagContent()
private func setupBindigs() {
dependencies.bagService.productsPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] bagProducts in
self?.products = bagProducts
}
.store(in: &subscriptions)
}

// MARK: - BagViewModelProtocol

func didSelectDelete(for selectedProduct: BagProduct) {
dependencies.bagService.removeProduct(selectedProduct)
products = dependencies.bagService.getBagContent()
dependencies.analytics.trackRemoveFromBag(productID: selectedProduct.product.id)
}

Expand Down
19 changes: 8 additions & 11 deletions Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ struct ProductDetailsView<ViewModel: ProductDetailsViewModelProtocol>: View {
complementaryViews
.padding(.horizontal, horizontalPadding)
}
addToBag
bagButton
}
.fullScreenCover(isPresented: $isMediaFullScreen) {
fullscreenMediaCarousel
Expand Down Expand Up @@ -255,7 +255,7 @@ extension ProductDetailsView {
}

VStack {
addToBag
bagButton
addToWishlist
}
.padding(.vertical, Spacing.space100)
Expand Down Expand Up @@ -481,14 +481,11 @@ extension ProductDetailsView {
}
}

@ViewBuilder private var addToBag: some View {
if viewModel.shouldShow(section: .addToBag) {
@ViewBuilder private var bagButton: some View {
if viewModel.shouldShow(section: .bagButton) {
VStack(spacing: Spacing.space0) {
let addToBagText = L10n.Product.AddToBag.Button.cta
let outOfStockText = L10n.Product.OutOfStock.Button.cta

ThemedButton(
text: viewModel.productHasStock ? addToBagText : outOfStockText,
text: viewModel.bagButtonTitle,
isDisabled: .init(
get: { !viewModel.isAddToBagEnabled },
set: { _ in }
Expand All @@ -502,10 +499,10 @@ extension ProductDetailsView {
}

@ViewBuilder private var addToWishlist: some View {
if viewModel.shouldShow(section: .addToWishlist) {
if viewModel.shouldShow(section: .wishlistButton) {
VStack(spacing: Spacing.space0) {
ThemedButton(
text: L10n.Product.AddToWishlist.Button.cta,
text: viewModel.wishlistButtonTitle,
style: .secondary,
isFullWidth: true
) {
Expand Down Expand Up @@ -608,7 +605,7 @@ private enum Constants {
viewModel: MockProductDetailsViewModel(
complementaryInfoToShow: [.paymentOptions, .returns],
onShouldShowLoadingForSectionCalled: { _ in true },
onShouldShowSectionCalled: { section in section != .addToBag }
onShouldShowSectionCalled: { section in section != .bagButton }
)
)
.environmentObject(Coordinator())
Expand Down
85 changes: 77 additions & 8 deletions Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
private(set) var sizingSelectionConfiguration: ColorAndSizingSelectorConfiguration<SizingSwatch> = .init(items: [])
public let productId: String
private let initialSelectedProduct: SelectedProduct?
private var subscriptions = Set<AnyCancellable>()

private var product: Product? {
guard case .success(let model) = state else {
Expand Down Expand Up @@ -100,6 +101,9 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
!sizingSelectionConfiguration.items.isEmpty ? selectedVariant?.size != nil : true
}

@Published var bagButtonTitle: String = ""
@Published var wishlistButtonTitle: String = ""

init(
configuration: ProductDetailsConfiguration,
dependencies: ProductDetailsDependencyContainer
Expand Down Expand Up @@ -132,6 +136,59 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
selectedVariant: selectedProduct.selectedVariant
)
}

setupBindings()
}

private func setupBindings() {
Publishers.CombineLatest(
$state,
dependencies.bagService.productsPublisher
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateBagButtonTitle()
}
.store(in: &subscriptions)

Publishers.CombineLatest(
$state,
dependencies.wishlistService.productsPublisher
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateWishlistButtonTitle()
}
.store(in: &subscriptions)
}

private func updateBagButtonTitle() {
guard productHasStock else {
bagButtonTitle = L10n.Product.OutOfStock.Button.cta
return
}

guard let selectedProduct else {
bagButtonTitle = L10n.Product.AddToBag.Button.cta
return
}

let bagProduct = BagProduct(selectedProduct: selectedProduct)
bagButtonTitle = dependencies.bagService.containsProduct(bagProduct)
? L10n.Product.RemoveFromBag.Button.cta
: L10n.Product.AddToBag.Button.cta
}

private func updateWishlistButtonTitle() {
guard let selectedProduct else {
wishlistButtonTitle = L10n.Product.AddToWishlist.Button.cta
return
}

let wishlistProduct = WishlistProduct(selectedProduct: selectedProduct)
wishlistButtonTitle = dependencies.wishlistService.containsProduct(wishlistProduct)
? L10n.Product.RemoveFromWishlist.Button.cta
: L10n.Product.AddToWishlist.Button.cta
}

func viewDidAppear() {
Expand All @@ -150,8 +207,8 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
.complementaryInfo:
return state.isLoading
case .productDescription,
.addToBag, // swiftlint:disable:this indentation_width
.addToWishlist:
.bagButton, // swiftlint:disable:this indentation_width
.wishlistButton:
return false
}
}
Expand All @@ -169,9 +226,9 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
return state.isLoading || !productImageUrls.isEmpty
case .productDescription:
return !productDescription.isEmpty
case .addToBag:
case .bagButton:
return state.isSuccess
case .addToWishlist:
case .wishlistButton:
return state.isSuccess && dependencies.configurationService.isFeatureEnabled(.wishlist)
}
// swiftlint:enable vertical_whitespace_between_cases
Expand All @@ -197,15 +254,27 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
func didTapAddToBag() {
guard let selectedProduct else { return }
let bagProduct = BagProduct(selectedProduct: selectedProduct)
dependencies.bagService.addProduct(bagProduct)
dependencies.analytics.trackAddToBag(productID: bagProduct.id)

if dependencies.bagService.containsProduct(bagProduct) {
dependencies.bagService.removeProduct(bagProduct)
dependencies.analytics.trackRemoveFromBag(productID: bagProduct.id)
} else {
dependencies.bagService.addProduct(bagProduct)
dependencies.analytics.trackAddToBag(productID: bagProduct.id)
}
}

func didTapAddToWishlist() {
guard let selectedProduct else { return }
let wishlistProduct = WishlistProduct(selectedProduct: selectedProduct)
dependencies.wishlistService.addProduct(wishlistProduct)
dependencies.analytics.trackAddToWishlist(productID: wishlistProduct.id)

if dependencies.wishlistService.containsProduct(wishlistProduct) {
dependencies.wishlistService.removeProduct(wishlistProduct)
dependencies.analytics.trackRemoveFromWishlist(productID: wishlistProduct.id)
} else {
dependencies.wishlistService.addProduct(wishlistProduct)
dependencies.analytics.trackAddToWishlist(productID: wishlistProduct.id)
}
}

func colorSwatches(filteredBy searchTerm: String) -> [ColorSwatch] {
Expand Down
20 changes: 15 additions & 5 deletions Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ final class ProductListingViewModel: ProductListingViewModelProtocol {
@Published var style: ProductListingListStyle
@Published var showRefine = false
@Published var sortOption: String?
@Published private(set) var wishlistContent: [SelectedProduct]
@Published private(set) var wishlistContent: [SelectedProduct] = []
@Published private(set) var state: PaginatedViewState<ProductListingViewStateModel, ProductListingViewErrorType>
private var subscriptions = Set<AnyCancellable>()

private enum Constants {
static let defaultSkeletonItemsSize = 12
Expand Down Expand Up @@ -54,11 +55,20 @@ final class ProductListingViewModel: ProductListingViewModelProtocol {
sortOption = sort
query = searchText ?? urlQueryParameters.map(\.values)?.joined(separator: ",")
state = .loadingFirstPage(.init(title: "", products: .skeleton(itemsSize: skeletonItemsSize)))
wishlistContent = dependencies.wishlistService.getWishlistContent()

setupBindings()
}

private func setupBindings() {
dependencies.wishlistService.productsPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] wishListProducts in
self?.wishlistContent = wishListProducts
}
.store(in: &subscriptions)
}

func viewDidAppear() {
wishlistContent = dependencies.wishlistService.getWishlistContent()
Task {
await loadProductsIfNeeded()
}
Expand All @@ -79,7 +89,8 @@ final class ProductListingViewModel: ProductListingViewModelProtocol {
func didSelect(_: Product) {}

func isFavoriteState(for product: Product) -> Bool {
wishlistContent.contains { $0.product.id == product.id }
let wishlistProduct = WishlistProduct(product: product)
return dependencies.wishlistService.containsProduct(wishlistProduct)
}

func didTapAddToWishlist(for product: Product, isFavorite: Bool) {
Expand All @@ -92,7 +103,6 @@ final class ProductListingViewModel: ProductListingViewModelProtocol {
dependencies.wishlistService.removeProductVariants(wishlistProduct)
dependencies.analytics.trackRemoveFromWishlist(productID: wishlistProduct.id)
}
wishlistContent = dependencies.wishlistService.getWishlistContent()
}

func didApplyFilters() {
Expand Down
3 changes: 0 additions & 3 deletions Alfie/Alfie/Views/WishlistView/WishlistView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ struct WishlistView<ViewModel: WishlistViewModelProtocol>: View {
.padding(.horizontal, Spacing.space200)
}
.padding(.vertical, Spacing.space200)
.onAppear {
viewModel.viewDidAppear()
}
}
}

Expand Down
22 changes: 14 additions & 8 deletions Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import Combine
import Foundation
import Models
import SharedUI

final class WishlistViewModel: WishlistViewModelProtocol {
@Published private(set) var products: [WishlistProduct]

@Published private(set) var products: [WishlistProduct] = []
private var subscriptions = Set<AnyCancellable>()
private let dependencies: WishlistDependencyContainer

init(dependencies: WishlistDependencyContainer) {
self.dependencies = dependencies
products = dependencies.wishlistService.getWishlistContent()
}

// MARK: - WishListViewModelProtocol
setupBindigs()
}

func viewDidAppear() {
products = dependencies.wishlistService.getWishlistContent()
private func setupBindigs() {
dependencies.wishlistService.productsPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] wishListProducts in
self?.products = wishListProducts
}
.store(in: &subscriptions)
}

// MARK: - WishListViewModelProtocol

func didSelectDelete(for wishlistProduct: WishlistProduct) {
dependencies.wishlistService.removeProduct(wishlistProduct)
dependencies.analytics.trackRemoveFromWishlist(productID: wishlistProduct.product.id)
products = dependencies.wishlistService.getWishlistContent()
}

func productCardViewModel(for wishlistProduct: WishlistProduct) -> VerticalProductCardViewModel {
Expand Down
11 changes: 8 additions & 3 deletions Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import Combine
import Foundation
import Models

// TODO: Update with an actual implementation with storage
public final class BagService: BagServiceProtocol {
private var products: [BagProduct] = []
@Published private var products: [BagProduct] = []

public var productsPublisher: AnyPublisher<[BagProduct], Never> {
$products.eraseToAnyPublisher()
}

public init() { }

Expand All @@ -17,7 +22,7 @@ public final class BagService: BagServiceProtocol {
products = products.filter { $0.id != bagProduct.id }
}

public func getBagContent() -> [BagProduct] {
products
public func containsProduct(_ bagProduct: BagProduct) -> Bool {
products.contains { $0.id == bagProduct.id }
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import Combine
import Foundation
import Models

// TODO: Update with an actual implementation with storage
public final class WishlistService: WishlistServiceProtocol {
private var products: [WishlistProduct] = []
@Published private var products: [WishlistProduct] = []

public var productsPublisher: AnyPublisher<[WishlistProduct], Never> {
$products.eraseToAnyPublisher()
}

public init() { }

Expand All @@ -21,7 +26,7 @@ public final class WishlistService: WishlistServiceProtocol {
products = products.filter { $0.product.id != wishlistProduct.product.id }
}

public func getWishlistContent() -> [WishlistProduct] {
products
public func containsProduct(_ wishlistProduct: WishlistProduct) -> Bool {
products.contains { $0.id == wishlistProduct.id }
}
}
Loading