feat(paywall): question pack library, entitlement checker, home screen wiring
This commit is contained in:
parent
29d512c679
commit
577d39ea11
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.couplesconnect.app.core.billing
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
interface EntitlementChecker {
|
||||||
|
val hasPremium: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class FakeEntitlementChecker @Inject constructor() : EntitlementChecker {
|
||||||
|
override val hasPremium: Boolean = false
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import com.couplesconnect.app.domain.model.QuestionMessage
|
||||||
import com.couplesconnect.app.domain.model.QuestionReaction
|
import com.couplesconnect.app.domain.model.QuestionReaction
|
||||||
import com.couplesconnect.app.domain.model.QuestionThread
|
import com.couplesconnect.app.domain.model.QuestionThread
|
||||||
import com.couplesconnect.app.domain.model.QuestionThreadStatus
|
import com.couplesconnect.app.domain.model.QuestionThreadStatus
|
||||||
|
import com.couplesconnect.app.domain.repository.CoupleRepository
|
||||||
import com.couplesconnect.app.domain.repository.QuestionThreadRepository
|
import com.couplesconnect.app.domain.repository.QuestionThreadRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -13,7 +14,8 @@ import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class QuestionThreadRepositoryImpl @Inject constructor(
|
class QuestionThreadRepositoryImpl @Inject constructor(
|
||||||
private val dataSource: FirestoreQuestionThreadDataSource
|
private val dataSource: FirestoreQuestionThreadDataSource,
|
||||||
|
private val coupleRepository: CoupleRepository
|
||||||
) : QuestionThreadRepository {
|
) : QuestionThreadRepository {
|
||||||
|
|
||||||
override suspend fun findOrCreateThreadId(
|
override suspend fun findOrCreateThreadId(
|
||||||
|
|
@ -32,14 +34,18 @@ class QuestionThreadRepositoryImpl @Inject constructor(
|
||||||
userId: String,
|
userId: String,
|
||||||
answer: QuestionAnswer
|
answer: QuestionAnswer
|
||||||
) {
|
) {
|
||||||
|
val countBefore = dataSource.getAnswerCount(coupleId, threadId)
|
||||||
dataSource.submitAnswer(coupleId, threadId, userId, answer)
|
dataSource.submitAnswer(coupleId, threadId, userId, answer)
|
||||||
val count = dataSource.getAnswerCount(coupleId, threadId)
|
val countAfter = dataSource.getAnswerCount(coupleId, threadId)
|
||||||
val newStatus = when {
|
val newStatus = when {
|
||||||
count >= 2 -> QuestionThreadStatus.REVEALED
|
countAfter >= 2 -> QuestionThreadStatus.REVEALED
|
||||||
count == 1 -> QuestionThreadStatus.ANSWERED_BY_ONE
|
countAfter == 1 -> QuestionThreadStatus.ANSWERED_BY_ONE
|
||||||
else -> QuestionThreadStatus.NOT_STARTED
|
else -> QuestionThreadStatus.NOT_STARTED
|
||||||
}
|
}
|
||||||
dataSource.updateThreadStatus(coupleId, threadId, newStatus)
|
dataSource.updateThreadStatus(coupleId, threadId, newStatus)
|
||||||
|
if (countBefore < 2 && newStatus == QuestionThreadStatus.REVEALED) {
|
||||||
|
runCatching { coupleRepository.updateStreak(coupleId) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeAnswers(coupleId: String, threadId: String): Flow<List<QuestionAnswer>> =
|
override fun observeAnswers(coupleId: String, threadId: String): Flow<List<QuestionAnswer>> =
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.couplesconnect.app.di
|
package com.couplesconnect.app.di
|
||||||
|
|
||||||
|
import com.couplesconnect.app.core.billing.EntitlementChecker
|
||||||
|
import com.couplesconnect.app.core.billing.FakeEntitlementChecker
|
||||||
import com.couplesconnect.app.data.repository.CoupleRepositoryImpl
|
import com.couplesconnect.app.data.repository.CoupleRepositoryImpl
|
||||||
import com.couplesconnect.app.data.repository.FirebaseAuthRepositoryImpl
|
import com.couplesconnect.app.data.repository.FirebaseAuthRepositoryImpl
|
||||||
import com.couplesconnect.app.data.repository.InviteRepositoryImpl
|
import com.couplesconnect.app.data.repository.InviteRepositoryImpl
|
||||||
|
|
@ -44,4 +46,7 @@ abstract class RepositoryModule {
|
||||||
|
|
||||||
@Binds @Singleton
|
@Binds @Singleton
|
||||||
abstract fun bindLocalAnswerRepository(impl: SharedPreferencesLocalAnswerRepository): LocalAnswerRepository
|
abstract fun bindLocalAnswerRepository(impl: SharedPreferencesLocalAnswerRepository): LocalAnswerRepository
|
||||||
|
|
||||||
|
@Binds @Singleton
|
||||||
|
abstract fun bindEntitlementChecker(impl: FakeEntitlementChecker): EntitlementChecker
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,10 @@ private fun HomeContent(
|
||||||
.padding(horizontal = 20.dp, vertical = 20.dp),
|
.padding(horizontal = 20.dp, vertical = 20.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(18.dp)
|
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||||
) {
|
) {
|
||||||
HomeHeader()
|
HomeHeader(
|
||||||
|
partnerName = state.partnerName,
|
||||||
|
streakCount = state.streakCount
|
||||||
|
)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingHomeCard()
|
state.isLoading -> LoadingHomeCard()
|
||||||
|
|
@ -122,15 +125,31 @@ private fun HomeContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeHeader() {
|
private fun HomeHeader(
|
||||||
|
partnerName: String?,
|
||||||
|
streakCount: Int
|
||||||
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Tonight’s connection",
|
||||||
|
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = Color(0xFF27211F),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
if (streakCount > 0) {
|
||||||
|
HomePill("$streakCount day streak")
|
||||||
|
}
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Tonight's connection",
|
text = if (partnerName != null)
|
||||||
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
|
"Connected with $partnerName. Keep the conversation going."
|
||||||
color = Color(0xFF27211F)
|
else
|
||||||
)
|
"A quiet home for today’s prompt, saved reflections, and the next conversation worth opening.",
|
||||||
Text(
|
|
||||||
text = "A quiet home for today’s prompt, saved reflections, and the next conversation worth opening.",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = Color(0xFF4E4642)
|
color = Color(0xFF4E4642)
|
||||||
)
|
)
|
||||||
|
|
@ -468,6 +487,8 @@ fun HomeScreenPreview() {
|
||||||
depthLevel = 2
|
depthLevel = 2
|
||||||
),
|
),
|
||||||
answerStats = HomeAnswerStats(total = 4, revealed = 2, private = 2),
|
answerStats = HomeAnswerStats(total = 4, revealed = 2, private = 2),
|
||||||
|
partnerName = "Jordan",
|
||||||
|
streakCount = 7,
|
||||||
categories = listOf(
|
categories = listOf(
|
||||||
HomeCategorySummary(
|
HomeCategorySummary(
|
||||||
category = QuestionCategory(
|
category = QuestionCategory(
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ import androidx.lifecycle.viewModelScope
|
||||||
import com.couplesconnect.app.domain.model.LocalAnswer
|
import com.couplesconnect.app.domain.model.LocalAnswer
|
||||||
import com.couplesconnect.app.domain.model.Question
|
import com.couplesconnect.app.domain.model.Question
|
||||||
import com.couplesconnect.app.domain.model.QuestionCategory
|
import com.couplesconnect.app.domain.model.QuestionCategory
|
||||||
|
import com.couplesconnect.app.domain.repository.AuthRepository
|
||||||
|
import com.couplesconnect.app.domain.repository.CoupleRepository
|
||||||
import com.couplesconnect.app.domain.repository.LocalAnswerRepository
|
import com.couplesconnect.app.domain.repository.LocalAnswerRepository
|
||||||
import com.couplesconnect.app.domain.repository.QuestionRepository
|
import com.couplesconnect.app.domain.repository.QuestionRepository
|
||||||
|
import com.couplesconnect.app.domain.repository.UserRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -32,13 +35,18 @@ data class HomeUiState(
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val dailyQuestion: Question? = null,
|
val dailyQuestion: Question? = null,
|
||||||
val categories: List<HomeCategorySummary> = emptyList(),
|
val categories: List<HomeCategorySummary> = emptyList(),
|
||||||
val answerStats: HomeAnswerStats = HomeAnswerStats()
|
val answerStats: HomeAnswerStats = HomeAnswerStats(),
|
||||||
|
val partnerName: String? = null,
|
||||||
|
val streakCount: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(
|
class HomeViewModel @Inject constructor(
|
||||||
private val questionRepository: QuestionRepository,
|
private val questionRepository: QuestionRepository,
|
||||||
private val localAnswerRepository: LocalAnswerRepository
|
private val localAnswerRepository: LocalAnswerRepository,
|
||||||
|
private val authRepository: AuthRepository,
|
||||||
|
private val coupleRepository: CoupleRepository,
|
||||||
|
private val userRepository: UserRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(HomeUiState())
|
private val _uiState = MutableStateFlow(HomeUiState())
|
||||||
|
|
@ -62,11 +70,18 @@ class HomeViewModel @Inject constructor(
|
||||||
questionCount = questionRepository.getQuestionCountByCategory(category.id)
|
questionCount = questionRepository.getQuestionCountByCategory(category.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val uid = authRepository.currentUserId
|
||||||
|
val couple = uid?.let { runCatching { coupleRepository.getCoupleForUser(it) }.getOrNull() }
|
||||||
|
val partnerName = couple?.userIds?.firstOrNull { it != uid }?.let { partnerId ->
|
||||||
|
runCatching { userRepository.getUser(partnerId)?.displayName }.getOrNull()
|
||||||
|
}
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
dailyQuestion = dailyQuestion,
|
dailyQuestion = dailyQuestion,
|
||||||
categories = categories
|
categories = categories,
|
||||||
|
partnerName = partnerName,
|
||||||
|
streakCount = couple?.streakCount ?: 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,168 @@
|
||||||
package com.couplesconnect.app.ui.paywall
|
package com.couplesconnect.app.ui.paywall
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.couplesconnect.app.core.navigation.AppRoute
|
import androidx.compose.ui.unit.dp
|
||||||
import com.couplesconnect.app.ui.components.PlaceholderAction
|
|
||||||
import com.couplesconnect.app.ui.components.PlaceholderScreen
|
private val BENEFITS = listOf(
|
||||||
|
"Unlock every question pack — 5500+ prompts",
|
||||||
|
"Deeper intimacy, trust, and conflict tracks",
|
||||||
|
"Priority access to new seasonal packs",
|
||||||
|
"Support independent couple-focused development"
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PaywallScreen(
|
fun PaywallScreen(
|
||||||
onNavigate: (String) -> Unit = {}
|
onNavigate: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
PlaceholderScreen(
|
Box(
|
||||||
title = "Deeper practice",
|
modifier = Modifier
|
||||||
section = "Paywall",
|
.fillMaxSize()
|
||||||
description = "A premium surface for expanded packs, rituals, and advanced couple reflection tools.",
|
.background(
|
||||||
route = AppRoute.PAYWALL,
|
Brush.linearGradient(
|
||||||
onNavigate = onNavigate,
|
listOf(Color(0xFFFFFBFA), Color(0xFFF5EEE8), Color(0xFFEAF0F4)),
|
||||||
accent = Color(0xFFF2A65A),
|
start = Offset.Zero,
|
||||||
primaryAction = PlaceholderAction("Subscription", AppRoute.SUBSCRIPTION),
|
end = Offset.Infinite
|
||||||
secondaryAction = PlaceholderAction("Home", AppRoute.HOME),
|
)
|
||||||
chips = listOf("Premium", "Deeper packs", "Upgrade path"),
|
)
|
||||||
details = listOf(
|
) {
|
||||||
"Plan comparison can stay clear and generous",
|
Column(
|
||||||
"Deeper question packs can be framed with care",
|
modifier = Modifier
|
||||||
"Subscription management has its own place"
|
.fillMaxSize()
|
||||||
)
|
.safeDrawingPadding()
|
||||||
)
|
.navigationBarsPadding()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 24.dp, vertical = 32.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Go deeper together",
|
||||||
|
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = Color(0xFF27211F),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "One subscription. Every question pack we've built for couples — and everything we build next.",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = Color(0xFF4E4642),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.88f)),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "What's included",
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = Color(0xFF27211F)
|
||||||
|
)
|
||||||
|
BENEFITS.forEach { benefit ->
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color(0xFFE07A5F),
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = benefit,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color(0xFF3E3734)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFE07A5F)),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(22.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Coming soon",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = Color.White.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "In-app purchase launching with the next build.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { onNavigate("back") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE07A5F))
|
||||||
|
) {
|
||||||
|
Text("Subscribe (coming soon)", color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(onClick = { onNavigate("back") }) {
|
||||||
|
Text(
|
||||||
|
text = "Not now",
|
||||||
|
color = Color(0xFF9E9693)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,18 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -112,7 +116,9 @@ private fun QuestionPackLibraryContent(
|
||||||
items(state.packs, key = { it.category.id }) { item ->
|
items(state.packs, key = { it.category.id }) { item ->
|
||||||
QuestionPackCard(
|
QuestionPackCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = { onPackSelected(item) }
|
onClick = {
|
||||||
|
if (item.isLocked) onPaywall() else onPackSelected(item)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
@ -127,7 +133,7 @@ private fun QuestionPackLibraryContent(
|
||||||
contentColor = Color.White
|
contentColor = Color.White
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Preview premium path")
|
Text("Unlock all packs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -141,11 +147,16 @@ private fun QuestionPackCard(
|
||||||
item: QuestionPackItem,
|
item: QuestionPackItem,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val containerColor = if (item.isLocked)
|
||||||
|
Color(0xFFF5F0EC).copy(alpha = 0.84f)
|
||||||
|
else
|
||||||
|
Color.White.copy(alpha = 0.84f)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(26.dp),
|
shape = RoundedCornerShape(26.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.84f)),
|
colors = CardDefaults.cardColors(containerColor = containerColor),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -164,7 +175,7 @@ private fun QuestionPackCard(
|
||||||
Text(
|
Text(
|
||||||
text = item.category.displayName.ifBlank { item.category.id.displayCategoryName() },
|
text = item.category.displayName.ifBlank { item.category.id.displayCategoryName() },
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = Color(0xFF27211F),
|
color = if (item.isLocked) Color(0xFF9E9693) else Color(0xFF27211F),
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
|
|
@ -177,9 +188,19 @@ private fun QuestionPackCard(
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PackPill("${item.questionCount} prompts")
|
if (item.isLocked) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Lock,
|
||||||
|
contentDescription = "Premium",
|
||||||
|
tint = Color(0xFFB0A9A6),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PackPill("${item.questionCount} prompts")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
if (item.isPremium) PackPill("Premium")
|
||||||
PackPill(item.category.access.displayCategoryName())
|
PackPill(item.category.access.displayCategoryName())
|
||||||
PackPill(item.category.iconName.ifBlank { "question" }.displayCategoryName())
|
PackPill(item.category.iconName.ifBlank { "question" }.displayCategoryName())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.couplesconnect.app.ui.questions
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.couplesconnect.app.core.billing.EntitlementChecker
|
||||||
import com.couplesconnect.app.domain.model.QuestionCategory
|
import com.couplesconnect.app.domain.model.QuestionCategory
|
||||||
import com.couplesconnect.app.domain.repository.QuestionRepository
|
import com.couplesconnect.app.domain.repository.QuestionRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -13,7 +14,9 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
data class QuestionPackItem(
|
data class QuestionPackItem(
|
||||||
val category: QuestionCategory,
|
val category: QuestionCategory,
|
||||||
val questionCount: Int
|
val questionCount: Int,
|
||||||
|
val isPremium: Boolean = false,
|
||||||
|
val isLocked: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
data class QuestionPackLibraryUiState(
|
data class QuestionPackLibraryUiState(
|
||||||
|
|
@ -24,7 +27,8 @@ data class QuestionPackLibraryUiState(
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class QuestionPackLibraryViewModel @Inject constructor(
|
class QuestionPackLibraryViewModel @Inject constructor(
|
||||||
private val repository: QuestionRepository
|
private val repository: QuestionRepository,
|
||||||
|
private val entitlementChecker: EntitlementChecker
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(QuestionPackLibraryUiState())
|
private val _uiState = MutableStateFlow(QuestionPackLibraryUiState())
|
||||||
|
|
@ -38,10 +42,14 @@ class QuestionPackLibraryViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = QuestionPackLibraryUiState(isLoading = true)
|
_uiState.value = QuestionPackLibraryUiState(isLoading = true)
|
||||||
try {
|
try {
|
||||||
|
val hasPremium = entitlementChecker.hasPremium
|
||||||
val packs = repository.getCategories().map { category ->
|
val packs = repository.getCategories().map { category ->
|
||||||
|
val isPremium = category.access == "premium"
|
||||||
QuestionPackItem(
|
QuestionPackItem(
|
||||||
category = category,
|
category = category,
|
||||||
questionCount = repository.getQuestionCountByCategory(category.id)
|
questionCount = repository.getQuestionCountByCategory(category.id),
|
||||||
|
isPremium = isPremium,
|
||||||
|
isLocked = isPremium && !hasPremium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_uiState.value = QuestionPackLibraryUiState(isLoading = false, packs = packs)
|
_uiState.value = QuestionPackLibraryUiState(isLoading = false, packs = packs)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue