Beg for subscriptions/donations periodically

pull/1823/head
Alex Baker 2 years ago
parent ed48ab15e3
commit d5ccc1aa8f

@ -66,6 +66,9 @@ class Firebase @Inject constructor(
val reviewCooldown: Boolean
get() = preferences.lastReviewRequest + days("review_cooldown", 30L) > now()
val subscribeCooldown: Boolean
get() = preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > now()
private fun days(key: String, default: Long): Long =
TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default)

@ -9,4 +9,8 @@
<key>install_cooldown</key>
<value>14</value>
</entry>
<entry>
<key>subscribe_cooldown</key>
<value>30</value>
</entry>
</defaultsMap>

@ -13,9 +13,7 @@ import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment
import com.todoroo.astrid.activity.TaskListFragment.TaskListFragmentCallbackHandler
@ -28,13 +26,15 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.data.AlarmDao
import org.tasks.data.LocationDao
@ -61,6 +61,8 @@ import org.tasks.ui.MainActivityEvent
import org.tasks.ui.MainActivityEventBus
import org.tasks.ui.NavigationDrawerFragment
import org.tasks.ui.NavigationDrawerFragment.Companion.newNavigationDrawer
import org.tasks.ui.TaskListEvent
import org.tasks.ui.TaskListEventBus
import timber.log.Timber
import javax.inject.Inject
@ -78,7 +80,9 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var eventBus: MainActivityEventBus
@Inject lateinit var taskListEventBus: TaskListEventBus
@Inject lateinit var playServices: PlayServices
@Inject lateinit var firebase: Firebase
private var currentNightMode = 0
private var currentPro = false
@ -101,11 +105,9 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
}
handleIntent()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
eventBus.collect(this@MainActivity::process)
}
}
eventBus
.onEach(this::process)
.launchIn(lifecycleScope)
}
private suspend fun process(event: MainActivityEvent) = when (event) {
@ -477,6 +479,16 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
taskEditFragment!!.onRemoteListChanged(filter)
}
override fun onStart() {
super.onStart()
lifecycleScope.launch {
if (!inventory.hasPro && !firebase.subscribeCooldown) {
taskListEventBus.tryEmit(TaskListEvent.BegForSubscription)
}
}
}
companion object {
/** For indicating the new list screen should be launched at fragment setup time */
const val TOKEN_CREATE_NEW_LIST_NAME = "newListName" // $NON-NLS-1$

@ -23,6 +23,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat
import androidx.core.view.forEach
@ -30,9 +33,7 @@ import androidx.core.view.isVisible
import androidx.core.view.setMargins
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.paging.PagedList
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
@ -41,9 +42,11 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.composethemeadapter.MdcTheme
import com.google.android.material.snackbar.Snackbar
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE
@ -69,12 +72,14 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.ShortcutManager
import org.tasks.Tasks.Companion.IS_GOOGLE_PLAY
import org.tasks.activities.FilterSettingsActivity
import org.tasks.activities.GoogleTaskListSettingsActivity
import org.tasks.activities.ListPicker
@ -82,7 +87,9 @@ import org.tasks.activities.ListPicker.Companion.newListPicker
import org.tasks.activities.PlaceSettingsActivity
import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase
import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.AnimatedBanner
import org.tasks.data.CaldavDao
import org.tasks.data.TagDataDao
import org.tasks.data.TaskContainer
@ -169,6 +176,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
private lateinit var callbacks: TaskListFragmentCallbackHandler
private lateinit var binding: FragmentTaskListBinding
@OptIn(ExperimentalAnimationApi::class)
private fun process(event: TaskListEvent) = when (event) {
is TaskListEvent.TaskCreated ->
onTaskCreated(event.uuid)
@ -176,6 +184,33 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
makeSnackbar(R.string.calendar_event_created, event.title)
?.setAction(R.string.action_open) { context?.openUri(event.uri) }
?.show()
is TaskListEvent.BegForSubscription -> {
binding.banner.setContent {
val showBanner = rememberSaveable { mutableStateOf(true) }
MdcTheme {
AnimatedBanner(
isVisible = showBanner,
dismiss = {
preferences.lastSubscribeRequest = now()
showBanner.value = false
},
subscribe = {
purchase()
showBanner.value = false
},
)
}
}
}
}
private fun purchase() {
if (IS_GOOGLE_PLAY) {
startActivity(Intent(context, PurchaseActivity::class.java))
} else {
preferences.lastSubscribeRequest = now()
context?.openUri(R.string.url_donate)
}
}
override fun onRefresh() {
@ -217,8 +252,16 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
outState.putLongArray(EXTRA_COLLAPSED, taskAdapter.getCollapsed().toLongArray())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
taskListEventBus
.onEach(this::process)
.launchIn(viewLifecycleOwner.lifecycleScope)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentTaskListBinding.inflate(inflater, container, false)
with (binding) {
swipeRefreshLayout = bodyStandard.swipeLayout
@ -283,12 +326,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
toolbar.setNavigationOnClickListener { callbacks.onNavigationIconClicked() }
setupMenu(toolbar)
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
taskListEventBus.collect(this@TaskListFragment::process)
}
}
return binding.root
}

@ -114,6 +114,7 @@ class Upgrader @Inject constructor(
} else {
setInstallDetails(to)
}
preferences.lastSubscribeRequest = 0L
preferences.setCurrentVersion(to)
}

@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
@ -16,6 +17,7 @@ import org.tasks.Tasks.Companion.IS_GENERIC
import org.tasks.compose.PurchaseText.PurchaseText
import org.tasks.extensions.Context.toast
import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.preferences.Preferences
import org.tasks.themes.Theme
import javax.inject.Inject
@ -25,6 +27,7 @@ class PurchaseActivity : InjectingAppCompatActivity(), OnPurchasesUpdated {
@Inject lateinit var billingClient: BillingClient
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var inventory: Inventory
@Inject lateinit var preferences: Preferences
private var currentSubscription: Purchase? = null
private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() {
@ -38,6 +41,8 @@ class PurchaseActivity : InjectingAppCompatActivity(), OnPurchasesUpdated {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
preferences.lastSubscribeRequest = DateUtilities.now()
val github = intent?.extras?.getBoolean(EXTRA_GITHUB) ?: false
theme.applyToContext(this)

@ -0,0 +1,108 @@
package org.tasks.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import org.tasks.R
import org.tasks.Tasks.Companion.IS_GENERIC
@ExperimentalAnimationApi
@Composable
fun AnimatedBanner(
isVisible: MutableState<Boolean>,
dismiss: () -> Unit,
subscribe: () -> Unit,
) {
var show by rememberSaveable { mutableStateOf(false) }
if (isVisible.value) {
LaunchedEffect(key1 = isVisible, block = {
delay(500)
show = true
})
} else {
show = false
}
AnimatedVisibility(
visible = show,
enter = expandVertically(
expandFrom = Alignment.Top
),
exit = shrinkVertically(),
) {
Column(
modifier = Modifier
.fillMaxWidth(),
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(
id = if (IS_GENERIC) {
R.string.enjoying_tasks
} else {
R.string.tasks_needs_your_support
}
),
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(
id = if (IS_GENERIC) {
R.string.tasks_needs_your_support
} else {
R.string.support_development_subscribe
}
),
style = MaterialTheme.typography.body2,
modifier = Modifier.padding(horizontal = 16.dp),
)
Row(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp)
.align(Alignment.End),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(onClick = dismiss) {
Text(text = stringResource(id = R.string.dismiss))
}
TextButton(onClick = subscribe) {
Text(
text = stringResource(
id = if (IS_GENERIC) {
R.string.TLA_menu_donate
} else {
R.string.button_subscribe
}
)
)
}
}
Divider()
}
}
}

@ -536,6 +536,10 @@ class Preferences @JvmOverloads constructor(
get() = getLong(R.string.p_last_review_request, 0L)
set(value) = setLong(R.string.p_last_review_request, value)
var lastSubscribeRequest: Long
get() = getLong(R.string.p_last_subscribe_request, 0L)
set(value) = setLong(R.string.p_last_subscribe_request, value)
companion object {
private const val PREF_SORT_SORT = "sort_sort" // $NON-NLS-1$

@ -7,4 +7,5 @@ typealias TaskListEventBus = MutableSharedFlow<TaskListEvent>
sealed interface TaskListEvent {
data class TaskCreated(val uuid: String) : TaskListEvent
data class CalendarEventCreated(val title: String?, val uri: String) : TaskListEvent
object BegForSubscription : TaskListEvent
}

@ -38,6 +38,10 @@
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"

@ -411,6 +411,7 @@
<string name="p_show_whats_new">show_whats_new</string>
<string name="p_just_updated">just_updated</string>
<string name="p_last_review_request">last_review_request</string>
<string name="p_last_subscribe_request">last_subscribe_request</string>
<string name="p_backups_enabled">backups_enabled</string>
<string name="p_backups_ignore_warnings">backups_ignore_warnings</string>
<string name="p_backups_android_backup_enabled">backups_android_backup_enabled</string>

@ -733,4 +733,5 @@ File %1$s contained %2$s.\n\n
<string name="caldav_server_other">Other</string>
<string name="caldav_server_type">Server type</string>
<string name="filter_snoozed">Snoozed</string>
<string name="dismiss">Dismiss</string>
</resources>

Loading…
Cancel
Save