From e65b696faf71e16d5f47bdac764c8461cb0d5823 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Minh Date: Sat, 30 Nov 2024 13:29:10 +0700 Subject: [PATCH] =?UTF-8?q?feat(explore):=20t=E1=BA=A1o=20study=20set=20b?= =?UTF-8?q?=E1=BA=B1ng=20AI=20(s=E1=BB=AD=20d=E1=BB=A5ng=20coin=20=C4=91?= =?UTF-8?q?=E1=BB=83=20t=E1=BA=A1o)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 +- .../quickmem/core/data/enums/CoinAction.kt | 6 ++ .../quickmem/core/datastore/AppManager.kt | 14 +++ .../quickmem/data/dto/auth/AuthResponseDto.kt | 2 + .../dto/auth/GetUserProfileResponseDto.kt | 2 + .../data/dto/user/UpdateCoinRequestDto.kt | 12 +++ .../data/dto/user/UpdateCoinResponseDto.kt | 12 +++ .../auth/GetUserProfileResponseMapper.kt | 6 +- .../mapper/user/UpdateCoinRequestMapper.kt | 16 ++++ .../mapper/user/UpdateCoinResponseMapper.kt | 16 ++++ .../pwhs/quickmem/data/remote/ApiService.kt | 8 ++ .../remote/repository/AuthRepositoryImpl.kt | 22 +++++ .../domain/model/auth/AuthResponseModel.kt | 1 + .../model/auth/GetUserProfileResponseModel.kt | 1 + .../model/users/UpdateCoinRequestModel.kt | 7 ++ .../model/users/UpdateCoinResponseModel.kt | 7 ++ .../domain/repository/AuthRepository.kt | 6 ++ .../presentation/app/explore/ExploreScreen.kt | 88 +++++++++++++++++- .../app/explore/ExploreUiAction.kt | 1 + .../app/explore/ExploreUiState.kt | 5 +- .../app/explore/ExploreViewModel.kt | 93 +++++++++++++++++-- .../CreateStudySetAITab.kt | 32 +------ .../app/profile/ProfileViewModel.kt | 4 + .../login/email/LoginWithEmailViewModel.kt | 1 + .../com/pwhs/quickmem/util/ads/AdsUtil.kt | 35 +------ app/src/main/res/drawable/ic_coin.xml | 39 ++++++++ 26 files changed, 358 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/com/pwhs/quickmem/core/data/enums/CoinAction.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/data/dto/user/UpdateCoinRequestDto.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/data/dto/user/UpdateCoinResponseDto.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/data/mapper/user/UpdateCoinRequestMapper.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/data/mapper/user/UpdateCoinResponseMapper.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/domain/model/users/UpdateCoinRequestModel.kt create mode 100644 app/src/main/java/com/pwhs/quickmem/domain/model/users/UpdateCoinResponseModel.kt create mode 100644 app/src/main/res/drawable/ic_coin.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e9e2e707..39c8fb2b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -40,7 +40,7 @@ android { localProperties.getProperty("REWARD_ADS_ID") ?: "ca-app-pub-5725743620724195/5188260450" val rewardedInterstitialAdsId: String = localProperties.getProperty("REWARDED_INTERSTITIAL_ADS_ID") - ?: "ca-app-pub-3940256099942544/5354046379" + ?: "ca-app-pub-5725743620724195/6760705307" val oneSignalAppId: String = localProperties.getProperty("ONESIGNAL_APP_ID") ?: "b2f7f966-d8cc-11e4-bed1-df8f05be55ba" val revenueCatApiKey: String = localProperties.getProperty("REVENUECAT_API_KEY") diff --git a/app/src/main/java/com/pwhs/quickmem/core/data/enums/CoinAction.kt b/app/src/main/java/com/pwhs/quickmem/core/data/enums/CoinAction.kt new file mode 100644 index 00000000..a7a11fa9 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/core/data/enums/CoinAction.kt @@ -0,0 +1,6 @@ +package com.pwhs.quickmem.core.data.enums + +enum class CoinAction (val action: String) { + ADD("add"), + SUBTRACT("subtract") +} \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/core/datastore/AppManager.kt b/app/src/main/java/com/pwhs/quickmem/core/datastore/AppManager.kt index 161a7e99..e2b59220 100644 --- a/app/src/main/java/com/pwhs/quickmem/core/datastore/AppManager.kt +++ b/app/src/main/java/com/pwhs/quickmem/core/datastore/AppManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Patterns import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import com.pwhs.quickmem.util.dataStore import kotlinx.coroutines.flow.Flow @@ -23,6 +24,7 @@ class AppManager(private val context: Context) { val USER_BIRTHDAY = stringPreferencesKey("USER_BIRTHDAY") val PUSH_NOTIFICATIONS = booleanPreferencesKey("PUSH_NOTIFICATIONS") val APP_PUSH_NOTIFICATIONS = booleanPreferencesKey("APP_PUSH_NOTIFICATIONS") + val USER_COINS = intPreferencesKey("USER_COINS") } val isFirstRun: Flow = context.dataStore.data @@ -72,6 +74,11 @@ class AppManager(private val context: Context) { preferences[USER_BIRTHDAY] ?: "" } + val userCoins: Flow = context.dataStore.data + .map { preferences -> + preferences[USER_COINS] ?: 0 + } + suspend fun saveIsFirstRun(isFirstRun: Boolean) { Timber.d("Saving is first run: $isFirstRun") context.dataStore.edit { preferences -> @@ -150,6 +157,13 @@ class AppManager(private val context: Context) { } } + suspend fun saveUserCoins(coins: Int) { + Timber.d("Saving user coins: $coins") + context.dataStore.edit { preferences -> + preferences[USER_COINS] = coins + } + } + suspend fun clearAllData() { context.dataStore.edit { preferences -> preferences.clear() diff --git a/app/src/main/java/com/pwhs/quickmem/data/dto/auth/AuthResponseDto.kt b/app/src/main/java/com/pwhs/quickmem/data/dto/auth/AuthResponseDto.kt index 8b885e44..e7c555e9 100644 --- a/app/src/main/java/com/pwhs/quickmem/data/dto/auth/AuthResponseDto.kt +++ b/app/src/main/java/com/pwhs/quickmem/data/dto/auth/AuthResponseDto.kt @@ -25,4 +25,6 @@ data class AuthResponseDto( val provider: String? = null, @SerializedName("isVerified") val isVerified: Boolean? = null, + @SerializedName("coin") + val coin: Int? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/dto/auth/GetUserProfileResponseDto.kt b/app/src/main/java/com/pwhs/quickmem/data/dto/auth/GetUserProfileResponseDto.kt index f752ad4f..60e01290 100644 --- a/app/src/main/java/com/pwhs/quickmem/data/dto/auth/GetUserProfileResponseDto.kt +++ b/app/src/main/java/com/pwhs/quickmem/data/dto/auth/GetUserProfileResponseDto.kt @@ -15,6 +15,8 @@ data class GetUserProfileResponseDto( val avatarUrl: String, @SerializedName("role") val role: String, + @SerializedName("coin") + val coin: Int, @SerializedName("createdAt") val createdAt: String, @SerializedName("updatedAt") diff --git a/app/src/main/java/com/pwhs/quickmem/data/dto/user/UpdateCoinRequestDto.kt b/app/src/main/java/com/pwhs/quickmem/data/dto/user/UpdateCoinRequestDto.kt new file mode 100644 index 00000000..2f771a4e --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/data/dto/user/UpdateCoinRequestDto.kt @@ -0,0 +1,12 @@ +package com.pwhs.quickmem.data.dto.user + +import com.google.gson.annotations.SerializedName + +data class UpdateCoinRequestDto( + @SerializedName("userId") + val userId: String, + @SerializedName("coin") + val coin: Int, + @SerializedName("action") + val action: String +) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/dto/user/UpdateCoinResponseDto.kt b/app/src/main/java/com/pwhs/quickmem/data/dto/user/UpdateCoinResponseDto.kt new file mode 100644 index 00000000..257d3f58 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/data/dto/user/UpdateCoinResponseDto.kt @@ -0,0 +1,12 @@ +package com.pwhs.quickmem.data.dto.user + +import com.google.gson.annotations.SerializedName + +data class UpdateCoinResponseDto( + @SerializedName("message") + val message: String, + @SerializedName("coinAction") + val coinAction: String, + @SerializedName("coins") + val coins: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/mapper/auth/GetUserProfileResponseMapper.kt b/app/src/main/java/com/pwhs/quickmem/data/mapper/auth/GetUserProfileResponseMapper.kt index 7d53daaf..c6eda0b1 100644 --- a/app/src/main/java/com/pwhs/quickmem/data/mapper/auth/GetUserProfileResponseMapper.kt +++ b/app/src/main/java/com/pwhs/quickmem/data/mapper/auth/GetUserProfileResponseMapper.kt @@ -11,7 +11,8 @@ fun GetUserProfileResponseDto.toModel() = GetUserProfileResponseModel( username = username, avatarUrl = avatarUrl, createdAt = createdAt, - updatedAt = updatedAt + updatedAt = updatedAt, + coin = coin ) fun GetUserProfileResponseModel.toDto() = GetUserProfileResponseDto( @@ -22,5 +23,6 @@ fun GetUserProfileResponseModel.toDto() = GetUserProfileResponseDto( username = username, avatarUrl = avatarUrl, createdAt = createdAt, - updatedAt = updatedAt + updatedAt = updatedAt, + coin = coin ) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/mapper/user/UpdateCoinRequestMapper.kt b/app/src/main/java/com/pwhs/quickmem/data/mapper/user/UpdateCoinRequestMapper.kt new file mode 100644 index 00000000..44068368 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/data/mapper/user/UpdateCoinRequestMapper.kt @@ -0,0 +1,16 @@ +package com.pwhs.quickmem.data.mapper.user + +import com.pwhs.quickmem.data.dto.user.UpdateCoinRequestDto +import com.pwhs.quickmem.domain.model.users.UpdateCoinRequestModel + +fun UpdateCoinRequestDto.toModel() = UpdateCoinRequestModel( + userId = userId, + coin = coin, + action = action +) + +fun UpdateCoinRequestModel.toDto() = UpdateCoinRequestDto( + userId = userId, + coin = coin, + action = action +) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/mapper/user/UpdateCoinResponseMapper.kt b/app/src/main/java/com/pwhs/quickmem/data/mapper/user/UpdateCoinResponseMapper.kt new file mode 100644 index 00000000..57877df9 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/data/mapper/user/UpdateCoinResponseMapper.kt @@ -0,0 +1,16 @@ +package com.pwhs.quickmem.data.mapper.user + +import com.pwhs.quickmem.data.dto.user.UpdateCoinResponseDto +import com.pwhs.quickmem.domain.model.users.UpdateCoinResponseModel + +fun UpdateCoinResponseDto.toModel() = UpdateCoinResponseModel( + message = message, + coinAction = coinAction, + coins = coins +) + +fun UpdateCoinResponseModel.toDto() = UpdateCoinResponseDto( + message = message, + coinAction = coinAction, + coins = coins +) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/data/remote/ApiService.kt b/app/src/main/java/com/pwhs/quickmem/data/remote/ApiService.kt index cd27dad4..1c214eda 100644 --- a/app/src/main/java/com/pwhs/quickmem/data/remote/ApiService.kt +++ b/app/src/main/java/com/pwhs/quickmem/data/remote/ApiService.kt @@ -81,6 +81,8 @@ import com.pwhs.quickmem.data.dto.subject.GetTop5SubjectResponseDto import com.pwhs.quickmem.data.dto.upload.DeleteImageDto import com.pwhs.quickmem.data.dto.upload.UploadImageResponseDto import com.pwhs.quickmem.data.dto.user.SearchUserResponseDto +import com.pwhs.quickmem.data.dto.user.UpdateCoinRequestDto +import com.pwhs.quickmem.data.dto.user.UpdateCoinResponseDto import com.pwhs.quickmem.data.dto.user.UserDetailResponseDto import okhttp3.MultipartBody import retrofit2.Response @@ -183,6 +185,12 @@ interface ApiService { @Query("page") page: Int? ): List + @POST("auth/coin") + suspend fun updateCoin( + @Header("Authorization") token: String, + @Body request: UpdateCoinRequestDto + ): UpdateCoinResponseDto + // Upload @Multipart @POST("upload") diff --git a/app/src/main/java/com/pwhs/quickmem/data/remote/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/pwhs/quickmem/data/remote/repository/AuthRepositoryImpl.kt index 1733757e..5dc99d3f 100644 --- a/app/src/main/java/com/pwhs/quickmem/data/remote/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/pwhs/quickmem/data/remote/repository/AuthRepositoryImpl.kt @@ -8,6 +8,7 @@ import com.pwhs.quickmem.core.utils.Resources import com.pwhs.quickmem.data.dto.verify_email.EmailRequestDto import com.pwhs.quickmem.data.mapper.auth.toDto import com.pwhs.quickmem.data.mapper.auth.toModel +import com.pwhs.quickmem.data.mapper.user.toDto import com.pwhs.quickmem.data.mapper.user.toModel import com.pwhs.quickmem.data.paging.UserPagingSource import com.pwhs.quickmem.data.remote.ApiService @@ -40,6 +41,8 @@ import com.pwhs.quickmem.domain.model.auth.VerifyEmailResponseModel import com.pwhs.quickmem.domain.model.auth.VerifyPasswordRequestModel import com.pwhs.quickmem.domain.model.auth.VerifyPasswordResponseModel import com.pwhs.quickmem.domain.model.users.SearchUserResponseModel +import com.pwhs.quickmem.domain.model.users.UpdateCoinRequestModel +import com.pwhs.quickmem.domain.model.users.UpdateCoinResponseModel import com.pwhs.quickmem.domain.model.users.UserDetailResponseModel import com.pwhs.quickmem.domain.repository.AuthRepository import kotlinx.coroutines.flow.Flow @@ -363,4 +366,23 @@ class AuthRepositoryImpl @Inject constructor( } } } + + override suspend fun updateCoin( + token: String, + updateCoinRequestModel: UpdateCoinRequestModel + ): Flow> { + return flow { + emit(Resources.Loading()) + try { + val response = apiService.updateCoin( + token, + updateCoinRequestModel.toDto() + ) + emit(Resources.Success(response.toModel())) + } catch (e: Exception) { + Timber.e(e) + emit(Resources.Error(e.toString())) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/domain/model/auth/AuthResponseModel.kt b/app/src/main/java/com/pwhs/quickmem/domain/model/auth/AuthResponseModel.kt index 51cfe1c2..6877baf3 100644 --- a/app/src/main/java/com/pwhs/quickmem/domain/model/auth/AuthResponseModel.kt +++ b/app/src/main/java/com/pwhs/quickmem/domain/model/auth/AuthResponseModel.kt @@ -12,4 +12,5 @@ data class AuthResponseModel( val refreshToken: String? = null, val provider: String? = null, val isVerified: Boolean? = null, + val coin: Int? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/domain/model/auth/GetUserProfileResponseModel.kt b/app/src/main/java/com/pwhs/quickmem/domain/model/auth/GetUserProfileResponseModel.kt index 1322c85b..98201dcd 100644 --- a/app/src/main/java/com/pwhs/quickmem/domain/model/auth/GetUserProfileResponseModel.kt +++ b/app/src/main/java/com/pwhs/quickmem/domain/model/auth/GetUserProfileResponseModel.kt @@ -7,6 +7,7 @@ data class GetUserProfileResponseModel( val email: String, val avatarUrl: String, val role: String, + val coin: Int, val createdAt: String, val updatedAt: String ) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/domain/model/users/UpdateCoinRequestModel.kt b/app/src/main/java/com/pwhs/quickmem/domain/model/users/UpdateCoinRequestModel.kt new file mode 100644 index 00000000..002eb3de --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/domain/model/users/UpdateCoinRequestModel.kt @@ -0,0 +1,7 @@ +package com.pwhs.quickmem.domain.model.users + +data class UpdateCoinRequestModel( + val userId: String, + val coin: Int, + val action: String +) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/domain/model/users/UpdateCoinResponseModel.kt b/app/src/main/java/com/pwhs/quickmem/domain/model/users/UpdateCoinResponseModel.kt new file mode 100644 index 00000000..ee6f5741 --- /dev/null +++ b/app/src/main/java/com/pwhs/quickmem/domain/model/users/UpdateCoinResponseModel.kt @@ -0,0 +1,7 @@ +package com.pwhs.quickmem.domain.model.users + +data class UpdateCoinResponseModel( + val message: String, + val coinAction: String, + val coins: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/domain/repository/AuthRepository.kt b/app/src/main/java/com/pwhs/quickmem/domain/repository/AuthRepository.kt index adf861ce..92b48edc 100644 --- a/app/src/main/java/com/pwhs/quickmem/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/pwhs/quickmem/domain/repository/AuthRepository.kt @@ -29,6 +29,8 @@ import com.pwhs.quickmem.domain.model.auth.VerifyEmailResponseModel import com.pwhs.quickmem.domain.model.auth.VerifyPasswordRequestModel import com.pwhs.quickmem.domain.model.auth.VerifyPasswordResponseModel import com.pwhs.quickmem.domain.model.users.SearchUserResponseModel +import com.pwhs.quickmem.domain.model.users.UpdateCoinRequestModel +import com.pwhs.quickmem.domain.model.users.UpdateCoinResponseModel import com.pwhs.quickmem.domain.model.users.UserDetailResponseModel import kotlinx.coroutines.flow.Flow @@ -110,4 +112,8 @@ interface AuthRepository { changeRoleRequestModel: ChangeRoleRequestModel ): Flow> + suspend fun updateCoin( + token: String, + updateCoinRequestModel: UpdateCoinRequestModel + ): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreScreen.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreScreen.kt index d5182b28..04476fcd 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreScreen.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreScreen.kt @@ -1,11 +1,21 @@ package com.pwhs.quickmem.presentation.app.explore import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography @@ -23,25 +33,31 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.pwhs.quickmem.R import com.pwhs.quickmem.core.data.enums.DifficultyLevel import com.pwhs.quickmem.core.data.enums.QuestionType import com.pwhs.quickmem.domain.model.streak.GetTopStreakResponseModel import com.pwhs.quickmem.presentation.app.explore.create_study_set_ai.CreateStudySetAITab import com.pwhs.quickmem.presentation.app.explore.top_streak.TopStreakScreen import com.pwhs.quickmem.presentation.component.LoadingOverlay +import com.pwhs.quickmem.util.ads.AdsUtil import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.StudySetDetailScreenDestination import com.ramcosta.composedestinations.generated.destinations.UserDetailScreenDestination import com.ramcosta.composedestinations.generated.destinations.UserDetailScreenDestination.invoke import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.revenuecat.purchases.CustomerInfo @Composable @Destination @@ -105,7 +121,10 @@ fun ExploreScreen( onCreateStudySet = { viewModel.onEvent(ExploreUiAction.OnCreateStudySet) }, - errorMessage = uiState.errorMessage + errorMessage = uiState.errorMessage, + coins = uiState.coins, + onEarnCoins = { viewModel.onEvent(ExploreUiAction.OnEarnCoins) }, + customerInfo = uiState.customerInfo ) } @@ -132,13 +151,17 @@ fun Explore( onQuestionTypeChange: (QuestionType) -> Unit = {}, onDifficultyLevelChange: (DifficultyLevel) -> Unit = {}, onCreateStudySet: () -> Unit = {}, - errorMessage: String = "" + onEarnCoins: () -> Unit = {}, + errorMessage: String = "", + coins: Int = 0, + customerInfo: CustomerInfo? = null ) { var tabIndex by rememberSaveable { mutableIntStateOf(0) } val tabTitles = listOf( "Create Study Set AI", "Top Streak", ) + val context = LocalContext.current Scaffold( modifier = modifier, @@ -153,7 +176,54 @@ fun Explore( ) }, actions = { - + if (tabIndex == ExploreTabEnum.TOP_STREAK.index) { + IconButton( + onClick = onTopStreakRefresh + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh", + ) + } + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(end = 16.dp) + ) { + Text( + text = when (customerInfo?.activeSubscriptions?.isNotEmpty()) { + true -> "Unlimited" + false -> coins.toString() + else -> "0" + }, + style = typography.titleMedium.copy( + fontWeight = FontWeight.Bold + ) + ) + Image( + painter = painterResource(id = R.drawable.ic_coin), + contentDescription = "Coins", + modifier = Modifier.size(24.dp), + contentScale = ContentScale.Crop + ) + if (customerInfo?.activeSubscriptions?.isNotEmpty() == false) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add", + tint = colorScheme.primary, + modifier = Modifier + .size(24.dp) + .clickable { + AdsUtil.rewardedInterstitialAd( + context, + onEarnCoins + ) + }, + ) + } + } + } } ) } @@ -202,7 +272,17 @@ fun Explore( onLanguageChange = onLanguageChange, onQuestionTypeChange = onQuestionTypeChange, onDifficultyLevelChange = onDifficultyLevelChange, - onCreateStudySet = onCreateStudySet + onCreateStudySet = { + if (coins > 0) { + onCreateStudySet() + } else { + Toast.makeText( + context, + "You need at least 1 coin to create a study set", + Toast.LENGTH_SHORT + ).show() + } + } ) ExploreTabEnum.TOP_STREAK.index -> TopStreakScreen( diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreUiAction.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreUiAction.kt index 70956200..3d57221f 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreUiAction.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreUiAction.kt @@ -12,4 +12,5 @@ sealed class ExploreUiAction { data class OnQuestionTypeChanged(val questionType: QuestionType) : ExploreUiAction() data class OnDifficultyLevelChanged(val difficultyLevel: DifficultyLevel) : ExploreUiAction() data object OnCreateStudySet : ExploreUiAction() + data object OnEarnCoins : ExploreUiAction() } \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreUiState.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreUiState.kt index 3818a6c9..403e342f 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreUiState.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreUiState.kt @@ -4,12 +4,14 @@ import com.pwhs.quickmem.core.data.enums.DifficultyLevel import com.pwhs.quickmem.core.data.enums.LanguageCode import com.pwhs.quickmem.core.data.enums.QuestionType import com.pwhs.quickmem.domain.model.streak.GetTopStreakResponseModel +import com.revenuecat.purchases.CustomerInfo data class ExploreUiState( val isLoading: Boolean = false, val ownerId: String = "", val topStreaks: List = emptyList(), val streakOwner: GetTopStreakResponseModel? = null, + val customerInfo: CustomerInfo? = null, val rankOwner: Int? = null, // AI val description: String = "", @@ -18,5 +20,6 @@ data class ExploreUiState( val numberOfFlashcards: Int = 15, val questionType: QuestionType = QuestionType.MULTIPLE_CHOICE, val title: String = "", - val errorMessage: String = "" + val errorMessage: String = "", + val coins: Int = 0 ) \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreViewModel.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreViewModel.kt index df4d55f5..46a5d9cd 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreViewModel.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/ExploreViewModel.kt @@ -3,24 +3,33 @@ package com.pwhs.quickmem.presentation.app.explore import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.pwhs.quickmem.core.data.enums.CoinAction import com.pwhs.quickmem.core.data.enums.DifficultyLevel import com.pwhs.quickmem.core.data.enums.QuestionType import com.pwhs.quickmem.core.datastore.AppManager import com.pwhs.quickmem.core.datastore.TokenManager import com.pwhs.quickmem.core.utils.Resources import com.pwhs.quickmem.domain.model.study_set.CreateStudySetByAIRequestModel +import com.pwhs.quickmem.domain.model.users.UpdateCoinRequestModel +import com.pwhs.quickmem.domain.repository.AuthRepository import com.pwhs.quickmem.domain.repository.StreakRepository import com.pwhs.quickmem.domain.repository.StudySetRepository import com.pwhs.quickmem.util.getLanguageCode +import com.revenuecat.purchases.CustomerInfo +import com.revenuecat.purchases.Purchases +import com.revenuecat.purchases.PurchasesError +import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -29,6 +38,7 @@ class ExploreViewModel @Inject constructor( private val appManager: AppManager, private val streakRepository: StreakRepository, private val studySetRepository: StudySetRepository, + private val authRepository: AuthRepository, application: Application ) : AndroidViewModel(application) { private val _uiState = MutableStateFlow(ExploreUiState()) @@ -40,15 +50,19 @@ class ExploreViewModel @Inject constructor( init { val languageCode = getApplication().getLanguageCode() viewModelScope.launch { - val userId = appManager.userId.firstOrNull() ?: "" - _uiState.update { - it.copy( - ownerId = userId, - language = languageCode - ) + combine(appManager.userId, appManager.userCoins) { userId, coins -> + _uiState.update { + it.copy( + ownerId = userId, + coins = coins, + language = languageCode + ) + } + }.collectLatest { + getTopStreaks() + getCustomerInfo() } } - getTopStreaks() } fun onEvent(event: ExploreUiAction) { @@ -94,6 +108,10 @@ class ExploreViewModel @Inject constructor( createStudySet() } } + + is ExploreUiAction.OnEarnCoins -> { + updateCoins(coinAction = CoinAction.ADD, coin = 1) + } } } @@ -170,7 +188,14 @@ class ExploreViewModel @Inject constructor( language = getApplication().getLanguageCode() ) } - _uiEvent.send(ExploreUiEvent.CreatedStudySet(resource.data?.id ?: "")) + if (_uiState.value.customerInfo?.activeSubscriptions?.isNotEmpty() == false) { + updateCoins(coinAction = CoinAction.SUBTRACT, coin = 1) + } + _uiEvent.send( + ExploreUiEvent.CreatedStudySet( + studySetId = resource.data?.id ?: "" + ) + ) } is Resources.Error -> { @@ -186,4 +211,54 @@ class ExploreViewModel @Inject constructor( } } } + + private fun updateCoins( + coinAction: CoinAction, + coin: Int = 1 + ) { + viewModelScope.launch { + val token = tokenManager.accessToken.firstOrNull() ?: "" + val userId = appManager.userId.firstOrNull() ?: "" + authRepository.updateCoin( + token, UpdateCoinRequestModel( + userId = userId, + action = coinAction.action, + coin = coin + ) + ).collect { coin -> + when (coin) { + is Resources.Error -> { + Timber.e("Too many requests, please wait 1 minute") + } + + is Resources.Loading -> { + // do nothing + } + + is Resources.Success -> { + appManager.saveUserCoins(coin.data?.coins ?: 0) + _uiState.update { + it.copy(coins = coin.data?.coins ?: 0) + } + } + } + } + } + } + + private fun getCustomerInfo() { + Purchases.sharedInstance.getCustomerInfo(object : ReceiveCustomerInfoCallback { + override fun onReceived(customerInfo: CustomerInfo) { + _uiState.update { + it.copy( + customerInfo = customerInfo + ) + } + } + + override fun onError(error: PurchasesError) { + Timber.e(error.message) + } + }) + } } \ No newline at end of file diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/create_study_set_ai/CreateStudySetAITab.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/create_study_set_ai/CreateStudySetAITab.kt index 67227987..8d763109 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/create_study_set_ai/CreateStudySetAITab.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/explore/create_study_set_ai/CreateStudySetAITab.kt @@ -31,7 +31,6 @@ import androidx.compose.material3.TextFieldDefaults.colors import androidx.compose.material3.VerticalDivider import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -51,12 +50,6 @@ import com.pwhs.quickmem.core.data.enums.DifficultyLevel import com.pwhs.quickmem.core.data.enums.LanguageCode import com.pwhs.quickmem.core.data.enums.QuestionType import com.pwhs.quickmem.ui.theme.QuickMemTheme -import com.pwhs.quickmem.util.ads.AdsUtil -import com.revenuecat.purchases.CustomerInfo -import com.revenuecat.purchases.Purchases -import com.revenuecat.purchases.PurchasesError -import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback -import timber.log.Timber @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -82,34 +75,11 @@ fun CreateStudySetAITab( mutableStateOf(false) } val context = LocalContext.current - var customer: CustomerInfo? by remember { mutableStateOf(null) } - LaunchedEffect(key1 = true) { - Purchases.sharedInstance.getCustomerInfo(object : ReceiveCustomerInfoCallback { - override fun onError(error: PurchasesError) { - Timber.e("Error getting customer info: $error") - } - - override fun onReceived(customerInfo: CustomerInfo) { - Timber.d("Customer info: $customerInfo") - customer = customerInfo - } - - }) - } Scaffold( floatingActionButton = { if (title.isNotEmpty()) { FloatingActionButton( - onClick = { - val isSubscribed = customer?.activeSubscriptions?.isNotEmpty() == true - if (isSubscribed) { - onCreateStudySet() - } else { - AdsUtil.rewardedAd(context) { - onCreateStudySet() - } - } - }, + onClick = onCreateStudySet, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/app/profile/ProfileViewModel.kt b/app/src/main/java/com/pwhs/quickmem/presentation/app/profile/ProfileViewModel.kt index 14ac1fdb..0220ee6f 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/app/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/app/profile/ProfileViewModel.kt @@ -119,6 +119,10 @@ class ProfileViewModel @Inject constructor( appManager.saveUserName(data.username) appManager.saveUserAvatar(data.avatarUrl) appManager.saveUserRole(data.role) + appManager.saveUserFullName(data.fullname) + appManager.saveUserEmail(data.email) + appManager.saveUserId(data.id) + appManager.saveUserCoins(data.coin) } } diff --git a/app/src/main/java/com/pwhs/quickmem/presentation/auth/login/email/LoginWithEmailViewModel.kt b/app/src/main/java/com/pwhs/quickmem/presentation/auth/login/email/LoginWithEmailViewModel.kt index 53e086f8..290854bd 100644 --- a/app/src/main/java/com/pwhs/quickmem/presentation/auth/login/email/LoginWithEmailViewModel.kt +++ b/app/src/main/java/com/pwhs/quickmem/presentation/auth/login/email/LoginWithEmailViewModel.kt @@ -140,6 +140,7 @@ class LoginWithEmailViewModel @Inject constructor( appManager.saveUserBirthday(login.data?.birthday ?: "") appManager.saveUserName(login.data?.username ?: "") appManager.saveUserRole(login.data?.role ?: "") + appManager.saveUserCoins(login.data?.coin ?: 0) Purchases.sharedInstance.apply { setEmail(login.data?.email) setDisplayName(login.data?.fullName) diff --git a/app/src/main/java/com/pwhs/quickmem/util/ads/AdsUtil.kt b/app/src/main/java/com/pwhs/quickmem/util/ads/AdsUtil.kt index 5df99f9c..68b5ea87 100644 --- a/app/src/main/java/com/pwhs/quickmem/util/ads/AdsUtil.kt +++ b/app/src/main/java/com/pwhs/quickmem/util/ads/AdsUtil.kt @@ -16,6 +16,7 @@ import com.pwhs.quickmem.MainActivity import com.pwhs.quickmem.core.utils.AppConstant.INTERSTITIAL_ADS_ID import com.pwhs.quickmem.core.utils.AppConstant.REWARDED_INTERSTITIAL_ADS_ID import com.pwhs.quickmem.core.utils.AppConstant.REWARD_ADS_ID +import timber.log.Timber object AdsUtil { fun interstitialAds( @@ -89,7 +90,7 @@ object AdsUtil { ) } - fun rewardedInterstitialTestAd(context: Context, onAdWatched: () -> Unit) { + fun rewardedInterstitialAd(context: Context, onAdWatched: () -> Unit) { // Load an ad RewardedInterstitialAd.load( context, @@ -99,47 +100,17 @@ object AdsUtil { override fun onAdFailedToLoad(error: LoadAdError) { super.onAdFailedToLoad(error) Toast.makeText(context, error.message, Toast.LENGTH_SHORT).show() - - } override fun onAdLoaded(adLoaded: RewardedInterstitialAd) { super.onAdLoaded(adLoaded) - Toast.makeText(context, "Ad Loaded", Toast.LENGTH_SHORT).show() - if (context is MainActivity) { adLoaded.show(context) { // The user earned the reward + Timber.d("User earned the reward") onAdWatched() } } - - adLoaded.fullScreenContentCallback = object : FullScreenContentCallback() { - override fun onAdDismissedFullScreenContent() { - super.onAdDismissedFullScreenContent() - Toast.makeText(context, "Ad Dismissed", Toast.LENGTH_SHORT).show() - } - - override fun onAdShowedFullScreenContent() { - super.onAdShowedFullScreenContent() - Toast.makeText(context, "Ad Showed", Toast.LENGTH_SHORT).show() - } - - override fun onAdClicked() { - super.onAdClicked() - Toast.makeText(context, "Ad Clicked", Toast.LENGTH_SHORT).show() - } - - override fun onAdImpression() { - super.onAdImpression() - Toast.makeText(context, "Ad Impression", Toast.LENGTH_SHORT).show() - } - - override fun onAdFailedToShowFullScreenContent(p0: AdError) { - super.onAdFailedToShowFullScreenContent(p0) - Toast.makeText(context, p0.message, Toast.LENGTH_SHORT).show() - } - } } } diff --git a/app/src/main/res/drawable/ic_coin.xml b/app/src/main/res/drawable/ic_coin.xml new file mode 100644 index 00000000..5d51693f --- /dev/null +++ b/app/src/main/res/drawable/ic_coin.xml @@ -0,0 +1,39 @@ + + + + + + + + + + +