Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
Binary file removed app/debug/app-debug.apk
Binary file not shown.
Binary file removed app/debug/app-debug.apk.zip
Binary file not shown.
21 changes: 0 additions & 21 deletions app/debug/output-metadata.json

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ internal fun Project.configureKotlinAndroid() {
implementation(platform(libs.findLibrary("firebase-bom").get()))
implementation(libs.findLibrary("firebase-analytics").get())
implementation(libs.findLibrary("firebase-crashlytics").get())

implementation(libs.findLibrary("kotlinx-collections-immutable").get())
}

configureKotlin()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ internal fun Project.configureAndroidCompose() {
dependencies {
val bom = libs.findLibrary("androidx-compose-bom").get()
add("implementation", platform(bom))
add("implementation", libs.findLibrary("compose.material3.adaptive").get())
add("implementation", libs.findLibrary("androidx.compose.material").get())
add("implementation", libs.findLibrary("androidx.compose.material3").get())
add("implementation", libs.findLibrary("androidx.compose.ui").get())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import android.app.Activity
import com.puzzle.billing.model.PieceProduct
import com.puzzle.domain.model.payment.CashProduct
import com.puzzle.domain.model.payment.PurchaseProduct
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.StateFlow

interface BillingHelper {
val purchaseProduct: StateFlow<PurchaseProduct?>
suspend fun getAvailableProducts(cashProducts: CashProduct): List<PieceProduct>
suspend fun getAvailableProducts(cashProducts: CashProduct): ImmutableList<PieceProduct>
fun purchaseProduct(activity: Activity, purchaseProduct: PieceProduct)
// fun consumeProductList(purchaseProductList: List<PurchaseProduct>)
suspend fun getUnconsumedProductList() : List<PurchaseProduct>
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,13 @@ data class NormalProduct(
val description: String,
val priceAmountMicros: Long,
val currencyCode: String,
val isOnSale: Boolean, // 서버에서 내려줘야 함
val originPrice: String, // 서버에서 내려줘야 함
val salePercent: Int, // 서버에서 내려줘야 함
val isOnSale: Boolean,
val originPrice: String,
val salePercent: Int
) : PieceProduct

data class PromotionProduct(
override val detail: ProductDetails,
override val price: String,
val imageUrl : String,
// val eventName: String, // 서버에서 내려줘야 함
// val eventDescription: String, // 서버에서 내려줘야 함
// val priceAmountMicros: Long,
// val currencyCode: String,
// val originPrice: String, // 서버에서 내려줘야 함
// val salePercent: Int, // 서버에서 내려줘야 함
// val itemCount: Int, // 서버에서 내려줘야 함
// val benefitDescription: String, // 서버에서 내려줘야 함
val imageUrl : String
) : PieceProduct
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.puzzle.domain.model.payment.CashProduct
import com.puzzle.domain.model.payment.Product
import com.puzzle.domain.model.payment.PurchaseProduct
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -61,7 +63,7 @@ class BillingHelperImpl @Inject constructor(
.enableAutoServiceReconnection()
.build()

override suspend fun getAvailableProducts(cashProducts: CashProduct): List<PieceProduct> {
override suspend fun getAvailableProducts(cashProducts: CashProduct): ImmutableList<PieceProduct> {
connectGooglePlayIfNeeded()

val productList = cashProducts.products.map {
Expand All @@ -79,14 +81,19 @@ class BillingHelperImpl @Inject constructor(
val products = result.productDetailsList ?: emptyList()

// 3. 서버 상품과 Google Play 상품 매칭 후 변환
// PromotionProduct 먼저, NormalProduct 나중
return products.mapNotNull { product ->
val serverProduct = cashProducts.products.find { it.uuid == product.productId }
serverProduct?.let { product.toPieceProduct(it) }
serverProduct?.let {
convertToPieceProduct(
productDetail = product,
serverProduct = it
)
}
}.let { list ->
// PromotionProduct와 NormalProduct 분리
val (promotions, normals) = list.partition { it is PromotionProduct }
// Promotion 먼저 표시
promotions + normals
// ImmutableList로 변환
(promotions + normals).toImmutableList()
}
}

Expand All @@ -111,47 +118,6 @@ class BillingHelperImpl @Inject constructor(
}
}

//consume 은 서버단에서 진행
// override fun consumeProductList(purchaseProductList: List<PurchaseProduct>) {
// purchaseProductList.forEach { purchaseProduct ->
// val consumeParams = ConsumeParams.newBuilder()
// .setPurchaseToken(purchaseProduct.credential)
// .build()
//
// billingClient.consumeAsync(consumeParams) { billingResult, _ ->
// if (billingResult.responseCode == BillingResponseCode.OK) {
// _purchaseProduct.value = null
// }
// }
// }

// consume 되지 않은 상품 획득
override suspend fun getUnconsumedProductList(): List<PurchaseProduct> {
connectGooglePlayIfNeeded()

return suspendCancellableCoroutine { continuation ->
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
) { result, purchases ->
if (result.responseCode == BillingResponseCode.OK) {
val list = purchases
.filter { !it.isAcknowledged }
.map { purchase ->
PurchaseProduct(
uuid = purchase.products.firstOrNull().orEmpty(),
credential = purchase.purchaseToken
)
}
continuation.resume(list) { cause, _, _ -> }
} else {
continuation.resume(emptyList()) { cause, _, _ -> }
}
}
}
}

private suspend fun connectGooglePlayIfNeeded() = suspendCancellableCoroutine { continuation ->
if (billingClient.isReady) {
continuation.resume(Result.success(Unit))
Expand All @@ -168,17 +134,19 @@ class BillingHelperImpl @Inject constructor(
})
}

// 위치가 고민스럽네요..
private fun ProductDetails.toPieceProduct(serverProduct: Product): PieceProduct? {
val offer = this.oneTimePurchaseOfferDetails ?: return null
this.oneTimePurchaseOfferDetails?.formattedPrice
private fun convertToPieceProduct(
productDetail: ProductDetails,
serverProduct: Product
): PieceProduct? {
val offer = productDetail.oneTimePurchaseOfferDetails ?: return null
productDetail.oneTimePurchaseOfferDetails?.formattedPrice

when (serverProduct) {
is Product.BasicCashProduct -> {
return NormalProduct(
detail = this,
name = this.name,
description = this.description,
detail = productDetail,
name = productDetail.name,
description = productDetail.description,
price = offer.formattedPrice,
currencyCode = offer.priceCurrencyCode,
priceAmountMicros = offer.priceAmountMicros,
Expand All @@ -190,7 +158,7 @@ class BillingHelperImpl @Inject constructor(

is Product.PromotionProduct -> {
return PromotionProduct(
detail = this,
detail = productDetail,
price = offer.formattedPrice,
imageUrl = serverProduct.imageUrl
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class PieceInterceptor @Inject constructor(
"/api/terms" -> false
"/api/valueTalks" -> false
"/api/valuePicks" -> false
// "/api/cash-products" -> false
else -> true
}
}
Expand Down
6 changes: 3 additions & 3 deletions feature/store/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ android {
}

dependencies {
implementation(libs.android.billingclient)

implementation(projects.core.common)
api(projects.core.billing)
implementation(projects.core.billing)

implementation(libs.compose.material3.adaptive)
}
14 changes: 8 additions & 6 deletions feature/store/src/main/java/com/puzzle/store/StoreScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.puzzle.billing.model.NormalProduct
import com.puzzle.billing.model.PieceProduct
import com.puzzle.billing.model.PromotionProduct
import com.puzzle.common.ui.AdaptiveLayout
import com.puzzle.store.ui.AdaptiveLayout
import com.puzzle.common.ui.clickable
import com.puzzle.designsystem.R
import com.puzzle.designsystem.component.PieceImage
Expand All @@ -45,6 +45,8 @@ import com.puzzle.store.contract.StoreIntent
import com.puzzle.store.contract.StoreState
import com.puzzle.store.ui.card.StoreCard
import com.puzzle.store.ui.text.DescriptionText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList

@Composable
internal fun StoreRoute(viewModel: StoreViewModel = hiltViewModel()) {
Expand Down Expand Up @@ -125,7 +127,7 @@ private fun ExpandedStoreScreen(

Row(modifier = Modifier.fillMaxSize()) {
ProductList(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분, UI레벨에서 말고 처음부터 StoreState로 매핑해줄 때 해주는 것이 더 깔끔해보여요!

products = state.products.filterIsInstance<PromotionProduct>(),
products = state.products.filterIsInstance<PromotionProduct>().toImmutableList(),
onPurchaseClick = onPurchaseClick,
modifier = Modifier
.weight(1f)
Expand All @@ -138,7 +140,7 @@ private fun ExpandedStoreScreen(
.weight(1f)
.fillMaxHeight()) {
ProductList(
products = state.products.filterIsInstance<NormalProduct>(),
products = state.products.filterIsInstance<NormalProduct>().toImmutableList(),
onPurchaseClick = onPurchaseClick,
)

Expand All @@ -159,7 +161,7 @@ private fun StoreTopBar(
modifier: Modifier = Modifier
) {
PiecePuzzleTopBar(
count = count, // 이 값도 state로부터 받는 것을 고려해볼 수 있습니다.
count = count,
chipColor = PieceTheme.colors.white,
contentColor = PieceTheme.colors.black,
isShowPlus = false,
Expand All @@ -179,7 +181,7 @@ private fun StoreTopBar(

@Composable
private fun ProductList(
products: List<PieceProduct>,
products: ImmutableList<PieceProduct>,
onPurchaseClick: (PieceProduct) -> Unit,
modifier: Modifier = Modifier
) {
Expand All @@ -190,7 +192,7 @@ private fun ProductList(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
) {
items(products, key = { it.detail.productId }) { product ->
items(products) { product ->
when (product) {
is PromotionProduct -> PieceImage(
model = product.imageUrl,
Expand Down
44 changes: 11 additions & 33 deletions feature/store/src/main/java/com/puzzle/store/StoreViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.puzzle.store

import android.content.ContentValues.TAG
import android.util.Log
import androidx.lifecycle.viewModelScope
import com.puzzle.billing.BillingHelper
Expand Down Expand Up @@ -29,9 +28,8 @@ class StoreViewModel @Inject constructor(

init {
viewModelScope.launch {
processUnConsumedProducts()
getAvailableProducts()
collectPurchaseProducts()
getAvailableProducts()
}
}

Expand All @@ -56,18 +54,16 @@ class StoreViewModel @Inject constructor(
}
}

private fun collectPurchaseProducts() {
viewModelScope.launch {
billingHelper.purchaseProduct.collectLatest { product ->
if (product != null) {
suspendRunCatching {
paymentRepository.purchaseProduct(product)
}.onSuccess {
Log.d([email protected], "purchaseProduct api success")
}.onFailure {
Log.e([email protected], "purchaseProduct api fail")
errorHelper.sendError(it)
}
private suspend fun collectPurchaseProducts() {
billingHelper.purchaseProduct.collectLatest { product ->
if (product != null) {
suspendRunCatching {
paymentRepository.purchaseProduct(product)
}.onSuccess {
Log.d([email protected], "purchaseProduct api success")
}.onFailure {
Log.e([email protected], "purchaseProduct api fail")
errorHelper.sendError(it)
}
}
}
Expand All @@ -76,30 +72,12 @@ class StoreViewModel @Inject constructor(
private suspend fun getAvailableProducts() {
suspendRunCatching {
val availableProducts = paymentRepository.getAvailableProduct()
Log.d("test", "viewModel : $availableProducts")
billingHelper.getAvailableProducts(availableProducts)
}.onSuccess { products ->
setState { copy(products = products) }
}.onFailure {
errorHelper.sendError(it)
}
}

// consume 되지 않은 상품 처리
private suspend fun processUnConsumedProducts() {
val unConsumedProducts = billingHelper.getUnconsumedProductList()

if (unConsumedProducts.isNotEmpty()) {
suspendRunCatching {
// paymentRepository.purchaseProductList(unConsumedProducts) 로 변경 예정
paymentRepository.purchaseProduct(unConsumedProducts.first())
}.onSuccess {
Log.d([email protected], "purchaseProduct api success")
}.onFailure {
Log.e([email protected], "purchaseProduct api fail")
errorHelper.sendError(it)
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.puzzle.store.contract

import com.puzzle.common.base.UiState
import com.puzzle.billing.model.PieceProduct
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

data class StoreState(
val products: List<PieceProduct> = emptyList(),
val products: ImmutableList<PieceProduct> = persistentListOf(),
) : UiState
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.puzzle.common.ui
package com.puzzle.store.ui

import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
Expand Down
Loading