Skip to content

Commit 6b6da16

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 f5fc889 commit 6b6da16

21 files changed

+213
-72
lines changed

Alfie/Alfie/Localization/L10n+Generated.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,18 @@ enum L10n {
189189
static let cta = L10n.tr("L10n", "product.out_of_stock.button.cta")
190190
}
191191
}
192+
enum RemoveFromBag {
193+
enum Button {
194+
/// Remove from bag
195+
static let cta = L10n.tr("L10n", "product.remove_from_bag.button.cta")
196+
}
197+
}
198+
enum RemoveFromWishlist {
199+
enum Button {
200+
/// Remove from wishlist
201+
static let cta = L10n.tr("L10n", "product.remove_from_wishlist.button.cta")
202+
}
203+
}
192204
enum Size {
193205
/// Size
194206
static let title = L10n.tr("L10n", "product.size.title")
@@ -474,6 +486,8 @@ extension L10n {
474486
case productColorTitle = "product.color.title"
475487
case productOneSizeTitle = "product.one_size.title"
476488
case productOutOfStockButtonCta = "product.out_of_stock.button.cta"
489+
case productRemoveFromBagButtonCta = "product.remove_from_bag.button.cta"
490+
case productRemoveFromWishlistButtonCta = "product.remove_from_wishlist.button.cta"
477491
case productSizeTitle = "product.size.title"
478492
case productSizeNoSelectionTitle = "product.size.no_selection.title"
479493
case searchScreenEmptyViewMessage = "search.screen.empty_view.message"

Alfie/Alfie/Localization/L10n.xcstrings

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,28 @@
409409
}
410410
}
411411
},
412+
"product.remove_from_bag.button.cta" : {
413+
"extractionState" : "manual",
414+
"localizations" : {
415+
"en" : {
416+
"stringUnit" : {
417+
"state" : "translated",
418+
"value" : "Remove from bag"
419+
}
420+
}
421+
}
422+
},
423+
"product.remove_from_wishlist.button.cta" : {
424+
"extractionState" : "manual",
425+
"localizations" : {
426+
"en" : {
427+
"stringUnit" : {
428+
"state" : "translated",
429+
"value" : "Remove from wishlist"
430+
}
431+
}
432+
}
433+
},
412434
"product.size.no_selection.title" : {
413435
"extractionState" : "manual",
414436
"localizations" : {

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,25 +1,31 @@
1+
import Combine
12
import Foundation
23
import Models
34

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

910
init(dependencies: BagDependencyContainer) {
1011
self.dependencies = dependencies
11-
products = dependencies.bagService.getBagContent()
12-
}
1312

14-
// MARK: - BagViewModelProtocol
13+
setupBindigs()
14+
}
1515

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

25+
// MARK: - BagViewModelProtocol
26+
2027
func didSelectDelete(for selectedProduct: BagProduct) {
2128
dependencies.bagService.removeProduct(selectedProduct)
22-
products = dependencies.bagService.getBagContent()
2329
dependencies.analytics.trackRemoveFromBag(productID: selectedProduct.product.id)
2430
}
2531

Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ struct ProductDetailsView<ViewModel: ProductDetailsViewModelProtocol>: View {
180180
complementaryViews
181181
.padding(.horizontal, horizontalPadding)
182182
}
183-
addToBag
183+
bagButton
184184
}
185185
.fullScreenCover(isPresented: $isMediaFullScreen) {
186186
fullscreenMediaCarousel
@@ -254,7 +254,7 @@ extension ProductDetailsView {
254254
}
255255

256256
VStack {
257-
addToBag
257+
bagButton
258258
addToWishlist
259259
}
260260
.padding(.vertical, Spacing.space100)
@@ -480,14 +480,11 @@ extension ProductDetailsView {
480480
}
481481
}
482482

