Skip to content

Commit a898473

Browse files
ALFMOB-162: Improve Wishlist functionality on PLP & Wishlist (#67)
* ALFMOB-162: Hide size from wishlist item card * ALFMOB-162: Redirect wishlist Add to Bag to PDP * ALFMOB-162: Migrate WishlistTests to Swift Testing * ALFMOB-162: Remove unused bagService from WishlistDependencyContainer * ALFMOB-162: Do not auto-select size on PDP entry * ALFMOB-162: Revert WishlistTests to XCTest for project consistency * ALFMOB-162: Add trackForMemoryLeak helper and apply to WishlistTests * ALFMOB-162: Remove size field and view from VerticalProductCard * ALFMOB-162: Disable PDP Add to Bag until size is selected * ALFMOB-162: Remove wishlist product by id and consolidate removal API * ALFMOB-162: Fix PDP Add to Bag for single-size and sizeless products * ALFMOB-162: Base PDP Add to Bag label on overall product stock * ALFMOB-150: Persist wishlist via UserDefaults-backed store * ALFMOB-150: Add unit tests for wishlist persistence * ALFMOB-150: Persist bag via UserDefaults-backed store * ALFMOB-150: Add unit tests for bag persistence * ALFMOB-150: Consolidate Bag/Wishlist storage keys into single StorageKey enum * Revert "ALFMOB-150: Persist Wishlist & Bag"
1 parent c4aa148 commit a898473

20 files changed

Lines changed: 408 additions & 68 deletions

File tree

Alfie/Alfie/Alfie.xctestplan

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@
102102
"identifier" : "ProductListingTests",
103103
"name" : "ProductListingTests"
104104
}
105+
},
106+
{
107+
"target" : {
108+
"containerPath" : "container:AlfieKit",
109+
"identifier" : "WishlistTests",
110+
"name" : "WishlistTests"
111+
}
105112
}
106113
],
107114
"version" : 1

