Update subscription screen to fix rejection

pull/3054/head
Alex Baker 1 year ago
parent a289cb80fd
commit 5fa00ea53d

@ -22,7 +22,6 @@ class Firebase @Inject constructor(
@param:ApplicationContext val context: Context,
private val preferences: Preferences
) {
private var crashlytics: FirebaseCrashlytics? = null
private var analytics: FirebaseAnalytics? = null
private var remoteConfig: FirebaseRemoteConfig? = null
@ -76,11 +75,8 @@ class Firebase @Inject constructor(
get() = installCooldown
|| preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > currentTimeMillis()
val moreOptionsBadge: Boolean
get() = remoteConfig?.getBoolean("more_options_badge") ?: false
val moreOptionsSolid: Boolean
get() = remoteConfig?.getBoolean("more_options_solid") ?: false
val subGroupA: Boolean
get() = remoteConfig?.getBoolean("sub_group_a") ?: false
private fun days(key: String, default: Long): Long =
TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default)

@ -302,7 +302,7 @@
<activity
android:name=".billing.PurchaseActivity"
android:theme="@style/TranslucentDialog" />
android:theme="@style/Tasks" />
<!-- ======================================================= Receivers = -->

@ -4,17 +4,18 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.compose.PurchaseText.PurchaseText
import org.tasks.compose.PurchaseText.SubscriptionScreen
import org.tasks.extensions.Context.toast
import org.tasks.preferences.Preferences
import org.tasks.themes.TasksTheme
@ -56,16 +57,19 @@ class PurchaseActivity : AppCompatActivity(), OnPurchasesUpdated {
setContent {
TasksTheme {
Dialog(onDismissRequest = { finish() }) {
PurchaseText(
BackHandler {
finish()
}
SubscriptionScreen(
nameYourPrice = nameYourPrice,
sliderPosition = sliderPosition,
github = github,
solidButton = firebase.moreOptionsSolid,
badge = firebase.moreOptionsBadge,
onDisplayed = { firebase.logEvent(R.string.event_showed_purchase_dialog) },
hideText = firebase.subGroupA,
subscribe = this::purchase,
onBack = { finish() },
)
LaunchedEffect(key1 = Unit) {
firebase.logEvent(R.string.event_showed_purchase_dialog)
}
}
}

@ -1,201 +0,0 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tasks.compose
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.unit.Density
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
/**
* This is a modified version of:
* https://gist.github.com/adamp/07d468f4bcfe632670f305ce3734f511
*/
class PagerState(
currentPage: Int = 0,
minPage: Int = 0,
maxPage: Int = 0
) {
private var _minPage by mutableStateOf(minPage)
var minPage: Int
get() = _minPage
set(value) {
_minPage = value.coerceAtMost(_maxPage)
_currentPage = _currentPage.coerceIn(_minPage, _maxPage)
}
private var _maxPage by mutableStateOf(maxPage, structuralEqualityPolicy())
var maxPage: Int
get() = _maxPage
set(value) {
_maxPage = value.coerceAtLeast(_minPage)
_currentPage = _currentPage.coerceIn(_minPage, maxPage)
}
private var _currentPage by mutableStateOf(currentPage.coerceIn(minPage, maxPage))
var currentPage: Int
get() = _currentPage
set(value) {
_currentPage = value.coerceIn(minPage, maxPage)
}
enum class SelectionState { Selected, Undecided }
var selectionState by mutableStateOf(SelectionState.Selected)
private suspend fun selectPage() {
currentPage -= currentPageOffset.roundToInt()
snapToOffset(0f)
selectionState = SelectionState.Selected
}
private var _currentPageOffset = Animatable(0f).apply {
updateBounds(-1f, 1f)
}
val currentPageOffset: Float
get() = _currentPageOffset.value
suspend fun snapToOffset(offset: Float) {
val max = if (currentPage == minPage) 0f else 1f
val min = if (currentPage == maxPage) 0f else -1f
_currentPageOffset.snapTo(offset.coerceIn(min, max))
}
suspend fun fling(velocity: Float) {
if (velocity < 0 && currentPage == maxPage) return
if (velocity > 0 && currentPage == minPage) return
_currentPageOffset.animateTo(currentPageOffset.roundToInt().toFloat())
selectPage()
}
override fun toString(): String = "PagerState{minPage=$minPage, maxPage=$maxPage, " +
"currentPage=$currentPage, currentPageOffset=$currentPageOffset}"
}
@Immutable
private data class PageData(val page: Int) : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any = this@PageData
}
private val Measurable.page: Int
get() = (parentData as? PageData)?.page ?: error("no PageData for measurable $this")
@Composable
fun Pager(
state: PagerState,
modifier: Modifier = Modifier,
offscreenLimit: Int = 2,
pageContent: @Composable PagerScope.() -> Unit
) {
var pageSize by remember { mutableStateOf(0) }
val coroutineScope = rememberCoroutineScope()
Layout(
content = {
val minPage = (state.currentPage - offscreenLimit).coerceAtLeast(state.minPage)
val maxPage = (state.currentPage + offscreenLimit).coerceAtMost(state.maxPage)
for (page in minPage..maxPage) {
val pageData = PageData(page)
val scope = PagerScope(page)
key(pageData) {
Box(contentAlignment = Alignment.Center, modifier = pageData) {
scope.pageContent()
}
}
}
},
modifier = modifier.draggable(
orientation = Orientation.Horizontal,
onDragStarted = {
state.selectionState = PagerState.SelectionState.Undecided
},
onDragStopped = { velocity ->
coroutineScope.launch {
// Velocity is in pixels per second, but we deal in percentage offsets, so we
// need to scale the velocity to match
state.fling(velocity / pageSize)
}
},
state = rememberDraggableState { dy ->
coroutineScope.launch {
with(state) {
val pos = pageSize * currentPageOffset
val max = if (currentPage == minPage) 0 else pageSize * offscreenLimit
val min = if (currentPage == maxPage) 0 else -pageSize * offscreenLimit
val newPos = (pos + dy).coerceIn(min.toFloat(), max.toFloat())
snapToOffset(newPos / pageSize)
}
}
},
)
) { measurables, constraints ->
layout(constraints.maxWidth, constraints.maxHeight) {
val currentPage = state.currentPage
val offset = state.currentPageOffset
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
measurables
.map {
it.measure(childConstraints) to it.page
}
.forEach { (placeable, page) ->
// TODO: current this centers each page. We should investigate reading
// gravity modifiers on the child, or maybe as a param to Pager.
val xCenterOffset = (constraints.maxWidth - placeable.width) / 2
val yCenterOffset = (constraints.maxHeight - placeable.height) / 2
if (currentPage == page) {
pageSize = placeable.width
}
val xItemOffset = ((page + offset - currentPage) * placeable.width).roundToInt()
placeable.place(
x = xCenterOffset + xItemOffset,
y = yCenterOffset
)
}
}
}
}
/**
* Scope for [Pager] content.
*/
class PagerScope(
val page: Int
)

@ -8,31 +8,43 @@ 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.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.ColorFilter
import androidx.compose.ui.platform.LocalContext
@ -45,10 +57,11 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.compose.Constants.HALF_KEYLINE
import org.tasks.compose.Constants.KEYLINE_FIRST
import org.tasks.compose.PurchaseText.PurchaseText
import org.tasks.compose.PurchaseText.SubscriptionScreen
import org.tasks.extensions.Context.openUri
import org.tasks.themes.TasksTheme
@ -59,7 +72,7 @@ object PurchaseText {
val title: Int,
val icon: Int,
val description: Int,
val tint: Boolean = true
val tint: Boolean = true,
)
private val featureList = listOf(
@ -121,36 +134,80 @@ object PurchaseText {
)
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PurchaseText(
fun SubscriptionScreen(
nameYourPrice: MutableState<Boolean> = mutableStateOf(false),
sliderPosition: MutableState<Float> = mutableStateOf(0f),
github: Boolean = false,
solidButton: Boolean = false,
badge: Boolean = false,
onDisplayed: () -> Unit = {},
subscribe: (Int, Boolean) -> Unit
hideText: Boolean,
subscribe: (Int, Boolean) -> Unit,
onBack: () -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.upgrade_to_pro),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge
)
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = null
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.background
)
)
},
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.background(color = colorResource(R.color.content_background)),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (!hideText) {
GreetingText(R.string.upgrade_blurb_1)
GreetingText(R.string.upgrade_blurb_2)
}
Spacer(Modifier.height(KEYLINE_FIRST))
val pagerState = remember {
PagerState(maxPage = (featureList.size - 1).coerceAtLeast(0))
val pagerState = rememberPagerState {
featureList.size
}
Pager(
state = pagerState,
modifier = Modifier
HorizontalPager(
state = pagerState // Optional: to control the pager's state
) { index ->
val item = featureList[index]
PagerItem(item, nameYourPrice.value && index == 0)
}
Row(
Modifier
.wrapContentHeight()
.fillMaxWidth()
.height(200.dp)
.align(Alignment.CenterHorizontally)
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.Center
) {
PagerItem(featureList[page], nameYourPrice.value && page == 0)
repeat(pagerState.pageCount) { iteration ->
val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
Box(
modifier = Modifier
.padding(2.dp)
.clip(CircleShape)
.background(color)
.size(16.dp)
)
}
}
if (github) {
SponsorButton()
@ -160,13 +217,9 @@ object PurchaseText {
sliderPosition = sliderPosition,
pagerState = pagerState,
subscribe = subscribe,
solidButton = solidButton,
badge = badge,
)
}
}
LaunchedEffect(key1 = Unit) {
onDisplayed()
}
}
@ -212,48 +265,36 @@ object PurchaseText {
sliderPosition: MutableState<Float>,
pagerState: PagerState,
subscribe: (Int, Boolean) -> Unit,
solidButton: Boolean,
badge: Boolean,
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Divider(color = MaterialTheme.colorScheme.onSurface, thickness = 0.25.dp)
Spacer(Modifier.height(KEYLINE_FIRST))
HorizontalDivider(modifier = Modifier.padding(vertical = KEYLINE_FIRST))
if (nameYourPrice.value) {
NameYourPrice(sliderPosition, subscribe)
} else {
TasksAccount(subscribe)
}
Spacer(Modifier.height(KEYLINE_FIRST))
val scope = rememberCoroutineScope()
OutlinedButton(
onClick = {
nameYourPrice.value = !nameYourPrice.value
pagerState.currentPage = 0
scope.launch {
pagerState.animateScrollToPage(0)
}
},
colors = ButtonDefaults.textButtonColors(
containerColor = if (solidButton)
MaterialTheme.colorScheme.primary
else
Color.Transparent
containerColor = Color.Transparent
)
) {
BadgedBox(badge = {
if (!nameYourPrice.value && badge) {
Badge()
}
}) {
Text(
text = stringResource(R.string.more_options),
color = if (solidButton)
MaterialTheme.colorScheme.onSecondary
else
MaterialTheme.colorScheme.onSurface,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge
)
}
}
Text(
text = stringResource(R.string.pro_free_trial),
style = MaterialTheme.typography.bodySmall,
@ -269,17 +310,16 @@ object PurchaseText {
@Composable
fun PagerItem(
feature: CarouselItem,
disabled: Boolean = false
disabled: Boolean = false,
) {
Column {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(.5f),
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.width(250.dp)
.height(150.dp)
.padding(HALF_KEYLINE),
horizontalAlignment = Alignment.CenterHorizontally
) {
@ -321,7 +361,6 @@ object PurchaseText {
}
}
}
}
@Composable
fun TasksAccount(subscribe: (Int, Boolean) -> Unit) {
@ -355,7 +394,7 @@ object PurchaseText {
price: Int,
monthly: Boolean = false,
popperText: String = "",
onClick: (Int, Boolean) -> Unit
onClick: (Int, Boolean) -> Unit,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(
@ -434,24 +473,23 @@ object PurchaseText {
@Composable
private fun PurchaseDialogPreview() {
TasksTheme {
PurchaseText { _, _ -> }
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun PurchaseDialogPreviewSolid() {
TasksTheme {
PurchaseText(solidButton = true) { _, _ -> }
SubscriptionScreen(
hideText = false,
subscribe = { _, _ -> },
onBack = {},
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun PurchaseDialogPreviewBadge() {
private fun PurchaseDialogPreviewNoText() {
TasksTheme {
PurchaseText(badge = true) { _, _ -> }
SubscriptionScreen(
hideText = true,
subscribe = { _, _ -> },
onBack = {},
)
}
}

Loading…
Cancel
Save