483-
@ViewBuilder private var addToBag: some View {
484-
if viewModel.shouldShow(section: .addToBag) {
483+
@ViewBuilder private var bagButton: some View {
484+
if viewModel.shouldShow(section: .bagButton) {
485485
VStack(spacing: Spacing.space0) {
486-
let addToBagText = L10n.Product.AddToBag.Button.cta
487-
let outOfStockText = L10n.Product.OutOfStock.Button.cta
488-
489486
ThemedButton(
490-
text: viewModel.productHasStock ? addToBagText : outOfStockText,
487+
text: viewModel.bagButtonTitle,
491488
isDisabled: .init(
492489
get: { !viewModel.isAddToBagEnabled },
493490
set: { _ in }
@@ -501,10 +498,10 @@ extension ProductDetailsView {
501498
}
502499

503500
@ViewBuilder private var addToWishlist: some View {
504-
if viewModel.shouldShow(section: .addToWishlist) {
501+
if viewModel.shouldShow(section: .wishlistButton) {
505502
VStack(spacing: Spacing.space0) {
506503
ThemedButton(
507-
text: L10n.Product.AddToWishlist.Button.cta,
504+
text: viewModel.wishlistButtonTitle,
508505
style: .secondary,
509506
isFullWidth: true
510507
) {
@@ -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
@@ -16,6 +16,7 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
1616
private(set) var sizingSelectionConfiguration: ColorAndSizingSelectorConfiguration<SizingSwatch> = .init(items: [])
1717
public let productId: String
1818
private let initialSelectedProduct: SelectedProduct?
19+
private var subscriptions = Set<AnyCancellable>()
1920

2021
private var product: Product? {
2122
guard case .success(let model) = state else {
@@ -102,6 +103,9 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
102103
sizingSelectionConfiguration.items.count > 1 ? selectedVariant?.size != nil : true
103104
}
104105

106+
@Published var bagButtonTitle: String = ""
107+
@Published var wishlistButtonTitle: String = ""
108+
105109
init(
106110
productId: String,
107111
product: Product?,
@@ -129,6 +133,59 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
129133
default:
130134
break
131135
}
136+
137+
setupBindings()
138+
}
139+
140+
private func setupBindings() {
141+
Publishers.CombineLatest(
142+
$state,
143+
dependencies.bagService.productsPublisher
144+
)
145+
.receive(on: DispatchQueue.main)
146+
.sink { [weak self] _ in
147+
self?.updateBagButtonTitle()
148+
}
149+
.store(in: &subscriptions)
150+
151+
Publishers.CombineLatest(
152+
$state,
153+
dependencies.wishlistService.productsPublisher
154+
)
155+
.receive(on: DispatchQueue.main)
156+
.sink { [weak self] _ in
157+
self?.updateWishlistButtonTitle()
158+
}
159+
.store(in: &subscriptions)
160+
}
161+
162+
private func updateBagButtonTitle() {
163+
guard productHasStock else {
164+
bagButtonTitle = L10n.Product.OutOfStock.Button.cta
165+
return
166+
}
167+
168+
guard let selectedProduct else {
169+
bagButtonTitle = L10n.Product.AddToBag.Button.cta
170+
return
171+
}
172+
173+
let bagProduct = BagProduct(selectedProduct: selectedProduct)
174+
bagButtonTitle = dependencies.bagService.containsProduct(bagProduct)
175+
? L10n.Product.RemoveFromBag.Button.cta
176+
: L10n.Product.AddToBag.Button.cta
177+
}
178+
179+
private func updateWishlistButtonTitle() {
180+
guard let selectedProduct else {
181+
wishlistButtonTitle = L10n.Product.AddToWishlist.Button.cta
182+
return
183+
}
184+
185+
let wishlistProduct = WishlistProduct(selectedProduct: selectedProduct)
186+
wishlistButtonTitle = dependencies.wishlistService.containsProduct(wishlistProduct)
187+
? L10n.Product.RemoveFromWishlist.Button.cta
188+
: L10n.Product.AddToWishlist.Button.cta
132189
}
133190

134191
func viewDidAppear() {
@@ -147,8 +204,8 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
147204
.complementaryInfo:
148205
return state.isLoading
149206
case .productDescription,
150-
.addToBag, // swiftlint:disable:this indentation_width
151-
.addToWishlist:
207+
.bagButton, // swiftlint:disable:this indentation_width
208+
.wishlistButton:
152209
return false
153210
}
154211
}
@@ -166,9 +223,9 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
166223
return state.isLoading || !productImageUrls.isEmpty
167224
case .productDescription:
168225
return !productDescription.isEmpty
169-
case .addToBag:
226+
case .bagButton:
170227
return state.isSuccess
171-
case .addToWishlist:
228+
case .wishlistButton:
172229
return state.isSuccess && dependencies.configurationService.isFeatureEnabled(.wishlist)
173230
}
174231
// swiftlint:enable vertical_whitespace_between_cases
@@ -194,15 +251,27 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
194251
func didTapAddToBag() {
195252
guard let selectedProduct else { return }
196253
let bagProduct = BagProduct(selectedProduct: selectedProduct)
197-
dependencies.bagService.addProduct(bagProduct)
198-
dependencies.analytics.trackAddToBag(productID: bagProduct.id)
254+
255+
if dependencies.bagService.containsProduct(bagProduct) {
256+
dependencies.bagService.removeProduct(bagProduct)
257+
dependencies.analytics.trackRemoveFromBag(productID: bagProduct.id)
258+
} else {
259+
dependencies.bagService.addProduct(bagProduct)
260+
dependencies.analytics.trackAddToBag(productID: bagProduct.id)
261+
}
199262
}
200263

201264
func didTapAddToWishlist() {
202265
guard let selectedProduct else { return }
203266
let wishlistProduct = WishlistProduct(selectedProduct: selectedProduct)
204-
dependencies.wishlistService.addProduct(wishlistProduct)
205-
dependencies.analytics.trackAddToWishlist(productID: wishlistProduct.id)
267+
268+
if dependencies.wishlistService.containsProduct(wishlistProduct) {
269+
dependencies.wishlistService.removeProduct(wishlistProduct)
270+
dependencies.analytics.trackRemoveFromWishlist(productID: wishlistProduct.id)
271+
} else {
272+
dependencies.wishlistService.addProduct(wishlistProduct)
273+
dependencies.analytics.trackAddToWishlist(productID: wishlistProduct.id)
274+
}
206275
}
207276

208277
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
@@ -41,9 +41,6 @@ struct WishlistView<ViewModel: WishlistViewModelProtocol>: View {
4141
}
4242
.padding(.vertical, Spacing.space200)
4343
.withToolbar(for: .wishlist)
44-
.onAppear {
45-
viewModel.viewDidAppear()
46-
}
4744
}
4845
}
4946

0 commit comments

Comments
 (0)