From a5c54dc1db4c8e36b9f40954d3bed8e6380e7d80 Mon Sep 17 00:00:00 2001 From: EFFemole Date: Mon, 1 Jul 2024 23:05:30 +0300 Subject: [PATCH 1/5] feat: updated purchase screen subs, updated network status checker --- .../screens/purchase/PurchaseScreen.kt | 83 +++++++++++++------ .../com/jobik/shkiper/util/ContextUtils.kt | 11 +++ 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt index d93e4e46..c6748700 100644 --- a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt +++ b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.material.icons.outlined.SignalWifiOff import androidx.compose.material.icons.outlined.TaskAlt import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -37,17 +39,26 @@ import com.jobik.shkiper.ui.components.modals.ImageActionDialog import com.jobik.shkiper.ui.components.modals.ImageActionDialogButton import com.jobik.shkiper.ui.helpers.allWindowInsetsPadding import com.jobik.shkiper.ui.theme.AppTheme +import com.jobik.shkiper.util.ContextUtils import kotlin.random.Random @Composable fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { val context = LocalContext.current - val connectivityManager = remember { context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } - val isNetworkActive = - remember { connectivityManager.activeNetworkInfo != null && connectivityManager.activeNetworkInfo!!.isConnected } + val hasInternetConnection by remember { + mutableStateOf( + ContextUtils.hasInternetConnection( + context + ) + ) + } if (purchaseViewModel.screenState.value.showGratitude) { - val stringResources = listOf(R.string.ThankForPurchase1, R.string.ThankForPurchase2, R.string.ThankForPurchase3) + val stringResources = listOf( + R.string.ThankForPurchase1, + R.string.ThankForPurchase2, + R.string.ThankForPurchase3 + ) ImageActionDialog( header = stringResource(stringResources[Random.nextInt(stringResources.size)]), onGoBack = {}, @@ -59,15 +70,25 @@ fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { ) } - if (!isNetworkActive) - ScreenStub(title = R.string.CheckInternetConnection, icon = Icons.Outlined.SignalWifiOff) - else if (purchaseViewModel.screenState.value.purchases.isEmpty() && purchaseViewModel.screenState.value.subscription != null) - ScreenStub(title = R.string.CheckUpdatesGooglePlay, icon = Icons.Default.Shop) - else - ScreenWrapper(modifier = Modifier - .verticalScroll(rememberScrollState()) - .allWindowInsetsPadding() - .padding(top = 85.dp, bottom = 30.dp)) { + if (hasInternetConnection.not()) { + ScreenStub( + modifier = Modifier.background(AppTheme.colors.background), + title = R.string.CheckInternetConnection, + icon = Icons.Outlined.SignalWifiOff + ) + } else if (purchaseViewModel.screenState.value.purchases.isEmpty() && purchaseViewModel.screenState.value.subscription != null) { + ScreenStub( + modifier = Modifier.background(AppTheme.colors.background), + title = R.string.CheckUpdatesGooglePlay, + icon = Icons.Default.Shop + ) + } else + ScreenWrapper( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .allWindowInsetsPadding() + .padding(top = 85.dp, bottom = 30.dp) + ) { Column( modifier = Modifier .fillMaxWidth() @@ -115,7 +136,9 @@ fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { ProductPurchaseCardContent( product = productDetails, imageRes = R.drawable.tea, - isPurchased = purchaseViewModel.checkIsProductPurchased(productDetails.productId) + isPurchased = purchaseViewModel.checkIsProductPurchased( + productDetails.productId + ) ) }, purchaseViewModel.screenState.value.purchases.find { it.productId == AppProducts.SweetsForMyCat } @@ -124,7 +147,9 @@ fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { product = productDetails, imageRes = R.drawable.photo_my_favorite_cat_2, isHighlighted = true, - isPurchased = purchaseViewModel.checkIsProductPurchased(productDetails.productId) + isPurchased = purchaseViewModel.checkIsProductPurchased( + productDetails.productId + ) ) }, purchaseViewModel.screenState.value.purchases.find { it.productId == AppProducts.GymMembership } @@ -132,15 +157,19 @@ fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { ProductPurchaseCardContent( product = productDetails, imageRes = R.drawable.fitness, - isPurchased = purchaseViewModel.checkIsProductPurchased(productDetails.productId) + isPurchased = purchaseViewModel.checkIsProductPurchased( + productDetails.productId + ) ) } ) for (product in productList) if (product != null) - Box(modifier = Modifier - .weight(1f) - .padding(horizontal = 4.dp)) { + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 4.dp) + ) { PurchaseCard(product) { purchaseViewModel.makePurchase(product.product, context as Activity) } @@ -178,9 +207,11 @@ fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { ) { productDetails.subscriptionOfferDetails?.find { it.basePlanId == AppProducts.Monthly } ?.let { subscriptionOffer -> - Box(modifier = Modifier - .weight(1f) - .padding(horizontal = 4.dp)) { + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 4.dp) + ) { PurchaseCard( TitlePurchaseCardContent( titleRes = R.string.Monthly, @@ -198,9 +229,11 @@ fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { } productDetails.subscriptionOfferDetails?.find { it.basePlanId == AppProducts.Yearly } ?.let { subscriptionOffer -> - Box(modifier = Modifier - .weight(1f) - .padding(horizontal = 4.dp)) { + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 4.dp) + ) { PurchaseCard( TitlePurchaseCardContent( titleRes = R.string.Annually, diff --git a/app/src/main/java/com/jobik/shkiper/util/ContextUtils.kt b/app/src/main/java/com/jobik/shkiper/util/ContextUtils.kt index 3f173c26..f9a97c80 100644 --- a/app/src/main/java/com/jobik/shkiper/util/ContextUtils.kt +++ b/app/src/main/java/com/jobik/shkiper/util/ContextUtils.kt @@ -2,6 +2,8 @@ package com.jobik.shkiper.util import android.app.UiModeManager import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.os.Build import com.jobik.shkiper.util.settings.Localization import java.util.Locale @@ -49,4 +51,13 @@ object ContextUtils { configuration.setLayoutDirection(locale) return context.createConfigurationContext(configuration) } + + fun hasInternetConnection(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork = connectivityManager.activeNetwork ?: return false + val networkCapabilities = + connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false + return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } } From 7349310d9ad8b67be384b13f20b4a7e5b852730e Mon Sep 17 00:00:00 2001 From: EFFemole Date: Tue, 2 Jul 2024 15:36:21 +0300 Subject: [PATCH 2/5] feat: updated purchase screen ui --- .../screens/purchase/PurchaseScreen.kt | 489 +++++++++++------- .../screens/purchase/PurchaseViewModel.kt | 10 +- .../screens/settings/SettingsScreen.kt | 2 +- .../shkiper/services/billing/AppProducts.kt | 10 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-en/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-th/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- app/src/main/res/values/strings.xml | 7 +- 19 files changed, 345 insertions(+), 201 deletions(-) diff --git a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt index c6748700..f9862231 100644 --- a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt +++ b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt @@ -1,45 +1,58 @@ package com.jobik.shkiper.screens.purchase import android.app.Activity -import android.content.Context -import android.net.ConnectivityManager +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.basicMarquee +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.Shop import androidx.compose.material.icons.outlined.SignalWifiOff -import androidx.compose.material.icons.outlined.TaskAlt +import androidx.compose.material.icons.rounded.Verified import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.android.billingclient.api.ProductDetails import com.jobik.shkiper.R import com.jobik.shkiper.services.billing.AppProducts -import com.jobik.shkiper.ui.components.cards.ProductPurchaseCardContent -import com.jobik.shkiper.ui.components.cards.PurchaseCard -import com.jobik.shkiper.ui.components.cards.TitlePurchaseCardContent import com.jobik.shkiper.ui.components.layouts.ScreenStub -import com.jobik.shkiper.ui.components.layouts.ScreenWrapper import com.jobik.shkiper.ui.components.modals.ImageActionDialog import com.jobik.shkiper.ui.components.modals.ImageActionDialogButton import com.jobik.shkiper.ui.helpers.allWindowInsetsPadding +import com.jobik.shkiper.ui.modifiers.bounceClick import com.jobik.shkiper.ui.theme.AppTheme import com.jobik.shkiper.util.ContextUtils +import kotlinx.coroutines.delay import kotlin.random.Random @Composable @@ -70,186 +83,310 @@ fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { ) } - if (hasInternetConnection.not()) { - ScreenStub( - modifier = Modifier.background(AppTheme.colors.background), - title = R.string.CheckInternetConnection, - icon = Icons.Outlined.SignalWifiOff - ) - } else if (purchaseViewModel.screenState.value.purchases.isEmpty() && purchaseViewModel.screenState.value.subscription != null) { - ScreenStub( - modifier = Modifier.background(AppTheme.colors.background), - title = R.string.CheckUpdatesGooglePlay, - icon = Icons.Default.Shop - ) - } else - ScreenWrapper( + Column( + modifier = Modifier + .fillMaxSize() + .background(AppTheme.colors.background), + ) { + if (hasInternetConnection.not()) { + ScreenStub( + title = R.string.CheckInternetConnection, + icon = Icons.Outlined.SignalWifiOff + ) + } else { + ScreenContent(purchaseViewModel = purchaseViewModel) + } + } +} + +@Composable +private fun ScreenContent(purchaseViewModel: PurchaseViewModel) { + val context = LocalContext.current + + var highlight by remember { mutableIntStateOf(0) } + val count = 4 + + LaunchedEffect(Unit) { + while (true) { + delay(2000) + highlight = (highlight + 1) % count + } + } + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .allWindowInsetsPadding() + .padding(top = 85.dp, bottom = 40.dp) + .padding(horizontal = 10.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Column( modifier = Modifier - .verticalScroll(rememberScrollState()) - .allWindowInsetsPadding() - .padding(top = 85.dp, bottom = 30.dp) + .fillMaxWidth() + .clip(AppTheme.shapes.large) + .background(AppTheme.colors.secondaryContainer) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(5.dp) ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 15.dp, start = 10.dp, end = 10.dp) - .clip(AppTheme.shapes.large) - .background(AppTheme.colors.container) - .padding(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 15.dp), - ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.support_the_project), + color = AppTheme.colors.onSecondaryContainer, + maxLines = 1, + fontWeight = FontWeight.SemiBold, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.headlineMedium + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.support_project_description), + color = AppTheme.colors.onSecondaryContainer, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Normal + ) + } + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + purchaseViewModel.screenState.value.purchases.forEachIndexed { index, it -> + ProductCard( + modifier = Modifier.weight(1f), + highlight = index == highlight, + isBought = purchaseViewModel.checkIsProductPurchased(it.productId), + product = it + ) { + purchaseViewModel.makePurchase( + productDetails = it, + activity = context as Activity + ) + } + } + } + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + val sub = purchaseViewModel.screenState.value.subscriptions.firstOrNull() + + sub?.subscriptionOfferDetails?.forEachIndexed { index, subDetail -> + subDetail?.let { + SubscriptionCard( + modifier = Modifier.weight(1f), + highlight = index + 2 == highlight, + product = subDetail + ) { + purchaseViewModel.makePurchaseSubscription( + productDetails = sub, + subscriptionOfferDetails = subDetail, + activity = context as Activity + ) + } + } + } + } + } + } +} + +@Composable +private fun ProductCard( + modifier: Modifier = Modifier, + highlight: Boolean = false, + isBought: Boolean = false, + product: ProductDetails, + onClick: (ProductDetails) -> Unit +) { + val type = stringResource(R.string.purchase) + val description = stringResource(R.string.support) + val price = product.oneTimePurchaseOfferDetails?.formattedPrice ?: "" + + val backgroundColor by + animateColorAsState( + targetValue = if (highlight) AppTheme.colors.secondaryContainer else AppTheme.colors.container, + tween(500) + ) + + val titleColor by + animateColorAsState( + targetValue = if (highlight) AppTheme.colors.onSecondaryContainer else AppTheme.colors.text, + tween(500) + ) + + val scale by animateFloatAsState(if (highlight) 1.05f else 1f, tween(500)) + + Column( + modifier = modifier + .bounceClick() + .graphicsLayer { + scaleX = scale + scaleY = scale + } + .clip(AppTheme.shapes.medium) + .background(backgroundColor) + .clickable { onClick(product) } + .padding(15.dp), + ) { + Row { + Column(modifier = Modifier.weight(1f)) { Text( - stringResource(R.string.PurchaseScreenTitle), - color = AppTheme.colors.text, - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold), - textAlign = TextAlign.Center, modifier = Modifier - .padding(bottom = 15.dp, top = 5.dp) - .fillMaxWidth() - ) - Text( - text = stringResource(R.string.PurchaseScreenDescription), + .basicMarquee(), + text = type, color = AppTheme.colors.textSecondary, - fontSize = 16.sp, - lineHeight = 24.sp, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.fillMaxWidth() + fontWeight = FontWeight.Normal, + maxLines = 1 ) + Text( + modifier = Modifier + .height(50.dp) + .basicMarquee(), + text = description, + color = titleColor, + style = MaterialTheme.typography.titleMedium, + maxLines = 1 + ) + } + if (isBought) { + Box( + modifier = Modifier + .clip(CircleShape) + .background(AppTheme.colors.primary) + .padding(4.dp) + ) { + Icon( + imageVector = Icons.Rounded.Verified, + contentDescription = null, + tint = AppTheme.colors.onPrimary + ) + } } + } + Row( + modifier = Modifier + .fillMaxWidth() + .clip(CircleShape) + .background(AppTheme.colors.primary) + .padding(10.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { Text( - stringResource(R.string.BuyMe), - color = AppTheme.colors.textSecondary, + modifier = Modifier.basicMarquee(), + text = price, + color = AppTheme.colors.onPrimary, style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .padding(horizontal = 10.dp) - .padding(bottom = 5.dp) - .fillMaxWidth() + fontWeight = FontWeight.Normal, + maxLines = 1 ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 7.dp), - horizontalArrangement = Arrangement.Center - ) { - val productList = listOf( - purchaseViewModel.screenState.value.purchases.find { it.productId == AppProducts.CupOfTea } - ?.let { productDetails -> - ProductPurchaseCardContent( - product = productDetails, - imageRes = R.drawable.tea, - isPurchased = purchaseViewModel.checkIsProductPurchased( - productDetails.productId - ) - ) - }, - purchaseViewModel.screenState.value.purchases.find { it.productId == AppProducts.SweetsForMyCat } - ?.let { productDetails -> - ProductPurchaseCardContent( - product = productDetails, - imageRes = R.drawable.photo_my_favorite_cat_2, - isHighlighted = true, - isPurchased = purchaseViewModel.checkIsProductPurchased( - productDetails.productId - ) - ) - }, - purchaseViewModel.screenState.value.purchases.find { it.productId == AppProducts.GymMembership } - ?.let { productDetails -> - ProductPurchaseCardContent( - product = productDetails, - imageRes = R.drawable.fitness, - isPurchased = purchaseViewModel.checkIsProductPurchased( - productDetails.productId - ) - ) - } - ) - for (product in productList) - if (product != null) - Box( - modifier = Modifier - .weight(1f) - .padding(horizontal = 4.dp) - ) { - PurchaseCard(product) { - purchaseViewModel.makePurchase(product.product, context as Activity) - } - } + } + } +} + +@Composable +private fun SubscriptionCard( + modifier: Modifier = Modifier, + highlight: Boolean = false, + isBought: Boolean = false, + product: ProductDetails.SubscriptionOfferDetails, + onClick: (ProductDetails.SubscriptionOfferDetails) -> Unit +) { + val type = stringResource(R.string.subscription) + val description = stringResource(R.string.ongoing_support) + val price = product.pricingPhases.pricingPhaseList.first().formattedPrice + val subPeriod = when (product.basePlanId) { + AppProducts.Yearly -> stringResource(R.string.yearly) + AppProducts.Monthly -> stringResource(R.string.monthly) + else -> "" + } + + val backgroundColor by + animateColorAsState( + targetValue = if (highlight) AppTheme.colors.secondaryContainer else AppTheme.colors.container, + tween(500) + ) + + val titleColor by + animateColorAsState( + targetValue = if (highlight) AppTheme.colors.onSecondaryContainer else AppTheme.colors.text, + tween(500) + ) + + val scale by animateFloatAsState(if (highlight) 1.05f else 1f, tween(500)) + + Column( + modifier = modifier + .bounceClick() + .graphicsLayer { + scaleX = scale + scaleY = scale } - Spacer(Modifier.height(4.dp)) - purchaseViewModel.screenState.value.subscription?.let { productDetails -> - Row( - verticalAlignment = Alignment.CenterVertically, + .clip(AppTheme.shapes.medium) + .background(backgroundColor) + .clickable { onClick(product) } + .padding(15.dp), + ) { + Row { + Column(modifier = Modifier.weight(1f)) { + Text( modifier = Modifier - .padding(horizontal = 10.dp) .fillMaxWidth() - ) { - Text( - stringResource(R.string.BuySubscription), - color = AppTheme.colors.textSecondary, - style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .padding(bottom = 5.dp) - .padding(top = 15.dp) - ) - Spacer(Modifier.width(8.dp)) - if (purchaseViewModel.checkIsProductPurchased(productDetails.productId)) - Icon( - imageVector = Icons.Outlined.TaskAlt, - contentDescription = null, - tint = AppTheme.colors.primary, - ) - } - Row( + .basicMarquee(), + text = type, + color = AppTheme.colors.textSecondary, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Normal, + maxLines = 1 + ) + Text( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 7.dp), - horizontalArrangement = Arrangement.Center - ) { - productDetails.subscriptionOfferDetails?.find { it.basePlanId == AppProducts.Monthly } - ?.let { subscriptionOffer -> - Box( - modifier = Modifier - .weight(1f) - .padding(horizontal = 4.dp) - ) { - PurchaseCard( - TitlePurchaseCardContent( - titleRes = R.string.Monthly, - imageRes = R.drawable.one_month, - isPurchased = false - ) - ) { - purchaseViewModel.makePurchaseSubscription( - productDetails, - subscriptionOffer, - context as Activity - ) - } - } - } - productDetails.subscriptionOfferDetails?.find { it.basePlanId == AppProducts.Yearly } - ?.let { subscriptionOffer -> - Box( - modifier = Modifier - .weight(1f) - .padding(horizontal = 4.dp) - ) { - PurchaseCard( - TitlePurchaseCardContent( - titleRes = R.string.Annually, - imageRes = R.drawable.full_year, - isPurchased = false - ) - ) { - purchaseViewModel.makePurchaseSubscription( - productDetails, - subscriptionOffer, - context as Activity - ) - } - } - } - } + .basicMarquee(), + text = description, + color = titleColor, + style = MaterialTheme.typography.titleMedium, + maxLines = 1 + ) + Text( + modifier = Modifier + .height(50.dp) + .fillMaxWidth() + .basicMarquee(), + text = subPeriod, + color = titleColor, + style = MaterialTheme.typography.titleMedium, + maxLines = 1 + ) } +// if (isBought) { +// Box( +// modifier = Modifier +// .clip(CircleShape) +// .background(AppTheme.colors.primary) +// .padding(4.dp) +// ) { +// Icon( +// imageVector = Icons.Rounded.Verified, +// contentDescription = null, +// tint = AppTheme.colors.onPrimary +// ) +// } +// } } + Row( + modifier = Modifier + .fillMaxWidth() + .clip(CircleShape) + .background(AppTheme.colors.primary) + .padding(10.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.basicMarquee(), + text = price, + color = AppTheme.colors.onPrimary, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Normal, + maxLines = 1 + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseViewModel.kt b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseViewModel.kt index 5de12876..08b432c8 100644 --- a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseViewModel.kt +++ b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseViewModel.kt @@ -23,7 +23,7 @@ import javax.inject.Inject data class PurchaseScreenState( val purchases: List = emptyList(), - val subscription: ProductDetails? = null, + val subscriptions: List = emptyList(), val showGratitude: Boolean = false, ) @@ -38,7 +38,7 @@ class PurchaseViewModel @Inject constructor( init { _screenState.value = _screenState.value.copy( purchases = billingClient.productDetails.value, - subscription = if (billingClient.subscriptionsDetails.value.isNotEmpty()) billingClient.subscriptionsDetails.value.first() else null, + subscriptions = billingClient.subscriptionsDetails.value, ) billingClient.registerPurchaseCallback(this) } @@ -91,7 +91,11 @@ class PurchaseViewModel @Inject constructor( subscriptionOfferDetails: SubscriptionOfferDetails, activity: Activity ) { - val billingResult = billingClient.makePurchaseSubscription(activity, productDetails, subscriptionOfferDetails) + val billingResult = billingClient.makePurchaseSubscription( + activity, + productDetails, + subscriptionOfferDetails + ) } fun checkIsProductPurchased(productId: String): Boolean { diff --git a/app/src/main/java/com/jobik/shkiper/screens/settings/SettingsScreen.kt b/app/src/main/java/com/jobik/shkiper/screens/settings/SettingsScreen.kt index 08ba396e..578fdb6a 100644 --- a/app/src/main/java/com/jobik/shkiper/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/jobik/shkiper/screens/settings/SettingsScreen.kt @@ -180,7 +180,7 @@ private fun DevSupportSettings( settingsViewModel: SettingsViewModel, navController: NavController ) { - SettingsGroup(header = stringResource(R.string.Support), accent = true) { + SettingsGroup(header = stringResource(R.string.support), accent = true) { SettingsItem( modifier = Modifier.heightIn(min = 50.dp), icon = Icons.Outlined.Stars, diff --git a/app/src/main/java/com/jobik/shkiper/services/billing/AppProducts.kt b/app/src/main/java/com/jobik/shkiper/services/billing/AppProducts.kt index 2542e2b9..795b0ec7 100644 --- a/app/src/main/java/com/jobik/shkiper/services/billing/AppProducts.kt +++ b/app/src/main/java/com/jobik/shkiper/services/billing/AppProducts.kt @@ -5,9 +5,8 @@ import androidx.annotation.Keep @Keep object AppProducts { //Product IDs - const val CupOfTea = "cup_of_tea" - const val SweetsForMyCat = "sweets_for_my_cat" - const val GymMembership = "gym_membership" + const val Product_1 = "product_1" + const val Product_2 = "product_2" //Subscriptions IDs const val SimpleSubscription = "simple_subscription" @@ -15,9 +14,8 @@ object AppProducts { const val Yearly = "simple-yearly" val ListOfProducts = listOf( - CupOfTea, - SweetsForMyCat, - GymMembership + Product_1, + Product_2, ) val ListOfSubscriptions = listOf( SimpleSubscription, diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6147e908..55d50b57 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -267,7 +267,7 @@ Über Shkiper - Unterstützung + Unterstützung Erstellen, organisieren, erinnern. Ihr perfekter Begleiter für Aufgaben, Notizen und Erinnerungen – schlicht, schön und immer griffbereit. diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 42cff406..90fa34e8 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -140,7 +140,7 @@ Did you like the Shkiper? Support the developer with a coin! About Shkiper - Support + Support Create, organize, remember. Your perfect companion for tasks, notes, and reminders – simple, beautiful, and always at hand. Contact Choose an Email client: diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7fa42d6c..316e44f5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -267,7 +267,7 @@ Acerca de Shkiper - Apoyo + Apoyo Crea, organiza, recuerda. Tu compañero perfecto para tareas, notas y recordatorios: sencillo, bonito y siempre a mano. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6a440f78..db374147 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -267,7 +267,7 @@ À propos de Shkiper - Soutien + Soutien Créez, organisez, mémorisez. Votre compagnon idéal pour les tâches, les notes et les rappels – simple, beau et toujours à portée de main. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 8b30c99a..3f1d1b3a 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -267,7 +267,7 @@ Tentang Shkiper - Mendukung + Mendukung Buat, atur, ingat. Teman sempurna Anda untuk tugas, catatan, dan pengingat – sederhana, cantik, dan selalu siap sedia. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index cc3df58f..c5e998c1 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -267,7 +267,7 @@ A proposito di Shkiper - Supporto + Supporto Crea, organizza, ricorda. Il tuo compagno perfetto per attività, appunti e promemoria: semplice, bello e sempre a portata di mano. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d3ad434a..4b2cb4cc 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -267,7 +267,7 @@ シュキパーについて - サポート + サポート 作成、整理、記憶。タスク、メモ、リマインダーの完璧なパートナー – シンプルで美しく、いつでも手元にあります。 diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 18bad141..a907fa2b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -267,7 +267,7 @@ Sobre Shkiper - Apoiar + Apoiar Crie, organize, lembre-se. Seu companheiro perfeito para tarefas, anotações e lembretes – simples, bonito e sempre à mão. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c09500cb..9cbf9d9f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -144,7 +144,7 @@ Нравится Shkiper? Поддержите разработчика монеткой! Про Shkiper - Поддержать + Поддержать Создавайте, организовывайте и запоминайте. Ваш идеальный спутник для дел, заметок и напоминаний – простой, красивый и всегда под рукой. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index d8bbb1af..36ffd5cf 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -267,7 +267,7 @@ เกี่ยวกับ ชคิเปอร์ - สนับสนุน + สนับสนุน สร้าง จัดระเบียบ จดจำ เพื่อนที่สมบูรณ์แบบของคุณสำหรับงาน บันทึกย่อ และการเตือนความจำ เรียบง่าย สวยงาม และพร้อมเสมอ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 591bb4ce..654e1de7 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -170,7 +170,7 @@ Shkiper\'ı beğendiniz mi? Geliştiriciye bir kuruşla destek olun! Shkiper hakkında - Destek + Destek Yarat, organize et, hatırla. Görevler, notlar ve hatırlatmalar için mükemmel arkadaşınız – basit, güzel ve her zaman elinizin altında. İletişim Bir e-posta istemcisi seçin: diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 64f4fa45..c799bf4f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -141,7 +141,7 @@ Подобається Shkiper? Підтримайте розробника монетою! Про Shkiper - Підтримати + Підтримати Створюйте, упорядковуйте, запам’ятовуйте. Ваш ідеальний компаньйон для завдань, нотаток і нагадувань – простий, красивий і завжди під рукою. Зв\'язатися Виберіть поштовий клієнт: diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 6f3e6501..6eb68868 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -174,7 +174,7 @@ Bạn có thích Shkiper không? Hãy ủng hộ nhà phát triển bằng một coin! Về Shkiper - Hỗ trợ + Hỗ trợ Tạo, tổ chức, ghi nhớ. Người bạn đồng hành hoàn hảo của bạn cho các công việc, ghi chú và lời nhắc – đơn giản, đẹp mắt và luôn trong tầm tay. Liên hệ Chọn ứng dụng Email: diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index bb6c0131..14e84dec 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -271,7 +271,7 @@ 关于施基珀 - 支持 + 支持 创造、组织、记忆。您的任务、笔记和提醒的完美伴侣 - 简单、美观且随时可用。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0355c6a3..0d5321f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -174,7 +174,7 @@ Did you like the Shkiper? Support the developer with a coin! About Shkiper - Support + Support Create, organize, remember. Your perfect companion for tasks, notes, and reminders – simple, beautiful, and always at hand. Contact Choose an Email client: @@ -280,6 +280,11 @@ My apps Send bug report and feature request here Issue tracker + Support the project + Thank you for your interest in the project! Support plays a key role in its development and allows it to continue working on new ideas and improvements + Purchase + Subscription + Ongoing support \ No newline at end of file From 1ab661198aee00c8971bdef3309a684e12c9bd03 Mon Sep 17 00:00:00 2001 From: EFFemole Date: Tue, 2 Jul 2024 20:22:07 +0300 Subject: [PATCH 3/5] feat: updated purchase screen ui --- .../screens/purchase/PurchaseScreen.kt | 208 +++++++++++++++--- .../ui/components/modals/ImageActionDialog.kt | 124 ----------- app/src/main/res/values/strings.xml | 1 + 3 files changed, 175 insertions(+), 158 deletions(-) delete mode 100644 app/src/main/java/com/jobik/shkiper/ui/components/modals/ImageActionDialog.kt diff --git a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt index f9862231..6bb544f0 100644 --- a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt +++ b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt @@ -1,12 +1,27 @@ package com.jobik.shkiper.screens.purchase import android.app.Activity +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -15,13 +30,18 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.outlined.SignalWifiOff +import androidx.compose.material.icons.outlined.VolunteerActivism import androidx.compose.material.icons.rounded.Verified +import androidx.compose.material.icons.rounded.VolunteerActivism +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -31,10 +51,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf 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.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -46,13 +68,20 @@ import com.android.billingclient.api.ProductDetails import com.jobik.shkiper.R import com.jobik.shkiper.services.billing.AppProducts import com.jobik.shkiper.ui.components.layouts.ScreenStub -import com.jobik.shkiper.ui.components.modals.ImageActionDialog -import com.jobik.shkiper.ui.components.modals.ImageActionDialogButton +import com.jobik.shkiper.ui.components.modals.FullscreenPopup import com.jobik.shkiper.ui.helpers.allWindowInsetsPadding import com.jobik.shkiper.ui.modifiers.bounceClick import com.jobik.shkiper.ui.theme.AppTheme import com.jobik.shkiper.util.ContextUtils import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import nl.dionsegijn.konfetti.compose.KonfettiView +import nl.dionsegijn.konfetti.core.Angle +import nl.dionsegijn.konfetti.core.Party +import nl.dionsegijn.konfetti.core.Position +import nl.dionsegijn.konfetti.core.Spread +import nl.dionsegijn.konfetti.core.emitter.Emitter +import java.util.concurrent.TimeUnit import kotlin.random.Random @Composable @@ -66,23 +95,7 @@ fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { ) } - if (purchaseViewModel.screenState.value.showGratitude) { - val stringResources = listOf( - R.string.ThankForPurchase1, - R.string.ThankForPurchase2, - R.string.ThankForPurchase3 - ) - ImageActionDialog( - header = stringResource(stringResources[Random.nextInt(stringResources.size)]), - onGoBack = {}, - confirmButton = ImageActionDialogButton( - icon = Icons.Default.Favorite, - isActive = true, - onClick = purchaseViewModel::hideCompletedPurchase - ) - ) - } - + Congratulations(purchaseViewModel) Column( modifier = Modifier .fillMaxSize() @@ -99,6 +112,147 @@ fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { } } +@Composable +private fun Congratulations(purchaseViewModel: PurchaseViewModel) { + val titleRes by rememberSaveable { + mutableIntStateOf( + listOf( + R.string.ThankForPurchase1, + R.string.ThankForPurchase2, + R.string.ThankForPurchase3 + ).random() + ) + } + + fun parade(): List { + val party = Party( + speed = 10f, + maxSpeed = 30f, + damping = 0.9f, + angle = Angle.RIGHT - 45, + spread = Spread.SMALL, + colors = listOf(0xfce18a, 0xff726d, 0xf4306d, 0xb48def), + emitter = Emitter(duration = 5, TimeUnit.SECONDS).perSecond(30), + position = Position.Relative(0.0, 0.35) + ) + + return listOf( + party, + party.copy( + angle = party.angle - 90, // flip angle from right to left + position = Position.Relative(1.0, 0.35) + ), + ) + } + + val animationState = remember { MutableTransitionState(false) } + val visible = remember { mutableStateOf(false) } + + LaunchedEffect(purchaseViewModel.screenState.value.showGratitude) { + if (visible.value.not()) { + visible.value = true + } + animationState.targetState = purchaseViewModel.screenState.value.showGratitude + } + + LaunchedEffect(animationState.isIdle) { + if (animationState.isIdle) { + visible.value = purchaseViewModel.screenState.value.showGratitude + } + } + + if (visible.value) { + FullscreenPopup( + onDismiss = { }, + ) { + AnimatedVisibility( + visibleState = animationState, + enter = fadeIn() + scaleIn(initialScale = .95f), + exit = fadeOut() + scaleOut(targetScale = .95f) + ) { + BackHandler(true) {} + var buttonHideShow by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + delay(1500) + buttonHideShow = true + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.3f)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = {} + ), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .padding(20.dp) + .widthIn(max = 450.dp) + .fillMaxWidth() + .clip(AppTheme.shapes.large) + .background(AppTheme.colors.secondaryContainer) + .padding(20.dp) + ) { + Row( + modifier = Modifier.padding(bottom = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Box(Modifier.padding(horizontal = 10.dp)) { + Icon( + modifier = Modifier.size(70.dp), + imageVector = Icons.Outlined.VolunteerActivism, + contentDescription = null, + tint = AppTheme.colors.primary + ) + } + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(titleRes), + color = AppTheme.colors.onSecondaryContainer, + fontWeight = FontWeight.SemiBold, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge + ) + } + androidx.compose.animation.AnimatedVisibility( + visible = buttonHideShow, + enter = slideInVertically { it } + expandVertically { -it }, + exit = slideOutVertically { it } + shrinkVertically { -it }) + { + Button(modifier = Modifier + .fillMaxWidth() + .height(50.dp), + colors = ButtonDefaults.buttonColors( + contentColor = AppTheme.colors.onPrimary, + containerColor = AppTheme.colors.primary + ), + onClick = { purchaseViewModel.hideCompletedPurchase() }) { + Text( + modifier = Modifier.basicMarquee(), + text = stringResource(R.string.thank_you), + maxLines = 1, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + } + } + KonfettiView( + modifier = Modifier.fillMaxSize(), + parties = parade(), + ) + } + } + } + } +} + @Composable private fun ScreenContent(purchaseViewModel: PurchaseViewModel) { val context = LocalContext.current @@ -355,20 +509,6 @@ private fun SubscriptionCard( maxLines = 1 ) } -// if (isBought) { -// Box( -// modifier = Modifier -// .clip(CircleShape) -// .background(AppTheme.colors.primary) -// .padding(4.dp) -// ) { -// Icon( -// imageVector = Icons.Rounded.Verified, -// contentDescription = null, -// tint = AppTheme.colors.onPrimary -// ) -// } -// } } Row( modifier = Modifier diff --git a/app/src/main/java/com/jobik/shkiper/ui/components/modals/ImageActionDialog.kt b/app/src/main/java/com/jobik/shkiper/ui/components/modals/ImageActionDialog.kt deleted file mode 100644 index 36256a57..00000000 --- a/app/src/main/java/com/jobik/shkiper/ui/components/modals/ImageActionDialog.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.jobik.shkiper.ui.components.modals - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.jobik.shkiper.R -import com.jobik.shkiper.ui.components.buttons.ButtonStyle -import com.jobik.shkiper.ui.components.buttons.CustomButton -import com.jobik.shkiper.ui.theme.AppTheme - -data class ImageActionDialogButton( - val text: String? = null, - val icon: ImageVector? = null, - val isActive: Boolean = false, - val onClick: () -> Unit, -) - -@Composable -fun ImageActionDialog( - @DrawableRes - image: Int? = null, - header: String? = null, - text: String? = null, - onGoBack: () -> Unit, - confirmButton: ImageActionDialogButton, - backButton: ImageActionDialogButton? = null -) { - Dialog(onGoBack, DialogProperties(true, dismissOnClickOutside = true)) { - Column( - Modifier.clip(RoundedCornerShape(15.dp)).background(AppTheme.colors.container) - ) { - image?.let { image -> - Row( - modifier = Modifier.fillMaxWidth().height(210.dp) - .padding(top = 20.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Image( - modifier = Modifier.fillMaxSize(), - painter = painterResource(id = image), - contentDescription = stringResource(R.string.Image), - contentScale = ContentScale.Fit - ) - } - } - Column( - modifier = Modifier.padding(top = 20.dp).padding(horizontal = 20.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - header?.let { header -> - Text( - text = header, - color = AppTheme.colors.text, - style = MaterialTheme.typography.h6, - modifier = Modifier.padding(bottom = 4.dp) - ) - } - text?.let { text -> - Text( - text = text, - color = AppTheme.colors.textSecondary, - style = MaterialTheme.typography.body1, - ) - } - Spacer(modifier = Modifier.height(10.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - backButton?.let { button -> - Row( - modifier = Modifier.weight(1f), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - CustomButton( - text = button.text, - onClick = button.onClick, - icon = button.icon, - style = if (button.isActive) ButtonStyle.Filled else ButtonStyle.Text, - modifier = Modifier.fillMaxWidth() - ) - } - Spacer(Modifier.width(7.dp)) - } - confirmButton.let { button -> - Row( - modifier = Modifier.weight(1f), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - CustomButton( - text = button.text, - onClick = button.onClick, - icon = button.icon, - style = if (button.isActive) ButtonStyle.Filled else ButtonStyle.Text, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - } - Spacer(modifier = Modifier.height(10.dp)) - } - } -} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d5321f7..517b6646 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -285,6 +285,7 @@ Purchase Subscription Ongoing support + Thank you 💜 \ No newline at end of file From 39e537d9d4036779e6304a11c57dc6d007eced05 Mon Sep 17 00:00:00 2001 From: EFFemole Date: Wed, 3 Jul 2024 12:00:05 +0300 Subject: [PATCH 4/5] feat: updated purchase screen ui --- .../screens/purchase/PurchaseScreen.kt | 161 +++++++++++++++++- app/src/main/res/drawable/ic_btc.xml | 14 ++ app/src/main/res/drawable/ic_eth.xml | 38 +++++ app/src/main/res/drawable/ic_usdt.xml | 14 ++ app/src/main/res/values/non_translatable.xml | 9 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/ic_btc.xml create mode 100644 app/src/main/res/drawable/ic_eth.xml create mode 100644 app/src/main/res/drawable/ic_usdt.xml diff --git a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt index 6bb544f0..f317ce85 100644 --- a/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt +++ b/app/src/main/java/com/jobik/shkiper/screens/purchase/PurchaseScreen.kt @@ -1,14 +1,13 @@ package com.jobik.shkiper.screens.purchase import android.app.Activity +import android.content.Context import androidx.activity.compose.BackHandler +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn @@ -18,6 +17,7 @@ import androidx.compose.animation.scaleOut import androidx.compose.animation.shrinkVertically import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable @@ -36,10 +36,10 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CopyAll import androidx.compose.material.icons.outlined.SignalWifiOff import androidx.compose.material.icons.outlined.VolunteerActivism import androidx.compose.material.icons.rounded.Verified -import androidx.compose.material.icons.rounded.VolunteerActivism import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon @@ -51,6 +51,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -58,8 +59,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -73,6 +80,9 @@ import com.jobik.shkiper.ui.helpers.allWindowInsetsPadding import com.jobik.shkiper.ui.modifiers.bounceClick import com.jobik.shkiper.ui.theme.AppTheme import com.jobik.shkiper.util.ContextUtils +import com.jobik.shkiper.util.SnackbarHostUtil +import com.jobik.shkiper.util.SnackbarVisualsCustom +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import nl.dionsegijn.konfetti.compose.KonfettiView @@ -82,7 +92,6 @@ import nl.dionsegijn.konfetti.core.Position import nl.dionsegijn.konfetti.core.Spread import nl.dionsegijn.konfetti.core.emitter.Emitter import java.util.concurrent.TimeUnit -import kotlin.random.Random @Composable fun PurchaseScreen(purchaseViewModel: PurchaseViewModel = hiltViewModel()) { @@ -258,7 +267,7 @@ private fun ScreenContent(purchaseViewModel: PurchaseViewModel) { val context = LocalContext.current var highlight by remember { mutableIntStateOf(0) } - val count = 4 + val count = 7 LaunchedEffect(Unit) { while (true) { @@ -336,6 +345,146 @@ private fun ScreenContent(purchaseViewModel: PurchaseViewModel) { } } } + + val clipboardManager = LocalClipboardManager.current + val scope = rememberCoroutineScope() + + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + CryptoWalletCard( + isHighlight = highlight == 4, + image = R.drawable.ic_btc, + walletName = stringResource(id = R.string.btc), + walletAddress = stringResource(id = R.string.wallet_address_btc) + ) { + copyToClipboard( + clipboardManager = clipboardManager, + context = context, + scope = scope, + walletAddress = context.getString(R.string.wallet_address_btc), + walletName = context.getString(R.string.btc) + ) + } + CryptoWalletCard( + isHighlight = highlight == 5, + image = R.drawable.ic_eth, + walletName = stringResource(id = R.string.eth), + walletAddress = stringResource(id = R.string.wallet_address_eth) + ) { + copyToClipboard( + clipboardManager = clipboardManager, + context = context, + scope = scope, + walletAddress = context.getString(R.string.wallet_address_eth), + walletName = context.getString(R.string.eth) + ) + } + CryptoWalletCard( + isHighlight = highlight == 6, + image = R.drawable.ic_usdt, + walletName = stringResource(id = R.string.usdt), + walletAddress = stringResource(id = R.string.wallet_address_usdt) + ) { + copyToClipboard( + clipboardManager = clipboardManager, + context = context, + scope = scope, + walletAddress = context.getString(R.string.wallet_address_usdt), + walletName = context.getString(R.string.usdt) + ) + } + } + } +} + +private fun copyToClipboard( + clipboardManager: ClipboardManager, + context: Context, + scope: CoroutineScope, + walletName: String, + walletAddress: String, + icon: ImageVector = Icons.Outlined.CopyAll +) { + clipboardManager.setText(AnnotatedString(walletAddress)) + scope.launch { + SnackbarHostUtil.snackbarHostState.showSnackbar( + SnackbarVisualsCustom( + message = context.getString(R.string.copied_to_clipboard) + " " + walletName, + icon = icon + ) + ) + } +} + +@Composable +private fun CryptoWalletCard( + isHighlight: Boolean = false, + @DrawableRes image: Int, + walletName: String, + walletAddress: String, + onClick: () -> Unit +) { + val backgroundColor by + animateColorAsState( + targetValue = if (isHighlight) AppTheme.colors.secondaryContainer else AppTheme.colors.container, + tween(500) + ) + + val titleColor by + animateColorAsState( + targetValue = if (isHighlight) AppTheme.colors.onSecondaryContainer else AppTheme.colors.text, + tween(500) + ) + + val descriptionColor by + animateColorAsState( + targetValue = if (isHighlight) AppTheme.colors.textSecondary else AppTheme.colors.textSecondary, + tween(500) + ) + + val scale by animateFloatAsState(if (isHighlight) 1.05f else 1f, tween(500)) + + Row( + modifier = Modifier + .graphicsLayer { + scaleX = scale + scaleY = scale + } + .bounceClick() + .clip(AppTheme.shapes.large) + .background(backgroundColor) + .clickable { onClick() } + .padding(vertical = 10.dp, horizontal = 15.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + Image( + modifier = Modifier.size(50.dp), + painter = painterResource(id = image), + contentDescription = null, + contentScale = ContentScale.FillHeight, + ) + Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { + Text( + modifier = Modifier.fillMaxWidth(), + text = walletName, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = titleColor, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = walletAddress, + style = MaterialTheme.typography.bodyMedium, + color = descriptionColor, + overflow = TextOverflow.Ellipsis, + ) + } + Icon( + imageVector = Icons.Outlined.CopyAll, + contentDescription = null, + tint = AppTheme.colors.onPrimary + ) } } diff --git a/app/src/main/res/drawable/ic_btc.xml b/app/src/main/res/drawable/ic_btc.xml new file mode 100644 index 00000000..d181a012 --- /dev/null +++ b/app/src/main/res/drawable/ic_btc.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_eth.xml b/app/src/main/res/drawable/ic_eth.xml new file mode 100644 index 00000000..f4fd7c97 --- /dev/null +++ b/app/src/main/res/drawable/ic_eth.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_usdt.xml b/app/src/main/res/drawable/ic_usdt.xml new file mode 100644 index 00000000..172cef89 --- /dev/null +++ b/app/src/main/res/drawable/ic_usdt.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/values/non_translatable.xml b/app/src/main/res/values/non_translatable.xml index 1faecbc8..3d391d65 100644 --- a/app/src/main/res/values/non_translatable.xml +++ b/app/src/main/res/values/non_translatable.xml @@ -11,6 +11,15 @@ Game of Life https://play.google.com/store/apps/details?id=com.jobik.gameoflife + + BTC + ETH + USDT + + 1BwYb44bZK3KcUF6aNPv5gVUFVYNgYMtat + 0x3ac216afc303c9928f4ef1d610a37994f9f2b8c2 + TF9XMK8Vkx5VVTuANWoaMxV4nB4s6NWerd + Bahasa Indonesia Deutsch diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 517b6646..3c4e4902 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -286,6 +286,7 @@ Subscription Ongoing support Thank you 💜 + Copied to clipboard \ No newline at end of file From 994a4de6b5a0c118b7479c2f676e6b587ff38873 Mon Sep 17 00:00:00 2001 From: EFFemole Date: Wed, 3 Jul 2024 12:02:12 +0300 Subject: [PATCH 5/5] feat: gradle updated --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6c1f34f9..8cdae175 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -versionName = "1.8.6-beta01" -versionCode = "78" +versionName = "1.8.6-rc01" +versionCode = "79" accompanistSystemuicontroller = "0.32.0" activityCompose = "1.9.0"