1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2274 Payment | Design | Tier and code screens (#998)

This commit is contained in:
Konstantin Ivanov 2024-03-13 16:11:58 +01:00 committed by GitHub
parent 8ea6f0d3db
commit 7d65104c34
Signed by: github
GPG key ID: B5690EEEBB952194
16 changed files with 787 additions and 102 deletions

View file

@ -211,6 +211,7 @@ dependencies {
implementation libs.composeAccompanistThemeAdapter
implementation libs.composeAccompanistPagerIndicators
implementation libs.composeAccompanistPermissions
implementation libs.composeAccompanistNavigation
implementation libs.preference
implementation libs.activityCompose
implementation libs.composeReorderable

View file

@ -5,40 +5,123 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.platform.ComposeView
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.anytypeio.anytype.core_ui.common.ComposeDialogView
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.screens.CodeScreen
import com.anytypeio.anytype.screens.MainPaymentsScreen
import com.anytypeio.anytype.screens.TierScreen
import com.anytypeio.anytype.ui.settings.typography
import com.anytypeio.anytype.viewmodel.PaymentsNavigation
import com.anytypeio.anytype.viewmodel.PaymentsViewModel
import com.anytypeio.anytype.viewmodel.PaymentsViewModelFactory
import com.google.accompanist.navigation.material.BottomSheetNavigator
import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
import com.google.accompanist.navigation.material.ModalBottomSheetLayout
import com.google.accompanist.navigation.material.bottomSheet
import com.google.accompanist.navigation.material.rememberBottomSheetNavigator
import javax.inject.Inject
class PaymentsFragment: BaseBottomSheetComposeFragment() {
class PaymentsFragment : BaseBottomSheetComposeFragment() {
@Inject
lateinit var factory: PaymentsViewModelFactory
private val vm by viewModels<PaymentsViewModel> { factory }
private lateinit var navController: NavHostController
@OptIn(ExperimentalMaterialNavigationApi::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
return ComposeDialogView(context = requireContext(), dialog = requireDialog()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(typography = typography) {
MainPaymentsScreen(vm.viewState.collectAsStateWithLifecycle().value)
val bottomSheetNavigator = rememberBottomSheetNavigator()
navController = rememberNavController(bottomSheetNavigator)
SetupNavigation(bottomSheetNavigator, navController)
}
}
}
}
override fun onStart() {
super.onStart()
jobs += subscribe(vm.command) { command ->
when (command) {
PaymentsNavigation.Tier -> navController.navigate(PaymentsNavigation.Tier.route)
PaymentsNavigation.Code -> navController.navigate(PaymentsNavigation.Code.route)
PaymentsNavigation.Dismiss -> navController.popBackStack()
else -> {}
}
}
}
@OptIn(ExperimentalMaterialNavigationApi::class)
@Composable
private fun SetupNavigation(
bottomSheetNavigator: BottomSheetNavigator,
navController: NavHostController
) {
ModalBottomSheetLayout(bottomSheetNavigator = bottomSheetNavigator) {
NavigationGraph(navController = navController)
}
}
@OptIn(ExperimentalMaterialNavigationApi::class)
@Composable
private fun NavigationGraph(navController: NavHostController) {
NavHost(navController = navController, startDestination = PaymentsNavigation.Main.route) {
composable(PaymentsNavigation.Main.route) {
MainPaymentsScreen()
}
bottomSheet(PaymentsNavigation.Tier.route) {
TierScreen()
}
bottomSheet(PaymentsNavigation.Code.route) {
CodeScreen()
}
}
}
@Composable
private fun MainPaymentsScreen() {
MainPaymentsScreen(
state = vm.viewState.collectAsStateWithLifecycle().value,
tierClicked = vm::onTierClicked
)
}
@Composable
private fun TierScreen() {
TierScreen(
tier = vm.selectedTier.collectAsStateWithLifecycle().value,
onDismiss = vm::onDismissTier,
actionPay = vm::onPayButtonClicked
)
}
@Composable
private fun CodeScreen() {
CodeScreen(
state = vm.codeViewState.collectAsStateWithLifecycle().value,
actionResend = { },
actionCode = vm::onActionCode,
onDismiss = vm::onDismissCode
)
}
override fun injectDependencies() {
componentManager().paymentsComponent.get().inject(this)
}

View file

@ -8,6 +8,6 @@
android:fillColor="@color/glyph_selected"/>
<path
android:pathData="M4,14C10,13 13,10 14,4V14V14V24C13,18 10,15 4,14H4L4,14L4,14H4ZM14,4C15,10 18,13 24,14H24C24,14 24,14 24,14C24,14 24,14 24,14H24C18,15 15,18 14,24V14V14V4Z"
android:fillColor="#ffffff"
android:fillColor="@color/text_button_label"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M0,6.4C0,4.16 0,3.04 0.436,2.184C0.819,1.431 1.431,0.819 2.184,0.436C3.04,0 4.16,0 6.4,0H9.6C11.84,0 12.96,0 13.816,0.436C14.569,0.819 15.181,1.431 15.564,2.184C16,3.04 16,4.16 16,6.4V9.6C16,11.84 16,12.96 15.564,13.816C15.181,14.569 14.569,15.181 13.816,15.564C12.96,16 11.84,16 9.6,16H6.4C4.16,16 3.04,16 2.184,15.564C1.431,15.181 0.819,14.569 0.436,13.816C0,12.96 0,11.84 0,9.6V6.4Z"
android:fillColor="@color/text_primary"/>
<path
android:strokeWidth="1"
android:pathData="M4,7.5L7.531,11L12,4"
android:fillColor="#00000000"
android:strokeColor="@color/text_button_label"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:strokeWidth="1"
android:pathData="M0.5,4C0.5,2.067 2.067,0.5 4,0.5H12C13.933,0.5 15.5,2.067 15.5,4V12C15.5,13.933 13.933,15.5 12,15.5H4C2.067,15.5 0.5,13.933 0.5,12V4Z"
android:fillColor="#00000000"
android:strokeColor="@color/shape_primary"/>
</vector>

View file

@ -14,7 +14,7 @@ dokkaVersion = '1.8.20'
activityComposeVersion = '1.8.1'
composeReorderableVersion = '0.9.6'
accompanistVersion = "0.30.0"
accompanistVersion = "0.34.0"
appcompatVersion = '1.6.1'
androidXAnnotationVersion = '1.7.0'
fragmentVersion = "1.6.2"
@ -84,6 +84,7 @@ composeAccompanistPager = { module = "com.google.accompanist:accompanist-pager",
composeAccompanistPermissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistVersion" }
composeAccompanistPagerIndicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanistVersion" }
composeAccompanistThemeAdapter = { module = "com.google.accompanist:accompanist-themeadapter-material", version.ref = "accompanistVersion" }
composeAccompanistNavigation = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanistVersion" }
composeReorderable = { module = "org.burnoutcrew.composereorderable:reorderable", version.ref = "composeReorderableVersion" }
composeConstraintLayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "composeConstraintLayoutVersion" }
kotlinxSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.4.1" }

View file

@ -1378,9 +1378,10 @@
<!-- Membership Level Details -->
<string name="payments_details_name_title">Pick your unique name</string>
<string name="payments_details_name_subtitle">This name acts like a personal domain, making it easier for others to find you</string>
<string name="payments_details_name_subtitle">This is your unique name on the Anytype network, confirming your Membership. It acts as your personal domain and cannot be changed.</string>
<string name="payments_details_name_hint">Myself</string>
<string name="payments_details_name_domain">.any</string>
<string name="payments_details_name_min">Min 7 characters</string>
<string name="payments_details_name_error">This name is already taken!</string>
<string name="payments_details_name_success">This name is up for grabs!</string>
@ -1411,5 +1412,15 @@
<string name="payments_detials_button_pay">Pay by Card</string>
<string name="payments_detials_button_submit">Submit</string>
<string name="payments_details_whats_included">Whats included</string>
<string name="payments_email_title">Get your free membership</string>
<string name="payments_email_subtitle">We need your email to keep spam at bay and the fun in play!</string>
<string name="payments_email_checkbox_text">I\'d like to get updates on products and enjoy free perks!</string>
<string name="payments_email_hint">E-mail</string>
<!-- Payments Code -->
<string name="payments_code_title">Enter the code sent to your email</string>
<string name="payments_code_resend">Resend</string>
<string name="payments_code_resend_in">Resend in %1$n sec</string>
</resources>

View file

@ -43,8 +43,8 @@ dependencies {
implementation libs.appcompat
implementation libs.compose
implementation libs.composeFoundation
implementation libs.composeMaterial
implementation libs.composeToolingPreview
implementation libs.composeMaterial3
implementation libs.coilCompose

View file

@ -7,19 +7,30 @@ sealed class Tier {
data class Explorer(
override val id: String,
override val isCurrent: Boolean,
val price: String = ""
val price: String = "",
val email: String = "",
val isChecked: Boolean = true
) : Tier()
data class Builder(
override val id: String,
override val isCurrent: Boolean,
val price: String = ""
val price: String = "",
val interval: String = "",
val name: String = "",
val nameIsTaken: Boolean = false,
val nameIsFree: Boolean = false
) : Tier()
data class CoCreator(
override val id: String,
override val isCurrent: Boolean,
val price: String = ""
val price: String = "",
val interval: String = "",
val name: String = "",
val nameIsTaken: Boolean = false,
val nameIsFree: Boolean = false
) : Tier()
data class Custom(

View file

@ -0,0 +1,233 @@
package com.anytypeio.anytype.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
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.Spacer
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.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.views.BodyBold
import com.anytypeio.anytype.core_ui.views.HeadlineTitle
import com.anytypeio.anytype.core_ui.views.PreviewTitle1Regular
import com.anytypeio.anytype.peyments.R
import com.anytypeio.anytype.viewmodel.PaymentsCodeState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CodeScreen(
state: PaymentsCodeState,
actionResend: () -> Unit,
actionCode: (String) -> Unit,
onDismiss: () -> Unit
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismiss,
containerColor = colorResource(id = R.color.background_primary),
content = { ModalCodeContent(state = state, actionCode = actionCode) }
)
}
@Composable
private fun ModalCodeContent(state: PaymentsCodeState, actionCode: (String) -> Unit) {
val focusRequesters = remember { List(4) { FocusRequester() } }
val enteredDigits = remember { mutableStateListOf<Char>() }
val focusManager = LocalFocusManager.current
LaunchedEffect(key1 = enteredDigits.size) {
if (enteredDigits.size == 4) {
actionCode(enteredDigits.joinToString(""))
}
}
LaunchedEffect(key1 = state) {
if (state is PaymentsCodeState.Loading) {
focusManager.clearFocus(true)
}
}
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.padding(118.dp))
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp),
text = stringResource(id = com.anytypeio.anytype.localization.R.string.payments_code_title),
style = BodyBold,
color = colorResource(
id = R.color.text_primary
),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(44.dp))
val modifier = Modifier
.width(48.dp)
.height(64.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally),
horizontalArrangement = Arrangement.Center
) {
focusRequesters.forEachIndexed { index, focusRequester ->
CodeNumber(
isEnabled = state !is PaymentsCodeState.Loading,
modifier = modifier,
focusRequester = focusRequester,
onDigitEntered = { digit ->
if (enteredDigits.size < 4) {
enteredDigits.add(digit)
}
if (index < 3) focusRequesters[index + 1].requestFocus()
},
onBackspace = {
if (enteredDigits.isNotEmpty()) enteredDigits.removeLast()
if (index > 0) focusRequesters[index - 1].requestFocus()
}
)
if (index < 3) Spacer(modifier = Modifier.width(8.dp))
}
}
if (state is PaymentsCodeState.Error) {
Text(
text = state.message,
color = colorResource(id = R.color.palette_system_red),
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, top = 7.dp),
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(149.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = com.anytypeio.anytype.localization.R.string.payments_code_resend),
style = PreviewTitle1Regular,
color = colorResource(id = R.color.text_tertiary),
textAlign = TextAlign.Center
)
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = state is PaymentsCodeState.Loading,
enter = fadeIn(),
exit = fadeOut()
) {
CircularProgressIndicator(
modifier = Modifier
.size(24.dp),
color = colorResource(R.color.shape_secondary),
trackColor = colorResource(R.color.shape_primary)
)
}
}
}
@Composable
private fun CodeNumber(
isEnabled: Boolean,
focusRequester: FocusRequester,
onDigitEntered: (Char) -> Unit,
onBackspace: () -> Unit,
modifier: Modifier
) {
val (text, setText) = remember { mutableStateOf("") }
val borderColor = colorResource(id = R.color.shape_primary)
BasicTextField(
value = text,
onValueChange = { newValue ->
when {
newValue.length == 1 && newValue[0].isDigit() && text.isEmpty() -> {
setText(newValue)
onDigitEntered(newValue[0])
}
newValue.isEmpty() -> {
if (text.isNotEmpty()) {
setText("")
onBackspace()
}
}
}
},
modifier = modifier
.focusRequester(focusRequester)
.onKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && event.key == Key.Backspace && text.isEmpty()) {
onBackspace()
true
} else false
},
singleLine = true,
enabled = isEnabled,
cursorBrush = SolidColor(colorResource(id = R.color.text_primary)),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Number
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier
.border(BorderStroke(1.dp, borderColor), RoundedCornerShape(8.dp))
.padding(horizontal = 15.dp),
contentAlignment = Alignment.Center
) {
innerTextField()
}
},
textStyle = HeadlineTitle.copy(color = colorResource(id = R.color.text_primary))
)
}
@Preview
@Composable
fun EnterCodeModalPreview() {
ModalCodeContent(
state = PaymentsCodeState.Loading,
actionCode = {}
)
}

View file

@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

View file

@ -26,7 +26,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -57,7 +57,7 @@ import com.anytypeio.anytype.models.Tier
import com.anytypeio.anytype.viewmodel.PaymentsState
@Composable
fun MainPaymentsScreen(state: PaymentsState) {
fun MainPaymentsScreen(state: PaymentsState, tierClicked: (Tier) -> Unit) {
Box(
modifier = Modifier
.nestedScroll(rememberNestedScrollInteropConnection())
@ -74,11 +74,24 @@ fun MainPaymentsScreen(state: PaymentsState) {
.padding(bottom = 20.dp)
.verticalScroll(rememberScrollState())
) {
if (state is PaymentsState.Success) {
Header(state = state)
if (state is PaymentsState.Default) {
Header()
Spacer(modifier = Modifier.height(32.dp))
InfoCards()
TiersList(state = state)
TiersList(tiers = state.tiers, tierClicked = tierClicked)
Spacer(modifier = Modifier.height(32.dp))
LinkButton(text = stringResource(id = R.string.payments_member_link), action = {})
Divider()
LinkButton(text = stringResource(id = R.string.payments_privacy_link), action = {})
Divider()
LinkButton(text = stringResource(id = R.string.payments_terms_link), action = {})
Spacer(modifier = Modifier.height(32.dp))
BottomText()
}
if (state is PaymentsState.PaymentSuccess) {
Header()
Spacer(modifier = Modifier.height(32.dp))
TiersList(tiers = state.tiers, tierClicked = tierClicked)
Spacer(modifier = Modifier.height(32.dp))
LinkButton(text = stringResource(id = R.string.payments_member_link), action = {})
Divider()
@ -90,11 +103,10 @@ fun MainPaymentsScreen(state: PaymentsState) {
}
}
}
MembershipLevels(tier = Tier.Explorer(id = "888", isCurrent = true))
}
@Composable
private fun Header(state: PaymentsState.Success) {
private fun Header() {
// Dragger at the top, centered
Box(
@ -141,7 +153,7 @@ private fun Header(state: PaymentsState.Success) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TiersList(state: PaymentsState.Success) {
fun TiersList(tiers: List<Tier>, tierClicked: (Tier) -> Unit) {
val itemsScroll = rememberLazyListState(initialFirstVisibleItemIndex = 1)
LazyRow(
state = itemsScroll,
@ -152,17 +164,19 @@ fun TiersList(state: PaymentsState.Success) {
contentPadding = PaddingValues(start = 20.dp, end = 20.dp),
flingBehavior = rememberSnapFlingBehavior(lazyListState = itemsScroll)
) {
itemsIndexed(state.tiers) { _, tier ->
itemsIndexed(tiers) { _, tier ->
val resources = mapTierToResources(tier)
TierView(
title = resources.title,
subTitle = resources.subtitle,
colorGradient = resources.colorGradient,
radialGradient = resources.radialGradient,
icon = resources.smallIcon,
buttonText = stringResource(id = R.string.payments_button_learn),
onClick = { /*TODO*/ }
)
if (resources != null) {
TierView(
title = resources.title,
subTitle = resources.subtitle,
colorGradient = resources.colorGradient,
radialGradient = resources.radialGradient,
icon = resources.smallIcon,
buttonText = stringResource(id = R.string.payments_button_learn),
onClick = { tierClicked.invoke(tier) }
)
}
}
}
}
@ -272,7 +286,7 @@ fun MainPaymentsScreenPreview() {
Tier.CoCreator("999", isCurrent = false),
Tier.Custom("999", isCurrent = false)
)
MainPaymentsScreen(PaymentsState.Success(tiers))
MainPaymentsScreen(PaymentsState.PaymentSuccess(tiers), {})
}
val headerTextStyle = TextStyle(

View file

@ -1,108 +1,102 @@
package com.anytypeio.anytype.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.swipeable
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyBold
import com.anytypeio.anytype.core_ui.views.BodyCallout
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.HeadlineTitle
import com.anytypeio.anytype.core_ui.widgets.DragStates
import com.anytypeio.anytype.core_ui.views.Relations1
import com.anytypeio.anytype.core_ui.views.Relations2
import com.anytypeio.anytype.models.Tier
import com.anytypeio.anytype.peyments.R
import kotlin.math.roundToInt
@OptIn(ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MembershipLevels(tier: Tier) {
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
fun TierScreen(tier: Tier?, onDismiss: () -> Unit, actionPay: () -> Unit) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
modifier = Modifier.padding(top = 30.dp),
sheetState = sheetState,
containerColor = Color.Transparent,
dragHandle = null,
onDismissRequest = { onDismiss() },
content = {
MembershipLevels(tier = tier, actionPay = actionPay)
}
)
}
@Composable
fun MembershipLevels(tier: Tier?, actionPay: () -> Unit) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = colorResource(id = R.color.shape_tertiary)),
contentAlignment = Alignment.BottomStart,
.background(
color = colorResource(id = R.color.shape_tertiary),
shape = RoundedCornerShape(16.dp)
),
) {
val swipeableState = rememberSwipeableState(DragStates.VISIBLE)
val keyboardController = LocalSoftwareKeyboardController.current
if (swipeableState.isAnimationRunning && swipeableState.targetValue == DragStates.DISMISSED) {
DisposableEffect(Unit) {
onDispose {
keyboardController?.hide()
focusManager.clearFocus()
}
}
}
val sizePx =
with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
val tierResources = mapTierToResources(tier)
AnimatedVisibility(
visible = true,
enter = slideInVertically { it },
exit = slideOutVertically { it },
modifier = Modifier
.swipeable(state = swipeableState,
orientation = Orientation.Vertical,
anchors = mapOf(
0f to DragStates.VISIBLE, sizePx to DragStates.DISMISSED
),
thresholds = { _, _ -> FractionalThreshold(0.3f) })
.offset { IntOffset(0, swipeableState.offset.value.roundToInt()) }
) {
if (tierResources != null) {
val brush = Brush.verticalGradient(
listOf(
tierResources.colorGradient,
Color.Transparent
)
)
Column {
Box(
modifier = Modifier
@ -153,27 +147,70 @@ fun MembershipLevels(tier: Tier) {
Spacer(modifier = Modifier.height(6.dp))
}
Spacer(modifier = Modifier.height(30.dp))
NamePicker()
if (tier is Tier.Explorer) {
SubmitEmail(tier = tier, updateEmail = { email ->
//viewModel.updateEmail(email)
})
}
if (tier is Tier.Builder) {
NamePickerAndButton(
name = tier.name,
nameIsTaken = tier.nameIsTaken,
nameIsFree = tier.nameIsFree,
price = tier.price,
interval = tier.interval,
actionPay = actionPay
)
}
if (tier is Tier.CoCreator) {
NamePickerAndButton(
name = tier.name,
nameIsTaken = tier.nameIsTaken,
nameIsFree = tier.nameIsFree,
price = tier.price,
interval = tier.interval,
actionPay = actionPay
)
Price(tier.price, tier.interval)
Spacer(modifier = Modifier.height(14.dp))
ButtonPay(enabled = true, actionPay = {
})
}
}
}
}
}
@Composable
fun NamePicker() {
fun NamePickerAndButton(
name: String,
nameIsTaken: Boolean,
nameIsFree: Boolean,
price: String,
interval: String,
actionPay: () -> Unit
) {
var innerValue by remember(name) { mutableStateOf(name) }
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.fillMaxHeight()
.background(
shape = RoundedCornerShape(8.dp),
shape = RoundedCornerShape(16.dp),
color = colorResource(id = R.color.background_primary)
)
.padding(start = 20.dp, end = 20.dp)
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, top = 26.dp),
.padding(top = 26.dp),
text = stringResource(id = R.string.payments_details_name_title),
color = colorResource(id = R.color.text_primary),
style = BodyBold,
@ -182,17 +219,117 @@ fun NamePicker() {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, top = 6.dp),
.padding(top = 6.dp),
text = stringResource(id = R.string.payments_details_name_subtitle),
color = colorResource(id = R.color.text_primary),
style = BodyCallout,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(10.dp))
Row(modifier = Modifier.fillMaxWidth()) {
BasicTextField(
value = innerValue,
onValueChange = { innerValue = it },
textStyle = BodyRegular.copy(color = colorResource(id = com.anytypeio.anytype.core_ui.R.color.text_primary)),
singleLine = true,
enabled = true,
cursorBrush = SolidColor(colorResource(id = com.anytypeio.anytype.core_ui.R.color.text_primary)),
modifier = Modifier
.weight(1f)
.wrapContentHeight()
.padding(start = 0.dp, top = 2.dp)
.focusRequester(focusRequester)
.onFocusChanged { focusState ->
if (focusState.isFocused) {
} else {
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text
),
keyboardActions = KeyboardActions {
keyboardController?.hide()
focusManager.clearFocus()
},
decorationBox = { innerTextField ->
if (innerValue.isEmpty()) {
Text(
text = stringResource(id = com.anytypeio.anytype.localization.R.string.payments_details_name_hint),
style = BodyRegular,
color = colorResource(id = com.anytypeio.anytype.core_ui.R.color.text_tertiary),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
)
}
innerTextField()
}
)
Text(
text = stringResource(id = R.string.payments_details_name_domain),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
Spacer(modifier = Modifier.height(12.dp))
Divider(paddingStart = 0.dp, paddingEnd = 0.dp)
val (messageTextColor, messageText) = when {
nameIsTaken ->
colorResource(id = R.color.palette_system_red) to stringResource(id = R.string.payments_details_name_error)
nameIsFree ->
colorResource(id = R.color.palette_dark_lime) to stringResource(id = R.string.payments_details_name_success)
else ->
colorResource(id = R.color.text_secondary) to stringResource(id = R.string.payments_details_name_min)
}
Spacer(modifier = Modifier.height(10.dp))
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
text = messageText,
color = messageTextColor,
style = Relations2,
textAlign = TextAlign.Center
)
Price(price = price, interval = interval)
Spacer(modifier = Modifier.height(14.dp))
ButtonPay(enabled = true, actionPay = {
actionPay()
})
}
}
@Composable
private fun Price(price: String, interval: String) {
Row() {
Text(
modifier = Modifier
.wrapContentWidth()
.padding(start = 20.dp),
text = price,
color = colorResource(id = R.color.text_primary),
style = HeadlineTitle,
textAlign = TextAlign.Start
)
Text(
modifier = Modifier
.wrapContentWidth()
.align(Alignment.Bottom)
.padding(bottom = 4.dp, start = 6.dp),
text = interval,
color = colorResource(id = R.color.text_primary),
style = Relations1,
textAlign = TextAlign.Start
)
}
}
@Composable
fun Benefit(benefit: String) {
private fun Benefit(benefit: String) {
Box(
modifier = Modifier
.wrapContentHeight()
@ -218,9 +355,141 @@ fun Benefit(benefit: String) {
}
}
@Composable
private fun SubmitEmail(tier: Tier.Explorer, updateEmail: (String) -> Unit) {
var innerValue by remember(tier.email) { mutableStateOf(tier.email) }
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
var isChecked by remember(tier.isChecked) { mutableStateOf(tier.isChecked) }
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(
shape = RoundedCornerShape(16.dp),
color = colorResource(id = R.color.background_primary)
)
.padding(start = 20.dp, end = 20.dp)
) {
Spacer(modifier = Modifier.height(26.dp))
Text(
text = stringResource(id = R.string.payments_email_title),
style = BodyBold,
color = colorResource(id = R.color.text_primary)
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = stringResource(id = R.string.payments_email_subtitle),
style = BodyCallout,
color = colorResource(id = R.color.text_primary)
)
Spacer(modifier = Modifier.height(10.dp))
BasicTextField(
value = innerValue,
onValueChange = { innerValue = it },
textStyle = BodyRegular.copy(color = colorResource(id = com.anytypeio.anytype.core_ui.R.color.text_primary)),
singleLine = true,
enabled = true,
cursorBrush = SolidColor(colorResource(id = com.anytypeio.anytype.core_ui.R.color.text_primary)),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 0.dp, top = 2.dp)
.focusRequester(focusRequester)
.onFocusChanged { focusState ->
if (focusState.isFocused) {
} else {
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email
),
keyboardActions = KeyboardActions {
keyboardController?.hide()
focusManager.clearFocus()
},
decorationBox = { innerTextField ->
if (innerValue.isEmpty()) {
Text(
text = stringResource(id = com.anytypeio.anytype.localization.R.string.payments_email_hint),
style = BodyRegular,
color = colorResource(id = com.anytypeio.anytype.core_ui.R.color.text_tertiary),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
)
}
innerTextField()
}
)
Spacer(modifier = Modifier.height(12.dp))
Divider(paddingStart = 0.dp, paddingEnd = 0.dp)
val icon = if (isChecked) {
R.drawable.ic_system_checkbox
} else {
R.drawable.ic_system_checkbox_empty
}
Spacer(modifier = Modifier.height(15.dp))
Row {
Image(
modifier = Modifier
.padding(top = 3.dp)
.size(16.dp)
.noRippleClickable { isChecked = !isChecked },
painter = painterResource(id = icon),
contentDescription = "checkbox"
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp),
text = stringResource(id = R.string.payments_email_checkbox_text),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
Spacer(modifier = Modifier.height(31.dp))
val enabled = innerValue.isNotEmpty()
ButtonPrimary(
enabled = enabled,
text = stringResource(id = R.string.payments_detials_button_submit),
onClick = { updateEmail.invoke(innerValue) },
size = ButtonSize.Large,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
private fun ButtonPay(enabled: Boolean, actionPay: () -> Unit) {
ButtonPrimary(
enabled = enabled,
text = stringResource(id = R.string.payments_detials_button_pay),
onClick = { actionPay() },
size = ButtonSize.Large,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
)
}
@Preview()
@Composable
fun MyLevel() {
MembershipLevels(tier = Tier.Explorer("121", true))
MembershipLevels(
tier = Tier.Builder(
id = "121",
isCurrent = true,
price = "$99",
interval = "per year"
),
actionPay = {}
)
}

View file

@ -10,8 +10,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
@ -120,7 +120,7 @@ fun PriceOrOption() {
}
@Composable
fun mapTierToResources(tier: Tier): TierResources {
fun mapTierToResources(tier: Tier?): TierResources? {
return when (tier) {
is Tier.Builder -> TierResources(
title = stringResource(id = R.string.payments_tier_builder),
@ -160,6 +160,8 @@ fun mapTierToResources(tier: Tier): TierResources {
radialGradient = Color(0xFF24BFD4),
benefits = stringArrayResource(id = R.array.payments_benefits_explorer).toList()
)
else -> null
}
}

View file

@ -4,5 +4,20 @@ import com.anytypeio.anytype.models.Tier
sealed class PaymentsState {
object Loading : PaymentsState()
data class Success(val tiers: List<Tier>) : PaymentsState()
data class Default(val tiers: List<Tier>) : PaymentsState()
data class PaymentSuccess(val tiers: List<Tier>) : PaymentsState()
}
sealed class PaymentsCodeState {
object Empty : PaymentsCodeState()
object Loading : PaymentsCodeState()
object Success : PaymentsCodeState()
data class Error(val message: String) : PaymentsCodeState()
}
sealed class PaymentsNavigation(val route: String) {
object Main : PaymentsNavigation("main")
object Tier : PaymentsNavigation("tier")
object Code : PaymentsNavigation("code")
object Dismiss : PaymentsNavigation("")
}

View file

@ -11,10 +11,14 @@ class PaymentsViewModel(
) : ViewModel() {
val viewState = MutableStateFlow<PaymentsState>(PaymentsState.Loading)
val codeViewState = MutableStateFlow<PaymentsCodeState>(PaymentsCodeState.Empty)
val command = MutableStateFlow<PaymentsNavigation?>(null)
val selectedTier = MutableStateFlow<Tier?>(null)
init {
Timber.d("PaymentsViewModel created")
viewState.value = PaymentsState.Success(
viewState.value = PaymentsState.Default(
listOf(
Tier.Explorer("Free", true),
Tier.Builder("$9.99/mo", false),
@ -24,8 +28,24 @@ class PaymentsViewModel(
)
}
interface PaymentsNavigation {
object MembershipMain : PaymentsNavigation
object MembershipLevel : PaymentsNavigation
fun onTierClicked(tier: Tier) {
selectedTier.value = tier
command.value = PaymentsNavigation.Tier
}
fun onActionCode(code: String) {
Timber.d("onActionCode: $code")
}
fun onPayButtonClicked() {
command.value = PaymentsNavigation.Code
}
fun onDismissTier() {
command.value = PaymentsNavigation.Dismiss
}
fun onDismissCode() {
command.value = PaymentsNavigation.Dismiss
}
}