Skip to content

Commit 060e707

Browse files
ALFMOB-158: Update PDP Button Copy Based on Item State
- Added `productsPublisher` to both `WishlistService` and `BagService` to enable real-time updates of button titles based on item state. - Updated all relevant view models to observe `productsPublisher` instead of using the previous `getWishlistContent` and `getBagContent` methods. - Adjusted button titles dynamically to reflect the correct state (`"Add to Bag"` / `"Remove from Bag"` and `"Add to Wishlist"` / `"Remove from Wishlist"`). - Ensured that changing color resets both buttons, while changing size only resets the `"Add to Bag"` button (since wishlist state is unaffected by size). - Added necessary localized strings to `L10n` table.
1 parent e7722e1 commit 060e707

22 files changed

+214
-73
lines changed

Alfie/Alfie/Views/BagView/BagView.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@ struct BagView<ViewModel: BagViewModelProtocol>: View {
3838
.listRowSpacing(Spacing.space200)
3939
.padding(.vertical, Spacing.space200)
4040
.withToolbar(for: .tab(.bag))
41-
.onAppear {
42-
viewModel.viewDidAppear()
43-
}
4441
}
4542
}
4643

Alfie/Alfie/Views/BagView/BagViewModel.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
1+
import Combine
12
import Foundation
23
import Models
34
import SharedUI
45

56
final class BagViewModel: BagViewModelProtocol {
6-
@Published private(set) var products: [BagProduct]
7-
7+
@Published private(set) var products: [BagProduct] = []
8+
private var subscriptions = Set<AnyCancellable>()
89
private let dependencies: BagDependencyContainer
910

1011
init(dependencies: BagDependencyContainer) {
1112
self.dependencies = dependencies
12-
products = dependencies.bagService.getBagContent()
13-
}
1413

15-
// MARK: - BagViewModelProtocol
14+
setupBindigs()
15+
}
1616

17-
func viewDidAppear() {
18-
products = dependencies.bagService.getBagContent()
17+
private func setupBindigs() {
18+
dependencies.bagService.productsPublisher
19+
.receive(on: DispatchQueue.main)
20+
.sink { [weak self] bagProducts in
21+
self?.products = bagProducts
22+
}
23+
.store(in: &subscriptions)
1924
}
2025

26+
// MARK: - BagViewModelProtocol
27+
2128
func didSelectDelete(for selectedProduct: BagProduct) {
2229
dependencies.bagService.removeProduct(selectedProduct)
23-
products = dependencies.bagService.getBagContent()
2430
dependencies.analytics.trackRemoveFromBag(productID: selectedProduct.product.id)
2531
}
2632

Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ struct ProductDetailsView<ViewModel: ProductDetailsViewModelProtocol>: View {
181181
complementaryViews
182182
.padding(.horizontal, horizontalPadding)
183183
}
184-
addToBag
184+
bagButton
185185
}
186186
.fullScreenCover(isPresented: $isMediaFullScreen) {
187187
fullscreenMediaCarousel
@@ -255,7 +255,7 @@ extension ProductDetailsView {
255255
}
256256

257257
VStack {
258-
addToBag
258+
bagButton
259259
addToWishlist
260260
}
261261
.padding(.vertical, Spacing.space100)
@@ -481,14 +481,11 @@ extension ProductDetailsView {
481481
}
482482
}
483483