Alfie/AlfieKit/Sources/AppFeature/Navigation/AppFeatureViewModel.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ public final class AppFeatureViewModel: AppFeatureViewModelProtocol {
8181
)
8282
let wishlistDependencyContainer = WishlistDependencyContainer(
8383
wishlistService: serviceProvider.wishlistService,
84-
bagService: serviceProvider.bagService,
8584
analytics: serviceProvider.analytics
8685
)
8786
let categorySelectorDependencyContainer = CategorySelectorDependencyContainer(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ public final class WishlistService: WishlistServiceProtocol {
1313
products.append(product)
1414
}
1515

16-
public func removeProduct(_ product: SelectedProduct) {
17-
products = products.filter { $0.id != product.id }
16+
public func removeProduct(withId productId: String) {
17+
products = products.filter { $0.product.id != productId }
1818
}
1919

2020
public func getWishlistContent() -> [SelectedProduct] {

Alfie/AlfieKit/Sources/Mocks/Core/Features/MockProductDetailsViewModel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ public class MockProductDetailsViewModel: ProductDetailsViewModelProtocol {
88
public var productId: String = ""
99
public var productTitle: String = ""
1010
public var productHasStock: Bool = true
11+
public var productHasAnyStock: Bool = true
12+
public var isAddToBagEnabled: Bool = true
13+
public var canShowSizeSelector: Bool = true
1114
public var productName: String = ""
1215
public var productImageUrls: [URL] = []
1316
public var colorSelectionConfiguration: ColorAndSizingSelectorConfiguration<ColorSwatch>

Alfie/AlfieKit/Sources/Mocks/Core/Services/MockWishlistService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public final class MockWishlistService: WishlistServiceProtocol {
1212
products.append(product)
1313
}
1414

15-
public func removeProduct(_ product: SelectedProduct) {
16-
products = products.filter { $0.id != product.id }
15+
public func removeProduct(withId productId: String) {
16+
products = products.filter { $0.product.id != productId }
1717
}
1818

1919
public func getWishlistContent() -> [SelectedProduct] {

Alfie/AlfieKit/Sources/Model/Models/ProductDetails/Protocols/ProductDetailsViewModelProtocol.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ public protocol ProductDetailsViewModelProtocol: ObservableObject {
77
var productTitle: String { get }
88
var productName: String { get }
99
var productHasStock: Bool { get }
10+
var productHasAnyStock: Bool { get }
11+
var isAddToBagEnabled: Bool { get }
12+
var canShowSizeSelector: Bool { get }
1013
var productImageUrls: [URL] { get }
1114
var productDescription: String { get }
1215
var colorSelectionConfiguration: ColorAndSizingSelectorConfiguration<ColorSwatch> { get }

Alfie/AlfieKit/Sources/Model/Services/Wishlist/WishlistServiceProtocol.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import Foundation
22

33
public protocol WishlistServiceProtocol {
44
func addProduct(_ product: SelectedProduct)
5-
func removeProduct(_ product: SelectedProduct)
5+
func removeProduct(withId productId: String)
66
func getWishlistContent() -> [SelectedProduct]
77
}

Alfie/AlfieKit/Sources/Model/UI/ProductCard/VerticalProductCardViewModel.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ public struct VerticalProductCardViewModel {
99
public var priceType: PriceType
1010
public var colorTitle: String?
1111
public var color: String?
12-
public var sizeTitle: String?
13-
public var size: String?
1412
public var addToBagTitle: String?
1513
public var outOfStockTitle: String?
1614
public var isAddToBagDisabled = false
@@ -24,8 +22,6 @@ public struct VerticalProductCardViewModel {
2422
priceType: PriceType,
2523
colorTitle: String? = nil,
2624
color: String? = nil,
27-
sizeTitle: String? = nil,
28-
size: String? = nil,
2925
addToBagTitle: String? = nil,
3026
outOfStockTitle: String? = nil,
3127
isAddToBagDisabled: Bool = false
@@ -38,8 +34,6 @@ public struct VerticalProductCardViewModel {
3834
self.priceType = priceType
3935
self.colorTitle = colorTitle
4036
self.color = color
41-
self.sizeTitle = sizeTitle
42-
self.size = size
4337
self.addToBagTitle = addToBagTitle
4438
self.outOfStockTitle = outOfStockTitle
4539
self.isAddToBagDisabled = isAddToBagDisabled
@@ -51,8 +45,6 @@ public extension VerticalProductCardViewModel {
5145
configuration: VerticalProductCardConfiguration,
5246
product: Product,
5347
colorTitle: String? = nil,
54-
sizeTitle: String? = nil,
55-
oneSizeTitle: String? = nil,
5648
addToBagTitle: String? = nil,
5749
outOfStockTitle: String? = nil,
5850
isAddToBagDisabled: Bool = false
@@ -64,8 +56,6 @@ public extension VerticalProductCardViewModel {
6456
self.name = product.name
6557
self.colorTitle = colorTitle
6658
self.color = product.defaultVariant.colour?.name
67-
self.sizeTitle = sizeTitle
68-
self.size = product.isSingleSizeProduct ? oneSizeTitle : product.sizeText
6959
self.priceType = product.priceType
7060
self.addToBagTitle = addToBagTitle
7161
self.outOfStockTitle = outOfStockTitle

Alfie/AlfieKit/Sources/ProductDetails/UI/ProductDetailsView.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ public struct ProductDetailsView<ViewModel: ProductDetailsViewModelProtocol>: Vi
4040
viewModel.sizingSelectionConfiguration.items.count > 6
4141
}
4242

43-
private var canShowSizeSelector: Bool {
44-
viewModel.sizingSelectionConfiguration.items.count > 1
45-
}
46-
4743
private var isOneSize: Bool {
4844
viewModel.sizingSelectionConfiguration.items.count == 1
4945
}
@@ -418,7 +414,7 @@ extension ProductDetailsView {
418414
@ViewBuilder private var sizeSelector: some View {
419415
if viewModel.shouldShow(section: .sizeSelector) {
420416
VStack(alignment: .leading, spacing: Spacing.space150) {
421-
if canShowSizeSelector {
417+
if viewModel.canShowSizeSelector {
422418
ColorAndSizingSelectorHeaderView(
423419
configuration: viewModel.sizingSelectionConfiguration,
424420
isExpandable: canShowSizePickers
@@ -487,9 +483,9 @@ extension ProductDetailsView {
487483
let outOfStockText = L10n.Product.OutOfStock.Button.cta
488484

489485
ThemedButton(
490-
text: viewModel.productHasStock ? addToBagText : outOfStockText,
486+
text: viewModel.productHasAnyStock ? addToBagText : outOfStockText,
491487
isDisabled: .init(
492-
get: { !viewModel.productHasStock },
488+
get: { !viewModel.isAddToBagEnabled },
493489
set: { _ in }
494490
),
495491
isFullWidth: true

Alfie/AlfieKit/Sources/ProductDetails/UI/ProductDetailsViewModel.swift

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,26 @@ public final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
172172
(selectedVariant?.stock ?? 0) > 0
173173
}
174174

175+
/// True when at least one variant of the product has stock.
176+
/// Use this for the CTA label (avoid showing "Out of Stock" while the product is still buyable in another size).
177+
public var productHasAnyStock: Bool {
178+
product?.variants.contains { $0.stock > 0 } ?? false
179+
}
180+
181+
/// True when the user is offered an interactive size choice (more than one size swatch).
182+
/// When false, the size is implicit (sizeless product or a single available size).
183+
public var canShowSizeSelector: Bool {
184+
sizingSelectionConfiguration.items.count > 1
185+
}
186+
187+
public var isAddToBagEnabled: Bool {
188+
productHasStock
189+
&& colorSelectionConfiguration.selectedItem != nil
190+
&& (!canShowSizeSelector || sizingSelectionConfiguration.selectedItem != nil)
191+
}
192+
175193
public func didTapAddToBag() {
176-
guard let selectedProduct else { return }
194+
guard isAddToBagEnabled, let selectedProduct else { return }
177195
dependencies.bagService.addProduct(selectedProduct)
178196
dependencies.analytics.trackAddToBag(productID: selectedProduct.id)
179197
}
@@ -296,15 +314,11 @@ public final class ProductDetailsViewModel: ProductDetailsViewModelProtocol {
296314

297315
let sizingSwatches = buildSizingSwatches(product: product, selectedVariant: selectedVariant)
298316

299-
var selectedSwatch: SizingSwatch?
300-
if let selectedVariant {
301-
selectedSwatch = sizingSwatches.first { $0.id == selectedVariant.size?.id }
302-
}
303-
317+
// Size is never auto-selected on PDP entry — the user must tap a swatch.
304318
sizingSelectionConfiguration = .init(
305319
selectedTitle: L10n.Product.Size.title + ":",
306320
items: sizingSwatches,
307-
selectedItem: selectedSwatch
321+
selectedItem: nil
308322
)
309323
sizingSelectionSubscription = sizingSelectionConfiguration.$selectedItem
310324
.receive(on: dependencies.scheduler)

0 commit comments

Comments
 (0)