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 | Welcome screen (#1000)

This commit is contained in:
Konstantin Ivanov 2024-03-14 13:21:13 +01:00 committed by GitHub
parent 7d65104c34
commit d123867aaf
Signed by: github
GPG key ID: B5690EEEBB952194
12 changed files with 536 additions and 201 deletions

View file

@ -19,6 +19,7 @@ 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.PaymentWelcomeScreen
import com.anytypeio.anytype.screens.TierScreen
import com.anytypeio.anytype.ui.settings.typography
import com.anytypeio.anytype.viewmodel.PaymentsNavigation
@ -62,6 +63,10 @@ class PaymentsFragment : BaseBottomSheetComposeFragment() {
when (command) {
PaymentsNavigation.Tier -> navController.navigate(PaymentsNavigation.Tier.route)
PaymentsNavigation.Code -> navController.navigate(PaymentsNavigation.Code.route)
PaymentsNavigation.Welcome -> {
navController.popBackStack(PaymentsNavigation.Main.route, false)
navController.navigate(PaymentsNavigation.Welcome.route)
}
PaymentsNavigation.Dismiss -> navController.popBackStack()
else -> {}
}
@ -84,19 +89,24 @@ class PaymentsFragment : BaseBottomSheetComposeFragment() {
private fun NavigationGraph(navController: NavHostController) {
NavHost(navController = navController, startDestination = PaymentsNavigation.Main.route) {
composable(PaymentsNavigation.Main.route) {
MainPaymentsScreen()
InitMainPaymentsScreen()
}
bottomSheet(PaymentsNavigation.Tier.route) {
TierScreen()
InitTierScreen()
}
bottomSheet(PaymentsNavigation.Code.route) {
CodeScreen()
InitCodeScreen()
}
bottomSheet(PaymentsNavigation.Welcome.route) {
InitWelcomeScreen()
}
}
}
@Composable
private fun MainPaymentsScreen() {
private fun InitMainPaymentsScreen() {
skipCollapsed()
expand()
MainPaymentsScreen(
state = vm.viewState.collectAsStateWithLifecycle().value,
tierClicked = vm::onTierClicked
@ -104,24 +114,32 @@ class PaymentsFragment : BaseBottomSheetComposeFragment() {
}
@Composable
private fun TierScreen() {
private fun InitTierScreen() {
TierScreen(
tier = vm.selectedTier.collectAsStateWithLifecycle().value,
state = vm.tierState.collectAsStateWithLifecycle().value,
onDismiss = vm::onDismissTier,
actionPay = vm::onPayButtonClicked
)
}
@Composable
private fun CodeScreen() {
private fun InitCodeScreen() {
CodeScreen(
state = vm.codeViewState.collectAsStateWithLifecycle().value,
state = vm.codeState.collectAsStateWithLifecycle().value,
actionResend = { },
actionCode = vm::onActionCode,
onDismiss = vm::onDismissCode
)
}
@Composable
private fun InitWelcomeScreen() {
PaymentWelcomeScreen(
state = vm.welcomeState.collectAsStateWithLifecycle().value,
onDismiss = vm::onDismissWelcome
)
}
override fun injectDependencies() {
componentManager().paymentsComponent.get().inject(this)
}

View file

@ -223,4 +223,6 @@
<color name="widget_edit_view_stroke_color_active">@color/palette_system_amber_50</color>
<color name="widget_edit_view_stroke_color_inactive">@color/shape_primary</color>
<color name="payments_tier_current_background">#F6F6F6</color>
</resources>

View file

@ -1343,20 +1343,20 @@
<!--region PAYMENTS -->
<string name="payments_header">Lets build\ntogether</string>
<string name="payments_header">Membership</string>
<string name="payments_subheader">Joining Anytype network means contributing to its story</string>
<string name="payments_card_text_1">Co-create with us</string>
<string name="payments_card_description_1">Stay closely connected with our team and community. Join calls with the team, influence Anytype\'s evolution, and have your say on features.</string>
<string name="payments_card_text_1">Co-create the Vision</string>
<string name="payments_card_description_1">As a valued member your voice matters! Engage in exclusive events, shape strategic choices, and influence our roadmap.</string>
<string name="payments_card_text_2">Gain Benefits</string>
<string name="payments_card_description_2">Our members have unique identity on Anytype Network, more storage, shared spaces and members per space for extensive collaboration.</string>
<string name="payments_card_text_2">Unlock Member Benefits</string>
<string name="payments_card_description_2">Members enjoy higher backup storage &amp; sync limits, invitations for multiple guests to collaborate in shared spaces, and a unique identity on the Anytype Network.</string>
<string name="payments_card_text_3">Support the Vision</string>
<string name="payments_card_description_3">Your contribution supports our independent team and endorses our vision of a user-driven, secure, and collaborative digital environment.</string>
<string name="payments_card_text_3">Support Digital Independence</string>
<string name="payments_card_description_3">Your contribution supports our team and endorses our vision of a user-owned, secure, and collaborative digital network.</string>
<string name="payments_card_text_4">Invest in Connectivity</string>
<string name="payments_card_description_4">Our software is free by design, but we thrive on the network that connects us all. Support us, and you\'re investing in the very infrastructure that keeps us united.</string>
<string name="payments_card_description_4">Our network\'s value exceeds the sum of its parts. Your membership sustains the infrastructure for its growth which underpins this network.</string>
<string name="payments_tier_explorer">Explorer</string>
<string name="payments_tier_explorer_description">Dive into the network and enjoy the thrill of one-on-one collaboration</string>
@ -1375,24 +1375,25 @@
<string name="payments_terms_link">Terms and conditions</string>
<string name="payments_let_us_link_start">Would you like to use Anytype for business, education, etc.?</string>
<string name="payments_let_us_link_end">Please let us know here.</string>
<string name="payments_current_label">Current</string>
<!-- Membership Level Details -->
<string name="payments_details_name_title">Pick your unique name</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>
<!-- Tier details -->
<string name="payments_tier_details_name_title">Pick your unique name</string>
<string name="payments_tier_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_tier_details_name_hint">Myself</string>
<string name="payments_tier_details_name_domain">.any</string>
<string name="payments_tier_details_name_min">Min 7 characters</string>
<string name="payments_tier_details_name_error">This name is already taken!</string>
<string name="payments_tier_details_name_success">This name is up for grabs!</string>
<string name="payments_details_info_explorer">Dive into the network and enjoy the thrill of one-on-one collaboration</string>
<string name="payments_tier_details_info_explorer">Dive into the network and enjoy the thrill of one-on-one collaboration</string>
<string-array name="payments_benefits_explorer">
<item>1 GB of network space</item>
<item>10 one-to-one spaces</item>
<item>Up to 10 shared spaces in read-only mode</item>
</string-array>
<string name="payments_details_info_builder">Unlock the magic of multi-party collaboration and enjoy top-notch support</string>
<string name="payments_tier_details_info_builder">Unlock the magic of multi-party collaboration and enjoy top-notch support</string>
<string-array name="payments_benefits_builder">
<item>Unique name (from 7 characters)</item>
<item>128 GB of network space</item>
@ -1400,7 +1401,7 @@
<item>Priority support</item>
</string-array>
<string name="payments_details_info_cocreator">Support our adventure and unlock exclusive access and perks</string>
<string name="payments_tier_details_info_cocreator">Support our adventure and unlock exclusive access and perks</string>
<string-array name="payments_benefits_cocreator">
<item>Unique name (from 5 characters)</item>
<item>256 GB of network space</item>
@ -1417,10 +1418,20 @@
<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>
<string name="payments_tier_current_title">Your current status:</string>
<string name="payments_tier_current_valid">Valid until</string>
<string name="payments_tier_current_paid_by">Paid by Card</string>
<string name="payments_tier_current_button">Manage payment</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>
<!-- Payments Welcome -->
<string name="payments_welcome_title">Welcome to the network,\n%1$s</string>
<string name="payments_welcome_subtitle">Big cheers for your curiosity!</string>
<string name="payments_welcome_button">Continue</string>
</resources>

View file

@ -1,31 +1,36 @@
package com.anytypeio.anytype.models
import com.anytypeio.anytype.viewmodel.TierId
sealed class Tier {
abstract val id: String
abstract val id: TierId
abstract val isCurrent: Boolean
abstract val validUntil: String
data class Explorer(
override val id: String,
override val id: TierId,
override val isCurrent: Boolean,
override val validUntil: String,
val price: String = "",
val email: String = "",
val isChecked: Boolean = true
) : Tier()
data class Builder(
override val id: String,
override val id: TierId,
override val isCurrent: Boolean,
override val validUntil: 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 id: TierId,
override val isCurrent: Boolean,
override val validUntil: String,
val price: String = "",
val interval: String = "",
val name: String = "",
@ -34,8 +39,9 @@ sealed class Tier {
) : Tier()
data class Custom(
override val id: String,
override val id: TierId,
override val isCurrent: Boolean,
override val validUntil: String,
val price: String = ""
) : Tier()
}

View file

@ -52,38 +52,42 @@ 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
import com.anytypeio.anytype.viewmodel.TierId
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CodeScreen(
state: PaymentsCodeState,
actionResend: () -> Unit,
actionCode: (String) -> Unit,
actionCode: (String, TierId) -> 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) }
)
if (state is PaymentsCodeState.Visible) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismiss,
containerColor = colorResource(id = R.color.background_primary),
content = { ModalCodeContent(state = state, actionCode = { code -> actionCode(code, state.tierId)}) }
)
}
}
@Composable
private fun ModalCodeContent(state: PaymentsCodeState, actionCode: (String) -> Unit) {
private fun ModalCodeContent(state: PaymentsCodeState.Visible, 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(""))
val code = enteredDigits.joinToString("")
actionCode(code)
}
}
LaunchedEffect(key1 = state) {
if (state is PaymentsCodeState.Loading) {
if (state is PaymentsCodeState.Visible.Loading) {
focusManager.clearFocus(true)
}
}
@ -114,7 +118,7 @@ private fun ModalCodeContent(state: PaymentsCodeState, actionCode: (String) -> U
) {
focusRequesters.forEachIndexed { index, focusRequester ->
CodeNumber(
isEnabled = state !is PaymentsCodeState.Loading,
isEnabled = state !is PaymentsCodeState.Visible.Loading,
modifier = modifier,
focusRequester = focusRequester,
onDigitEntered = { digit ->
@ -131,7 +135,7 @@ private fun ModalCodeContent(state: PaymentsCodeState, actionCode: (String) -> U
if (index < 3) Spacer(modifier = Modifier.width(8.dp))
}
}
if (state is PaymentsCodeState.Error) {
if (state is PaymentsCodeState.Visible.Error) {
Text(
text = state.message,
color = colorResource(id = R.color.palette_system_red),
@ -152,7 +156,7 @@ private fun ModalCodeContent(state: PaymentsCodeState, actionCode: (String) -> U
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = state is PaymentsCodeState.Loading,
visible = state is PaymentsCodeState.Visible.Loading,
enter = fadeIn(),
exit = fadeOut()
) {
@ -227,7 +231,7 @@ private fun CodeNumber(
@Composable
fun EnterCodeModalPreview() {
ModalCodeContent(
state = PaymentsCodeState.Loading,
actionCode = {}
state = PaymentsCodeState.Visible.Loading(TierId("123")),
actionCode = { _ -> }
)
}

View file

@ -54,10 +54,11 @@ import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.Relations2
import com.anytypeio.anytype.core_ui.views.fontRiccioneRegular
import com.anytypeio.anytype.models.Tier
import com.anytypeio.anytype.viewmodel.PaymentsState
import com.anytypeio.anytype.viewmodel.PaymentsMainState
import com.anytypeio.anytype.viewmodel.TierId
@Composable
fun MainPaymentsScreen(state: PaymentsState, tierClicked: (Tier) -> Unit) {
fun MainPaymentsScreen(state: PaymentsMainState, tierClicked: (TierId) -> Unit) {
Box(
modifier = Modifier
.nestedScroll(rememberNestedScrollInteropConnection())
@ -74,10 +75,13 @@ fun MainPaymentsScreen(state: PaymentsState, tierClicked: (Tier) -> Unit) {
.padding(bottom = 20.dp)
.verticalScroll(rememberScrollState())
) {
if (state is PaymentsState.Default) {
Header()
if (state is PaymentsMainState.Default) {
Title()
Spacer(modifier = Modifier.height(7.dp))
Subtitle()
Spacer(modifier = Modifier.height(32.dp))
InfoCards()
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 = {})
@ -88,9 +92,9 @@ fun MainPaymentsScreen(state: PaymentsState, tierClicked: (Tier) -> Unit) {
Spacer(modifier = Modifier.height(32.dp))
BottomText()
}
if (state is PaymentsState.PaymentSuccess) {
Header()
Spacer(modifier = Modifier.height(32.dp))
if (state is PaymentsMainState.PaymentSuccess) {
Title()
Spacer(modifier = Modifier.height(39.dp))
TiersList(tiers = state.tiers, tierClicked = tierClicked)
Spacer(modifier = Modifier.height(32.dp))
LinkButton(text = stringResource(id = R.string.payments_member_link), action = {})
@ -106,7 +110,7 @@ fun MainPaymentsScreen(state: PaymentsState, tierClicked: (Tier) -> Unit) {
}
@Composable
private fun Header() {
private fun Title() {
// Dragger at the top, centered
Box(
@ -133,7 +137,10 @@ private fun Header() {
textAlign = TextAlign.Center
)
}
}
@Composable
private fun Subtitle() {
Box(
modifier = Modifier
.fillMaxWidth()
@ -142,7 +149,7 @@ private fun Header() {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 60.dp, end = 60.dp, top = 7.dp),
.padding(start = 60.dp, end = 60.dp),
text = stringResource(id = R.string.payments_subheader),
color = colorResource(id = R.color.text_primary),
style = Relations2,
@ -153,13 +160,12 @@ private fun Header() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TiersList(tiers: List<Tier>, tierClicked: (Tier) -> Unit) {
fun TiersList(tiers: List<Tier>, tierClicked: (TierId) -> Unit) {
val itemsScroll = rememberLazyListState(initialFirstVisibleItemIndex = 1)
LazyRow(
state = itemsScroll,
modifier = Modifier
.fillMaxWidth()
.padding(top = 32.dp),
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(20.dp),
contentPadding = PaddingValues(start = 20.dp, end = 20.dp),
flingBehavior = rememberSnapFlingBehavior(lazyListState = itemsScroll)
@ -174,7 +180,8 @@ fun TiersList(tiers: List<Tier>, tierClicked: (Tier) -> Unit) {
radialGradient = resources.radialGradient,
icon = resources.smallIcon,
buttonText = stringResource(id = R.string.payments_button_learn),
onClick = { tierClicked.invoke(tier) }
onClick = { tierClicked.invoke(tier.id) },
isCurrent = tier.isCurrent
)
}
}
@ -281,12 +288,12 @@ fun BottomText() {
@Composable
fun MainPaymentsScreenPreview() {
val tiers = listOf(
Tier.Explorer("999", isCurrent = true),
Tier.Builder("999", isCurrent = false),
Tier.CoCreator("999", isCurrent = false),
Tier.Custom("999", isCurrent = false)
Tier.Explorer(TierId("999"), isCurrent = true, validUntil = "2022-12-31"),
Tier.Builder(TierId("999"), isCurrent = true, validUntil = "2022-12-31"),
Tier.CoCreator(TierId("999"), isCurrent = false, validUntil = "2022-12-31"),
Tier.Custom(TierId("999"), isCurrent = false, validUntil = "2022-12-31")
)
MainPaymentsScreen(PaymentsState.PaymentSuccess(tiers), {})
MainPaymentsScreen(PaymentsMainState.PaymentSuccess(tiers), {})
}
val headerTextStyle = TextStyle(

View file

@ -52,32 +52,37 @@ 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.ButtonPrimary
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.HeadlineTitle
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 com.anytypeio.anytype.viewmodel.PaymentsTierState
import com.anytypeio.anytype.viewmodel.TierId
@OptIn(ExperimentalMaterial3Api::class)
@Composable
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)
}
)
fun TierScreen(state: PaymentsTierState, onDismiss: () -> Unit, actionPay: (TierId) -> Unit) {
if (state is PaymentsTierState.Visible) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
modifier = Modifier.padding(top = 30.dp),
sheetState = sheetState,
containerColor = Color.Transparent,
dragHandle = null,
onDismissRequest = { onDismiss() },
content = {
MembershipLevels(tier = state.tier, actionPay = { actionPay(state.tier.id) })
}
)
}
}
@Composable
fun MembershipLevels(tier: Tier?, actionPay: () -> Unit) {
fun MembershipLevels(tier: Tier, actionPay: () -> Unit) {
Box(
modifier = Modifier
@ -153,29 +158,32 @@ fun MembershipLevels(tier: Tier?, actionPay: () -> Unit) {
})
}
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.isCurrent) {
StatusSubscribed(tier, {})
} else {
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 = {
})
if (tier.isCurrent) {
StatusSubscribed(tier, {})
} else {
NamePickerAndButton(
name = tier.name,
nameIsTaken = tier.nameIsTaken,
nameIsFree = tier.nameIsFree,
price = tier.price,
interval = tier.interval,
actionPay = actionPay
)
}
}
}
}
@ -211,7 +219,7 @@ fun NamePickerAndButton(
modifier = Modifier
.fillMaxWidth()
.padding(top = 26.dp),
text = stringResource(id = R.string.payments_details_name_title),
text = stringResource(id = R.string.payments_tier_details_name_title),
color = colorResource(id = R.color.text_primary),
style = BodyBold,
textAlign = TextAlign.Start
@ -220,7 +228,7 @@ fun NamePickerAndButton(
modifier = Modifier
.fillMaxWidth()
.padding(top = 6.dp),
text = stringResource(id = R.string.payments_details_name_subtitle),
text = stringResource(id = R.string.payments_tier_details_name_subtitle),
color = colorResource(id = R.color.text_primary),
style = BodyCallout,
textAlign = TextAlign.Start
@ -256,7 +264,7 @@ fun NamePickerAndButton(
decorationBox = { innerTextField ->
if (innerValue.isEmpty()) {
Text(
text = stringResource(id = com.anytypeio.anytype.localization.R.string.payments_details_name_hint),
text = stringResource(id = com.anytypeio.anytype.localization.R.string.payments_tier_details_name_hint),
style = BodyRegular,
color = colorResource(id = com.anytypeio.anytype.core_ui.R.color.text_tertiary),
modifier = Modifier
@ -268,7 +276,7 @@ fun NamePickerAndButton(
}
)
Text(
text = stringResource(id = R.string.payments_details_name_domain),
text = stringResource(id = R.string.payments_tier_details_name_domain),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
@ -277,13 +285,13 @@ fun NamePickerAndButton(
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)
colorResource(id = R.color.palette_system_red) to stringResource(id = R.string.payments_tier_details_name_error)
nameIsFree ->
colorResource(id = R.color.palette_dark_lime) to stringResource(id = R.string.payments_details_name_success)
colorResource(id = R.color.palette_dark_lime) to stringResource(id = R.string.payments_tier_details_name_success)
else ->
colorResource(id = R.color.text_secondary) to stringResource(id = R.string.payments_details_name_min)
colorResource(id = R.color.text_secondary) to stringResource(id = R.string.payments_tier_details_name_min)
}
Spacer(modifier = Modifier.height(10.dp))
Text(
@ -303,6 +311,80 @@ fun NamePickerAndButton(
}
}
@Composable
private fun StatusSubscribed(tier: Tier, actionManage: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(
shape = RoundedCornerShape(16.dp),
color = colorResource(id = R.color.background_primary)
)
.padding(start = 20.dp, end = 20.dp)
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 26.dp),
text = stringResource(id = R.string.payments_tier_current_title),
color = colorResource(id = R.color.text_primary),
style = BodyBold,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(14.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(
shape = RoundedCornerShape(12.dp),
color = colorResource(id = R.color.payments_tier_current_background)
)
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 34.dp),
text = stringResource(id = R.string.payments_tier_current_valid),
color = colorResource(id = R.color.text_primary),
style = Relations2,
textAlign = TextAlign.Center
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
text = tier.validUntil,
color = colorResource(id = R.color.text_primary),
style = HeadlineTitle,
textAlign = TextAlign.Center
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 23.dp, bottom = 15.dp),
text = stringResource(id = R.string.payments_tier_current_paid_by),
color = colorResource(id = R.color.text_secondary),
style = Relations2,
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(20.dp))
ButtonSecondary(
enabled = true,
text = stringResource(id = R.string.payments_tier_current_button),
onClick = { actionManage() },
size = ButtonSize.LargeSecondary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
)
}
}
@Composable
private fun Price(price: String, interval: String) {
Row() {
@ -485,10 +567,11 @@ private fun ButtonPay(enabled: Boolean, actionPay: () -> Unit) {
fun MyLevel() {
MembershipLevels(
tier = Tier.Builder(
id = "121",
id = TierId("121"),
isCurrent = true,
price = "$99",
interval = "per year"
interval = "per year",
validUntil = "12/12/2025",
),
actionPay = {}
)

View file

@ -0,0 +1,109 @@
package com.anytypeio.anytype.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.BodyRegular
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
import com.anytypeio.anytype.models.Tier
import com.anytypeio.anytype.peyments.R
import com.anytypeio.anytype.viewmodel.PaymentsWelcomeState
import com.anytypeio.anytype.viewmodel.TierId
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PaymentWelcomeScreen(state: PaymentsWelcomeState, onDismiss: () -> Unit) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
if (state is PaymentsWelcomeState.Initial) {
ModalBottomSheet(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp)
.fillMaxWidth()
.wrapContentHeight(),
sheetState = sheetState,
onDismissRequest = onDismiss,
containerColor = colorResource(id = R.color.background_secondary),
content = {
val tierResources = mapTierToResources(state.tier)
if (tierResources != null) WelcomeContent(tierResources, onDismiss)
},
shape = RoundedCornerShape(16.dp),
dragHandle = null
)
}
}
@Composable
private fun WelcomeContent(tierResources: TierResources, onDismiss: () -> Unit) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(36.dp))
Icon(
modifier = Modifier.wrapContentSize(),
painter = painterResource(id = tierResources.mediumIcon!!),
contentDescription = "logo",
tint = tierResources.radialGradient
)
Spacer(modifier = Modifier.height(14.dp))
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
text = stringResource(id = R.string.payments_welcome_title, tierResources.title),
color = colorResource(id = R.color.text_primary),
style = HeadlineHeading,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(7.dp))
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
text = stringResource(id = R.string.payments_welcome_subtitle),
color = colorResource(id = R.color.text_primary),
style = BodyRegular,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(30.dp))
ButtonSecondary(
text = stringResource(id = R.string.payments_welcome_button),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
onClick = { onDismiss() },
size = ButtonSize.LargeSecondary
)
Spacer(modifier = Modifier.height(16.dp))
}
}
@Preview
@Composable
fun PaymentWelcomeScreenPreview() {
PaymentWelcomeScreen(
PaymentsWelcomeState.Initial(Tier.Explorer(TierId("Free"), true, "01-01-2025")), {})
}

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -9,10 +10,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@ -32,6 +35,7 @@ import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.fontInterSemibold
import com.anytypeio.anytype.models.Tier
@ -43,7 +47,8 @@ fun TierView(
radialGradient: Color,
icon: Int,
buttonText: String,
onClick: () -> Unit
onClick: () -> Unit,
isCurrent: Boolean
) {
val brush = Brush.verticalGradient(
listOf(
@ -52,60 +57,81 @@ fun TierView(
)
)
Column(
modifier = Modifier
.width(192.dp)
.wrapContentHeight()
.background(
color = colorResource(id = R.color.shape_tertiary),
shape = RoundedCornerShape(16.dp)
)
.noRippleThrottledClickable { onClick() }
) {
Box(
Box(modifier = Modifier.wrapContentSize()) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.background(brush = brush, shape = RoundedCornerShape(16.dp)),
contentAlignment = androidx.compose.ui.Alignment.BottomStart
.width(192.dp)
.wrapContentHeight()
.background(
color = colorResource(id = R.color.shape_tertiary),
shape = RoundedCornerShape(16.dp)
)
.noRippleThrottledClickable { onClick() }
) {
Icon(
Box(
modifier = Modifier
.padding(start = 16.dp),
painter = painterResource(id = icon),
contentDescription = "logo",
tint = radialGradient
.fillMaxWidth()
.height(80.dp)
.background(brush = brush, shape = RoundedCornerShape(16.dp)),
contentAlignment = Alignment.BottomStart
) {
Icon(
modifier = Modifier
.padding(start = 16.dp),
painter = painterResource(id = icon),
contentDescription = "logo",
tint = radialGradient
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 17.dp, top = 10.dp),
text = title,
color = colorResource(id = R.color.text_primary),
style = titleTextStyle,
textAlign = TextAlign.Start
)
Text(
modifier = Modifier
.fillMaxWidth()
.height(96.dp)
.padding(start = 16.dp, end = 16.dp, top = 5.dp),
text = subTitle,
color = colorResource(id = R.color.text_primary),
style = Caption1Regular,
textAlign = TextAlign.Start
)
PriceOrOption()
ButtonPrimary(
text = buttonText,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
onClick = { onClick() },
size = ButtonSize.Small
)
Spacer(modifier = Modifier.height(10.dp))
}
if (isCurrent) {
Text(
modifier = Modifier
.wrapContentSize()
.padding(end = 15.5.dp, top = 18.dp)
.border(
shape = RoundedCornerShape(11.dp),
color = colorResource(id = R.color.text_primary),
width = 1.dp
)
.align(Alignment.TopEnd)
.padding(top = 2.dp, bottom = 3.dp, start = 8.dp, end = 8.dp),
text = stringResource(id = R.string.payments_current_label),
color = colorResource(id = R.color.text_primary),
style = Relations3,
textAlign = TextAlign.Center
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 17.dp, top = 10.dp),
text = title,
color = colorResource(id = R.color.text_primary),
style = titleTextStyle,
textAlign = TextAlign.Start
)
Text(
modifier = Modifier
.fillMaxWidth()
.height(96.dp)
.padding(start = 16.dp, end = 16.dp, top = 5.dp),
text = subTitle,
color = colorResource(id = R.color.text_primary),
style = Caption1Regular,
textAlign = TextAlign.Start
)
PriceOrOption()
ButtonPrimary(
text = buttonText,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
onClick = { /*TODO*/ },
size = ButtonSize.Small
)
Spacer(modifier = Modifier.height(10.dp))
}
}
@ -175,7 +201,8 @@ fun TierPreview() {
onClick = {},
icon = R.drawable.logo_co_creator_64,
colorGradient = Color(0xFFCFF6CF),
radialGradient = Color(0xFF24BFD4)
radialGradient = Color(0xFF24BFD4),
isCurrent = true
)
}

View file

@ -0,0 +1,49 @@
package com.anytypeio.anytype.viewmodel
import com.anytypeio.anytype.models.Tier
sealed class PaymentsMainState {
object Loading : PaymentsMainState()
data class Default(val tiers: List<Tier>) : PaymentsMainState()
data class PaymentSuccess(val tiers: List<Tier>) : PaymentsMainState()
}
sealed class PaymentsTierState {
object Hidden : PaymentsTierState()
sealed class Visible : PaymentsTierState() {
abstract val tier: Tier
data class Initial(override val tier: Tier) : Visible()
data class Subscribed(override val tier: Tier) : Visible()
}
}
sealed class PaymentsCodeState {
object Hidden : PaymentsCodeState()
sealed class Visible : PaymentsCodeState() {
abstract val tierId: TierId
data class Initial(override val tierId: TierId) : Visible()
data class Loading(override val tierId: TierId) : Visible()
data class Success(override val tierId: TierId) : Visible()
data class Error(override val tierId: TierId, val message: String) : Visible()
}
}
sealed class PaymentsWelcomeState {
object Hidden : PaymentsWelcomeState()
data class Initial(val tier: Tier) : PaymentsWelcomeState()
}
sealed class PaymentsNavigation(val route: String) {
object Main : PaymentsNavigation("main")
object Tier : PaymentsNavigation("tier")
object Code : PaymentsNavigation("code")
object Welcome : PaymentsNavigation("welcome")
object Dismiss : PaymentsNavigation("")
}
@JvmInline
value class TierId(val value: String)

View file

@ -1,23 +0,0 @@
package com.anytypeio.anytype.viewmodel
import com.anytypeio.anytype.models.Tier
sealed class PaymentsState {
object Loading : 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

@ -1,51 +1,93 @@
package com.anytypeio.anytype.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.models.Tier
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class PaymentsViewModel(
private val analytics: Analytics,
) : ViewModel() {
val viewState = MutableStateFlow<PaymentsState>(PaymentsState.Loading)
val codeViewState = MutableStateFlow<PaymentsCodeState>(PaymentsCodeState.Empty)
val viewState = MutableStateFlow<PaymentsMainState>(PaymentsMainState.Loading)
val codeState = MutableStateFlow<PaymentsCodeState>(PaymentsCodeState.Hidden)
val tierState = MutableStateFlow<PaymentsTierState>(PaymentsTierState.Hidden)
val welcomeState = MutableStateFlow<PaymentsWelcomeState>(PaymentsWelcomeState.Hidden)
val command = MutableStateFlow<PaymentsNavigation?>(null)
val selectedTier = MutableStateFlow<Tier?>(null)
private val _tiers = mutableListOf<Tier>()
init {
Timber.d("PaymentsViewModel created")
viewState.value = PaymentsState.Default(
listOf(
Tier.Explorer("Free", true),
Tier.Builder("$9.99/mo", false),
Tier.CoCreator("$19.99/mo", false),
Tier.Custom("$29.99/mo", false)
)
)
Timber.d("PaymentsViewModel init")
_tiers.addAll(gertTiers())
viewState.value = PaymentsMainState.Default(_tiers)
}
fun onTierClicked(tier: Tier) {
selectedTier.value = tier
fun onTierClicked(tierId: TierId) {
Timber.d("onTierClicked: tierId:$tierId")
tierState.value = PaymentsTierState.Visible.Initial(tier = _tiers.first { it.id == tierId })
command.value = PaymentsNavigation.Tier
}
fun onActionCode(code: String) {
Timber.d("onActionCode: $code")
fun onActionCode(code: String, tierId: TierId) {
Timber.d("onActionCode: tierId:$tierId, code:$code, _tiers:${_tiers}")
viewModelScope.launch {
codeState.value = PaymentsCodeState.Visible.Loading(tierId = tierId)
delay(2000)
welcomeState.value =
PaymentsWelcomeState.Initial(tier = _tiers.first { it.id == tierId })
val updatedTiers = _tiers.map {
if (it.id == tierId) {
when (it) {
is Tier.Builder -> it.copy(isCurrent = true)
is Tier.CoCreator -> it.copy(isCurrent = true)
is Tier.Custom -> it.copy(isCurrent = true)
is Tier.Explorer -> it.copy(isCurrent = true)
}
} else {
it
}
}
_tiers.clear()
_tiers.addAll(updatedTiers)
viewState.value = PaymentsMainState.PaymentSuccess(_tiers)
command.value = PaymentsNavigation.Welcome
}
}
fun onPayButtonClicked() {
fun onPayButtonClicked(tierId: TierId) {
Timber.d("onPayButtonClicked: tierId:$tierId")
codeState.value = PaymentsCodeState.Visible.Initial(tierId = tierId)
command.value = PaymentsNavigation.Code
}
fun onDismissTier() {
Timber.d("onDismissTier")
command.value = PaymentsNavigation.Dismiss
}
fun onDismissCode() {
Timber.d("onDismissCode")
command.value = PaymentsNavigation.Dismiss
}
fun onDismissWelcome() {
Timber.d("onDismissWelcome")
command.value = PaymentsNavigation.Dismiss
}
private fun gertTiers(): List<Tier> {
return listOf(
Tier.Explorer(id = TierId("idExplorer"), isCurrent = false, validUntil = "2022-12-31"),
Tier.Builder(id = TierId("idBuilder"), isCurrent = false, validUntil = "2022-12-31"),
Tier.CoCreator(id = TierId("idCoCreator"), isCurrent = false, validUntil = "2022-12-31"),
Tier.Custom(id = TierId("idCustom"), isCurrent = false, validUntil = "2022-12-31")
)
}
}