Skip to content

Commit 5f696be

Browse files
artyomromanovartyomvromanov1989Copilotamccall-mindera(Admin) Nhat Hoang
authored
ALFMOB-83: Wishlist Functionality(Continued) (#13)
* Add Database for wishlist IDs, and flows to get and set them III * Address code review: fix error handling, dispatcher injection, DB consolidation, and tests Agent-Logs-Url: https://github.com/Mindera/Alfie-Android/sessions/0a13adaa-7523-4bed-8732-c54534ff2cb4 Co-authored-by: amccall-mindera <241989343+amccall-mindera@users.noreply.github.com> * Fix detekt * Update ProductCard component * Fix Medium product card size for Carousel * [ALFMOB-83] Implement Add to Wishlist remaining functionality - Navigate from wishlist card tap to PDP - Navigate from wishlist 'Add to Bag' CTA to PDP - Disable PDP 'Add to Bag' button until a size is selected - Pre-wire onClick on WishlistProductUi via factory - Remove size & color from ProductCardType.Vertical (wishlist cards show none) - Update WishlistUIFactory, WishlistViewModel, WishlistScreen - Update tests and mock data Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [ALFMOB-83] Fix detekt violations - Remove trailing commas (TrailingCommaOnCallSite / TrailingCommaOnDeclarationSite) - Remove blank line before closing brace (NoBlankLineBeforeRbrace) - Update detekt baseline for pre-existing suppressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add unit tests * Make detekt happy * Fix tests, again * Fix baseline * Fix reviews * Fix unit tests * Add error handling and fix race condition for rapid favorite click * Fix detekt * Address comment * Address comments --------- Co-authored-by: artyom.romanov <artyom.romanov@mindera.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: amccall-mindera <241989343+amccall-mindera@users.noreply.github.com> Co-authored-by: (Admin) Nhat Hoang <admin.nhat.hoang@FKWFHCF94F-PT003126.local> Co-authored-by: Nhat Hoang <nhat.hoang@mindera.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent aa70fb6 commit 5f696be

56 files changed

Lines changed: 808 additions & 1015 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

config/detekt/baseline.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<ID>ComposableFunctionName:DebugComposeRunner.kt$DebugComposeRunner$@Composable operator fun invoke(block: @Composable () -&gt; Unit)</ID>
66
<ID>ComposableFunctionName:UIEventHandlers.kt$@Composable fun UIEventEmitter.handleUIEvents( navigator: DestinationsNavigator, navController: NavController, directionProvider: DirectionProvider, snackbarHostState: SnackbarCustomHostState, onCustomEvent: (UIEvent.Custom) -&gt; Unit = { }, onBaseEventOverride: ((UIEvent.Base) -&gt; Unit)? = null )</ID>
77
<ID>ComposableFunctionName:UIEventHandlers.kt$@Composable fun UIEventEmitter.handleUIEvents(onEvent: (UIEvent) -&gt; Unit)</ID>
8-
<ID>CyclomaticComplexMethod:Button.kt$@Composable fun Button( type: ButtonType, text: String, onClick: ClickEvent, modifier: Modifier = Modifier, buttonSize: ButtonSize = Small, iconButton: IconButton? = null, shape: RoundedCornerShape = Theme.shape.medium, isLoading: Boolean = false, isShimmering: Boolean = false, isEnabled: Boolean = true, overrideTextStyle: TextStyle? = null, overrideBorderThickness: Dp? = null, overrideColors: ButtonColors? = null, overrideTextColor: Color? = null, overrideTextDisabledColor: Color? = null )</ID>
8+
<ID>CyclomaticComplexMethod:Button.kt$@Composable fun Button( type: ButtonType, text: String, onClick: ClickEvent, modifier: Modifier = Modifier, buttonSize: ButtonSize = Small, iconButton: IconButton? = null, shape: RoundedCornerShape = Theme.shape.none, isLoading: Boolean = false, isShimmering: Boolean = false, isEnabled: Boolean = true, overrideTextStyle: TextStyle? = null, overrideBorderThickness: Dp? = null, overrideColors: ButtonColors? = null, overrideTextColor: Color? = null, overrideTextDisabledColor: Color? = null )</ID>
99
<ID>LongMethod:AppConventionPlugin.kt$AppConventionPlugin$override fun apply(target: Project)</ID>
1010
<ID>LongParameterList:ButtonType.kt$ButtonType$( val backgroundColor: Color, val contentColor: Color, val disabledBackgroundColor: Color, val disabledContentColor: Color, val hasBorder: Boolean, val loadingType: LoadingType, val disabledLoadingType: LoadingType, val shimmerColors: ShimmerColors = ShimmerColors.Light )</ID>
1111
<ID>LongParameterList:SearchTextType.kt$SearchTextType$( val textStyle: TextStyle, val textColor: Color, @DrawableRes val searchIcon: Int, @DrawableRes val clearIcon: Int, val cursorColor: Color, val unselectedBorderColor: Color, val selectedBorderColor: Color, val placeholderTextColor: Color, val selectedColor: Color, val unselectedColor: Color, val verticalPadding: Dp, val horizontalPadding: Dp )</ID>

core/ui/src/main/java/au/com/alfie/ecomm/core/ui/media/image/ImageUI.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@ package au.com.alfie.ecomm.core.ui.media.image
33
import androidx.compose.runtime.Stable
44
import au.com.alfie.ecomm.core.ui.media.MediaUI
55
import kotlinx.collections.immutable.ImmutableList
6+
import kotlinx.collections.immutable.persistentListOf
67
import kotlin.math.abs
78

89
@Stable
910
data class ImageUI(
1011
val images: ImmutableList<ImageSizeUI>,
1112
val alt: String?
12-
) : MediaUI
13+
) : MediaUI {
14+
companion object {
15+
fun preview(
16+
url: String = "https://images.pexels.com/photos/9362029/pexels-photo-9362029.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"
17+
) = ImageUI(
18+
images = persistentListOf(ImageSizeUI.Large(url)),
19+
alt = ""
20+
)
21+
}
22+
}
1323

1424
@Stable
1525
fun ImageUI.pickImageUrlBySize(width: Int): String = images.minByOrNull { abs(width - it.width) }?.url.orEmpty()

data/database/src/main/java/au/com/alfie/ecomm/data/database/PersistentDatabase.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@ import androidx.room.Database
44
import androidx.room.RoomDatabase
55
import au.com.alfie.ecomm.data.database.search.RecentSearchDao
66
import au.com.alfie.ecomm.data.database.search.model.RecentSearchEntity
7+
import au.com.alfie.ecomm.data.database.wishlist.WishlistDao
8+
import au.com.alfie.ecomm.data.database.wishlist.model.WishlistEntity
79

810
@Database(
911
entities = [
10-
RecentSearchEntity::class
12+
RecentSearchEntity::class,
13+
WishlistEntity::class
1114
],
12-
version = 2,
15+
version = 3,
1316
exportSchema = true
1417
)
1518
internal abstract class PersistentDatabase : RoomDatabase() {
1619

1720
abstract fun recentSearchDao(): RecentSearchDao
21+
22+
abstract fun wishlistDao(): WishlistDao
1823
}

data/database/src/main/java/au/com/alfie/ecomm/data/database/di/DatabaseModule.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import au.com.alfie.ecomm.data.database.PersistentDatabase
88
import au.com.alfie.ecomm.data.database.navigation.NavigationEntryDao
99
import au.com.alfie.ecomm.data.database.search.FeatureToggleDao
1010
import au.com.alfie.ecomm.data.database.search.RecentSearchDao
11+
import au.com.alfie.ecomm.data.database.wishlist.WishlistDao
1112
import dagger.Module
1213
import dagger.Provides
1314
import dagger.hilt.InstallIn
@@ -55,4 +56,7 @@ internal object DatabaseModule {
5556

5657
@Provides
5758
fun provideFeatureToggleDao(database: FeatureToggleDatabase): FeatureToggleDao = database.featureToggleDao()
59+
60+
@Provides
61+
fun provideWishlistDao(database: PersistentDatabase): WishlistDao = database.wishlistDao()
5862
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package au.com.alfie.ecomm.data.database.wishlist
2+
3+
import androidx.room.Dao
4+
import androidx.room.Insert
5+
import androidx.room.OnConflictStrategy
6+
import androidx.room.Query
7+
import au.com.alfie.ecomm.data.database.wishlist.model.WishlistEntity
8+
import kotlinx.coroutines.flow.Flow
9+
10+
@Dao
11+
interface WishlistDao {
12+
13+
@Insert(onConflict = OnConflictStrategy.REPLACE)
14+
suspend fun addToWishlist(product: WishlistEntity)
15+
16+
@Query("DELETE FROM wishlist WHERE id = :productId")
17+
suspend fun removeFromWishlist(productId: String)
18+
19+
@Query("SELECT * FROM wishlist")
20+
fun getWishlistIds(): Flow<List<WishlistEntity>>
21+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package au.com.alfie.ecomm.data.database.wishlist.model
2+
3+
import androidx.room.Entity
4+
import androidx.room.PrimaryKey
5+
6+
@Entity(tableName = "wishlist")
7+
data class WishlistEntity(
8+
@PrimaryKey
9+
val id: String
10+
)
Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,27 @@
11
package au.com.alfie.ecomm.data.wishlist
22

3+
import au.com.alfie.ecomm.data.database.wishlist.WishlistDao
4+
import au.com.alfie.ecomm.data.database.wishlist.model.WishlistEntity
35
import au.com.alfie.ecomm.data.toRepositoryResult
4-
import au.com.alfie.ecomm.repository.product.model.Product
5-
import au.com.alfie.ecomm.repository.result.RepositoryResult
66
import au.com.alfie.ecomm.repository.wishlist.WishlistRepository
77
import kotlinx.coroutines.flow.Flow
8-
import kotlinx.coroutines.flow.MutableStateFlow
98
import kotlinx.coroutines.flow.map
109
import javax.inject.Inject
1110

12-
class WishlistRepositoryImpl @Inject constructor() : WishlistRepository {
11+
class WishlistRepositoryImpl @Inject constructor(
12+
private val wishlistDao: WishlistDao
13+
) : WishlistRepository {
1314

14-
// TODO consider removing this property when the wishlist of product is saved on database or api
15-
private val _wishlist = MutableStateFlow<List<Product>>(listOf())
16-
17-
// TODO change this implementation to a proper implementation using data base or api to save the products on wishlist
18-
override fun addToWishlist(product: Product): RepositoryResult<Boolean> {
19-
if (_wishlist.value.none { it.id == product.id }) {
20-
_wishlist.value = buildList {
21-
addAll(_wishlist.value)
22-
add(product)
23-
}
15+
override fun getWishlist(): Flow<List<String>> =
16+
wishlistDao.getWishlistIds().map { wishlist ->
17+
wishlist.map { it.id }
2418
}
25-
return RepositoryResult.Success(true)
26-
}
2719

28-
// TODO change this implementation to a proper implementation using data base or api to save the products on wishlist
29-
override fun removeFromWishlist(product: Product): RepositoryResult<Boolean> {
30-
_wishlist.value = _wishlist.value.filter { it.id != product.id }.toMutableList()
31-
return RepositoryResult.Success(true)
32-
}
20+
override suspend fun addToWishlist(productId: String) =
21+
runCatching { wishlistDao.addToWishlist(WishlistEntity(productId)) }
22+
.toRepositoryResult()
3323

34-
// TODO change this implementation to a proper implementation using data base or api to get the wishlist
35-
override fun getWishlist(): Flow<RepositoryResult<List<Product>>> {
36-
return _wishlist.map { list ->
37-
Result.success(list).toRepositoryResult()
38-
}
39-
}
24+
override suspend fun removeFromWishlist(productId: String) =
25+
runCatching { wishlistDao.removeFromWishlist(productId) }
26+
.toRepositoryResult()
4027
}

debug/operational/src/main/java/au/com/alfie/ecomm/debug/operational/view/catalog/screen/ProductCardScreen.kt

Lines changed: 11 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
package au.com.alfie.ecomm.debug.operational.view.catalog.screen
22

3-
import androidx.compose.foundation.horizontalScroll
43
import androidx.compose.foundation.layout.Arrangement
54
import androidx.compose.foundation.layout.Column
6-
import androidx.compose.foundation.layout.Row
75
import androidx.compose.foundation.layout.Spacer
86
import androidx.compose.foundation.layout.height
97
import androidx.compose.foundation.layout.padding
10-
import androidx.compose.foundation.layout.size
118
import androidx.compose.foundation.lazy.grid.GridCells
129
import androidx.compose.foundation.lazy.grid.GridItemSpan
1310
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
1411
import androidx.compose.foundation.lazy.grid.items
15-
import androidx.compose.foundation.lazy.grid.itemsIndexed
16-
import androidx.compose.foundation.rememberScrollState
1712
import androidx.compose.material3.HorizontalDivider
1813
import androidx.compose.material3.Text
1914
import androidx.compose.runtime.Composable
@@ -23,7 +18,6 @@ import androidx.compose.runtime.mutableStateOf
2318
import androidx.compose.runtime.remember
2419
import androidx.compose.runtime.setValue
2520
import androidx.compose.ui.Modifier
26-
import androidx.compose.ui.unit.dp
2721
import au.com.alfie.ecomm.core.ui.media.image.ImageSizeUI
2822
import au.com.alfie.ecomm.core.ui.media.image.ImageUI
2923
import au.com.alfie.ecomm.designsystem.component.price.PriceType
@@ -49,7 +43,7 @@ internal fun ProductCardScreen() {
4943
horizontalArrangement = Arrangement.spacedBy(Theme.spacing.spacing16)
5044
) {
5145
item(span = { GridItemSpan(2) }) {
52-
Header("Product Card - XS")
46+
Header("Product Card - Horizontal")
5347
}
5448
items(
5549
items = mockProductsXSmall(),
@@ -66,41 +60,10 @@ internal fun ProductCardScreen() {
6660
)
6761
}
6862
item(span = { GridItemSpan(2) }) {
69-
Header("Product Card - Small")
70-
}
71-
item(span = { GridItemSpan(2) }) {
72-
Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
73-
Spacer(modifier = Modifier.size(Theme.spacing.spacing16))
74-
mockProductsSmall().forEach {
75-
ProductCard(
76-
productCardType = it,
77-
onClick = { },
78-
isLoading = isLoading
79-
)
80-
Spacer(modifier = Modifier.size(Theme.spacing.spacing12))
81-
}
82-
}
83-
}
84-
item(span = { GridItemSpan(2) }) {
85-
Header("Product Card - Medium")
86-
}
87-
itemsIndexed(items = mockProductsMedium()) { index, item ->
88-
ProductCard(
89-
productCardType = item,
90-
onClick = {},
91-
modifier = Modifier.padding(
92-
start = if (index % 2 == 0) Theme.spacing.spacing16 else 0.dp,
93-
end = if (index % 2 == 1) Theme.spacing.spacing16 else 0.dp,
94-
bottom = Theme.spacing.spacing32
95-
),
96-
isLoading = isLoading
97-
)
98-
}
99-
item(span = { GridItemSpan(2) }) {
100-
Header("Product Card - Large")
63+
Header("Product Card - Vertical")
10164
}
10265
items(
103-
items = mockProductsLarge(),
66+
items = mockProductsVertical(),
10467
span = { GridItemSpan(2) }
10568
) {
10669
ProductCard(
@@ -130,7 +93,7 @@ private fun Header(title: String) {
13093
}
13194

13295
private fun mockProductsXSmall() = listOf(
133-
ProductCardType.XSmall(
96+
ProductCardType.Horizontal(
13497
image = ImageUI(
13598
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/6046183/pexels-photo-6046183.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
13699
alt = ""
@@ -143,7 +106,7 @@ private fun mockProductsXSmall() = listOf(
143106
color = "Worn Blue",
144107
size = "29 in"
145108
),
146-
ProductCardType.XSmall(
109+
ProductCardType.Horizontal(
147110
image = ImageUI(
148111
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/6046184/pexels-photo-6046184.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
149112
alt = ""
@@ -157,7 +120,7 @@ private fun mockProductsXSmall() = listOf(
157120
color = "Worn Blue",
158121
size = "29 in"
159122
),
160-
ProductCardType.XSmall(
123+
ProductCardType.Horizontal(
161124
image = ImageUI(
162125
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/6046231/pexels-photo-6046231.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
163126
alt = ""
@@ -173,93 +136,8 @@ private fun mockProductsXSmall() = listOf(
173136
)
174137
)
175138

176-
private fun mockProductsSmall() = listOf(
177-
ProductCardType.Small(
178-
image = ImageUI(
179-
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/6046228/pexels-photo-6046228.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
180-
alt = ""
181-
),
182-
brand = "Skims",
183-
name = "Soft Lounge Long Sleeve Dress",
184-
price = PriceType.Default(price = "$ 219.00")
185-
),
186-
ProductCardType.Small(
187-
image = ImageUI(
188-
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/6046213/pexels-photo-6046213.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
189-
alt = ""
190-
),
191-
brand = "Unison",
192-
name = "Racer Slip Dress",
193-
price = PriceType.Range(
194-
startPrice = "$ 229.00",
195-
endPrice = "$ 319.00"
196-
)
197-
),
198-
ProductCardType.Small(
199-
image = ImageUI(
200-
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/6046221/pexels-photo-6046221.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
201-
alt = ""
202-
),
203-
brand = "Alemais Blank Konage Gucci",
204-
name = "Paradiso Star Man Pleated Mini Shirtdress",
205-
price = PriceType.Sale(
206-
fullPrice = "$ 340.00",
207-
salePrice = "$ 280.00"
208-
)
209-
)
210-
)
211-
212-
private fun mockProductsMedium() = listOf(
213-
ProductCardType.Medium(
214-
image = ImageUI(
215-
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/6046219/pexels-photo-6046219.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
216-
alt = ""
217-
),
218-
brand = "Anine Bing",
219-
name = "Miles Sweartshirt Anine Bing Logo Washed Dark Sage",
220-
price = PriceType.Default(price = "$ 219.00"),
221-
onFavoriteClick = {}
222-
),
223-
ProductCardType.Medium(
224-
image = ImageUI(
225-
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/6045708/pexels-photo-6045708.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
226-
alt = ""
227-
),
228-
brand = "Seed Heritage",
229-
name = "Half Sleeve Polo Top",
230-
price = PriceType.Range(
231-
startPrice = "$ 229.00",
232-
endPrice = "$ 319.00"
233-
),
234-
onFavoriteClick = {}
235-
),
236-
ProductCardType.Medium(
237-
image = ImageUI(
238-
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/7671168/pexels-photo-7671168.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
239-
alt = ""
240-
),
241-
brand = "Sportscraft",
242-
name = "Olivia Tape Yarn Cardi",
243-
price = PriceType.Sale(
244-
fullPrice = "$ 340.00",
245-
salePrice = "$ 280.00"
246-
),
247-
onFavoriteClick = {}
248-
),
249-
ProductCardType.Medium(
250-
image = ImageUI(
251-
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/2850487/pexels-photo-2850487.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
252-
alt = ""
253-
),
254-
brand = "Pangaia Sportscraft Casual Fashion",
255-
name = "Recycled Nylon NW FLWRDWN Quilted Collarless Jacket",
256-
price = PriceType.Default(price = "$ 222.00"),
257-
onFavoriteClick = {}
258-
)
259-
)
260-
261-
private fun mockProductsLarge() = listOf(
262-
ProductCardType.Large(
139+
private fun mockProductsVertical() = listOf(
140+
ProductCardType.Vertical(
263141
image = ImageUI(
264142
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/45982/pexels-photo-45982.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
265143
alt = ""
@@ -269,7 +147,7 @@ private fun mockProductsLarge() = listOf(
269147
price = PriceType.Default(price = "$ 219.00"),
270148
onFavoriteClick = {}
271149
),
272-
ProductCardType.Large(
150+
ProductCardType.Vertical(
273151
image = ImageUI(
274152
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/14641437/pexels-photo-14641437.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
275153
alt = ""
@@ -282,7 +160,7 @@ private fun mockProductsLarge() = listOf(
282160
),
283161
onFavoriteClick = {}
284162
),
285-
ProductCardType.Large(
163+
ProductCardType.Vertical(
286164
image = ImageUI(
287165
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/14641430/pexels-photo-14641430.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
288166
alt = ""
@@ -295,7 +173,7 @@ private fun mockProductsLarge() = listOf(
295173
),
296174
onFavoriteClick = {}
297175
),
298-
ProductCardType.Large(
176+
ProductCardType.Vertical(
299177
image = ImageUI(
300178
images = persistentListOf(ImageSizeUI.Large("https://images.pexels.com/photos/9603628/pexels-photo-9603628.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1")),
301179
alt = ""

0 commit comments

Comments
 (0)