484-
@ViewBuilder private var addToBag: some View {
485-
if viewModel.shouldShow(section: .addToBag) {
484+
@ViewBuilder private var bagButton: some View {
485+
if viewModel.shouldShow(section: .bagButton) {
486486
VStack(spacing: Spacing.space0) {
487-
let addToBagText = L10n.Product.AddToBag.Button.cta
488-
let outOfStockText = L10n.Product.OutOfStock.Button.cta
489-
490487
ThemedButton(
491-
text: viewModel.productHasStock ? addToBagText : outOfStockText,
488+
text: viewModel.bagButtonTitle,
492489
isDisabled: .init(
493490
get: { !viewModel.isAddToBagEnabled },
494491
set: { _ in }
@@ -502,10 +499,10 @@ extension ProductDetailsView {
502499
}
503500

504501
@ViewBuilder private var addToWishlist: some View {
505-
if viewModel.shouldShow(section: .addToWishlist) {
502+
if viewModel.shouldShow(section: .wishlistButton) {
506503
VStack(spacing: Spacing.space0) {
507504
ThemedButton(
508-
text: L10n.Product.AddToWishlist.Button.cta,
505+
text: viewModel.wishlistButtonTitle,
509506
style: .secondary,
510507
isFullWidth: true
511508
) {
@@ -608,7 +605,7 @@ private enum Constants {
608605
viewModel: MockProductDetailsViewModel(
609606
complementaryInfoToShow: [.paymentOptions, .returns],
610607
onShouldShowLoadingForSectionCalled: { _ in true },
611-
onShouldShowSectionCalled: { section in section != .addToBag }
608+
onShouldShowSectionCalled: { section in section != .bagButton }
612609
)
613610
)
614611
.environmentObject(Coordinator())

Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
1818
private(set) var sizingSelectionConfiguration: ColorAndSizingSelectorConfiguration<SizingSwatch> = .init(items: [])
1919
public let productId: String
2020
private let initialSelectedProduct: SelectedProduct?
21+
private var subscriptions = Set<AnyCancellable>()
2122

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

104+
@Published var bagButtonTitle: String = ""
105+
@Published var wishlistButtonTitle: String = ""
106+
103107
init(
104108
configuration: ProductDetailsConfiguration,
105109
dependencies: ProductDetailsDependencyContainer
@@ -132,6 +136,59 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
132136
selectedVariant: selectedProduct.selectedVariant
133137
)
134138
}
139+
140+
setupBindings()
141+
}
142+
143+
private func setupBindings() {
144+
Publishers.CombineLatest(
145+
$state,
146+
dependencies.bagService.productsPublisher
147+
)
148+
.receive(on: DispatchQueue.main)
149+
.sink { [weak self] _ in
150+
self?.updateBagButtonTitle()
151+
}
152+
.store(in: &subscriptions)
153+
154+
Publishers.CombineLatest(
155+
$state,
156+
dependencies.wishlistService.productsPublisher
157+
)
158+
.receive(on: DispatchQueue.main)
159+
.sink { [weak self] _ in
160+
self?.updateWishlistButtonTitle()
161+
}
162+
.store(in: &subscriptions)
163+
}
164+
165+
private func updateBagButtonTitle() {
166+
guard productHasStock else {
167+
bagButtonTitle = L10n.Product.OutOfStock.Button.cta
168+
return
169+
}
170+
171+
guard let selectedProduct else {
172+
bagButtonTitle = L10n.Product.AddToBag.Button.cta
173+
return
174+
}
175+
176+
let bagProduct = BagProduct(selectedProduct: selectedProduct)
177+
bagButtonTitle = dependencies.bagService.containsProduct(bagProduct)
178+
? L10n.Product.RemoveFromBag.Button.cta
179+
: L10n.Product.AddToBag.Button.cta
180+
}
181+
182+
private func updateWishlistButtonTitle() {
183+
guard let selectedProduct else {
184+
wishlistButtonTitle = L10n.Product.AddToWishlist.Button.cta
185+
return
186+
}
187+
188+
let wishlistProduct = WishlistProduct(selectedProduct: selectedProduct)
189+
wishlistButtonTitle = dependencies.wishlistService.containsProduct(wishlistProduct)
190+
? L10n.Product.RemoveFromWishlist.Button.cta
191+
: L10n.Product.AddToWishlist.Button.cta
135192
}
136193

137194
func viewDidAppear() {
@@ -150,8 +207,8 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
150207
.complementaryInfo:
151208
return state.isLoading
152209
case .productDescription,
153-
.addToBag, // swiftlint:disable:this indentation_width
154-
.addToWishlist:
210+
.bagButton, // swiftlint:disable:this indentation_width
211+
.wishlistButton:
155212
return false
156213
}
157214
}
@@ -169,9 +226,9 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
169226
return state.isLoading || !productImageUrls.isEmpty
170227
case .productDescription:
171228
return !productDescription.isEmpty
172-
case .addToBag:
229+
case .bagButton:
173230
return state.isSuccess
174-
case .addToWishlist:
231+
case .wishlistButton:
175232
return state.isSuccess && dependencies.configurationService.isFeatureEnabled(.wishlist)
176233
}
177234
// swiftlint:enable vertical_whitespace_between_cases
@@ -197,15 +254,27 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
197254
func didTapAddToBag() {
198255
guard let selectedProduct else { return }
199256
let bagProduct = BagProduct(selectedProduct: selectedProduct)
200-
dependencies.bagService.addProduct(bagProduct)
201-
dependencies.analytics.trackAddToBag(productID: bagProduct.id)
257+
258+
if dependencies.bagService.containsProduct(bagProduct) {
259+
dependencies.bagService.removeProduct(bagProduct)
260+
dependencies.analytics.trackRemoveFromBag(productID: bagProduct.id)
261+
} else {
262+
dependencies.bagService.addProduct(bagProduct)
263+
dependencies.analytics.trackAddToBag(productID: bagProduct.id)
264+
}
202265
}
203266

204267
func didTapAddToWishlist() {
205268
guard let selectedProduct else { return }
206269
let wishlistProduct = WishlistProduct(selectedProduct: selectedProduct)
207-
dependencies.wishlistService.addProduct(wishlistProduct)
208-
dependencies.analytics.trackAddToWishlist(productID: wishlistProduct.id)
270+
271+
if dependencies.wishlistService.containsProduct(wishlistProduct) {
272+
dependencies.wishlistService.removeProduct(wishlistProduct)
273+
dependencies.analytics.trackRemoveFromWishlist(productID: wishlistProduct.id)
274+
} else {
275+
dependencies.wishlistService.addProduct(wishlistProduct)
276+
dependencies.analytics.trackAddToWishlist(productID: wishlistProduct.id)
277+
}
209278
}
210279

211280
func colorSwatches(filteredBy searchTerm: String) -> [ColorSwatch] {

Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ final class ProductListingViewModel: ProductListingViewModelProtocol {
1414
@Published var style: ProductListingListStyle
1515
@Published var showRefine = false
1616
@Published var sortOption: String?
17-
@Published private(set) var wishlistContent: [SelectedProduct]
17+
@Published private(set) var wishlistContent: [SelectedProduct] = []
1818
@Published private(set) var state: PaginatedViewState<ProductListingViewStateModel, ProductListingViewErrorType>
19+
private var subscriptions = Set<AnyCancellable>()
1920

2021
private enum Constants {
2122
static let defaultSkeletonItemsSize = 12
@@ -54,11 +55,20 @@ final class ProductListingViewModel: ProductListingViewModelProtocol {
5455
sortOption = sort
5556
query = searchText ?? urlQueryParameters.map(\.values)?.joined(separator: ",")
5657
state = .loadingFirstPage(.init(title: "", products: .skeleton(itemsSize: skeletonItemsSize)))
57-
wishlistContent = dependencies.wishlistService.getWishlistContent()
58+
59+
setupBindings()
60+
}
61+
62+
private func setupBindings() {
63+
dependencies.wishlistService.productsPublisher
64+
.receive(on: DispatchQueue.main)
65+
.sink { [weak self] wishListProducts in
66+
self?.wishlistContent = wishListProducts
67+
}
68+
.store(in: &subscriptions)
5869
}
5970

6071
func viewDidAppear() {
61-
wishlistContent = dependencies.wishlistService.getWishlistContent()
6272
Task {
6373
await loadProductsIfNeeded()
6474
}
@@ -79,7 +89,8 @@ final class ProductListingViewModel: ProductListingViewModelProtocol {
7989
func didSelect(_: Product) {}
8090

8191
func isFavoriteState(for product: Product) -> Bool {
82-
wishlistContent.contains { $0.product.id == product.id }
92+
let wishlistProduct = WishlistProduct(product: product)
93+
return dependencies.wishlistService.containsProduct(wishlistProduct)
8394
}
8495

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

98108
func didApplyFilters() {

Alfie/Alfie/Views/WishlistView/WishlistView.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ struct WishlistView<ViewModel: WishlistViewModelProtocol>: View {
4040
.padding(.horizontal, Spacing.space200)
4141
}
4242
.padding(.vertical, Spacing.space200)
43-
.onAppear {
44-
viewModel.viewDidAppear()
45-
}
4643
}
4744
}
4845

Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
1+
import Combine
12
import Foundation
23
import Models
34
import SharedUI
45

56
final class WishlistViewModel: WishlistViewModelProtocol {
6-
@Published private(set) var products: [WishlistProduct]
7-
7+
@Published private(set) var products: [WishlistProduct] = []
8+
private var subscriptions = Set<AnyCancellable>()
89
private let dependencies: WishlistDependencyContainer
910

1011
init(dependencies: WishlistDependencyContainer) {
1112
self.dependencies = dependencies
12-
products = dependencies.wishlistService.getWishlistContent()
13-
}
1413

15-
// MARK: - WishListViewModelProtocol
14+
setupBindigs()
15+
}
1616

17-
func viewDidAppear() {
18-
products = dependencies.wishlistService.getWishlistContent()
17+
private func setupBindigs() {
18+
dependencies.wishlistService.productsPublisher
19+
.receive(on: DispatchQueue.main)
20+
.sink { [weak self] wishListProducts in
21+
self?.products = wishListProducts
22+
}
23+
.store(in: &subscriptions)
1924
}
2025

26+
// MARK: - WishListViewModelProtocol
27+
2128
func didSelectDelete(for wishlistProduct: WishlistProduct) {
2229
dependencies.wishlistService.removeProduct(wishlistProduct)
2330
dependencies.analytics.trackRemoveFromWishlist(productID: wishlistProduct.product.id)
24-
products = dependencies.wishlistService.getWishlistContent()
2531
}
2632

2733
func productCardViewModel(for wishlistProduct: WishlistProduct) -> VerticalProductCardViewModel {

Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import Combine
12
import Foundation
23
import Models
34

45
// TODO: Update with an actual implementation with storage
56
public final class BagService: BagServiceProtocol {
6-
private var products: [BagProduct] = []
7+
@Published private var products: [BagProduct] = []
8+
9+
public var productsPublisher: AnyPublisher<[BagProduct], Never> {
10+
$products.eraseToAnyPublisher()
11+
}
712

813
public init() { }
914

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

20-
public func getBagContent() -> [BagProduct] {
21-
products
25+
public func containsProduct(_ bagProduct: BagProduct) -> Bool {
26+
products.contains { $0.id == bagProduct.id }
2227
}
2328
}

Alfie/AlfieKit/Sources/Core/Services/Wishlist/WishlistService.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import Combine
12
import Foundation
23
import Models
34

45
// TODO: Update with an actual implementation with storage
56
public final class WishlistService: WishlistServiceProtocol {
6-
private var products: [WishlistProduct] = []
7+
@Published private var products: [WishlistProduct] = []
8+
9+
public var productsPublisher: AnyPublisher<[WishlistProduct], Never> {
10+
$products.eraseToAnyPublisher()
11+
}
712

813
public init() { }
914

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

24-
public func getWishlistContent() -> [WishlistProduct] {
25-
products
29+
public func containsProduct(_ wishlistProduct: WishlistProduct) -> Bool {
30+
products.contains { $0.id == wishlistProduct.id }
2631
}
2732
}

0 commit comments

Comments
 (0)