Added a donation popup to our application.

* The donation popup will be shown to the user every three months.
* If the user clicks the "Later" button, the popup will appear again after 3 days.
* The donation popup will only be shown when there is at least one book available in the library. If no ZIM file is present, it’s not ideal to ask for a donation, as the user has not yet used the application.
* The donation popup will only be shown for custom apps when the support_url is configured. If the support menu item is hidden in the sidebar (a feature we offer), the donation popup will not be displayed, as there is no support_url available for that custom app.
This commit is contained in:
MohitMaliFtechiz 2024-09-05 19:05:21 +05:30 committed by MohitMaliFtechiz
parent 681e3a04db
commit e55deb851f
8 changed files with 285 additions and 2 deletions

View File

@ -287,7 +287,7 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
drawerContainerLayout.closeDrawer(drawerNavView)
}
private fun openSupportKiwixExternalLink() {
fun openSupportKiwixExternalLink() {
externalLinkOpener.openExternalUrl(KIWIX_SUPPORT_URL.toUri().browserIntent())
}

View File

@ -46,6 +46,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.animation.AnimationUtils
@ -133,6 +134,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchItemToOpen
import org.kiwix.kiwixmobile.core.utils.AnimationUtils.rotate
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getToolbarHeight
import org.kiwix.kiwixmobile.core.utils.DonationDialogHandler
import org.kiwix.kiwixmobile.core.utils.DonationDialogHandler.ShowDonationDialogCallback
import org.kiwix.kiwixmobile.core.utils.ExternalLinkOpener
import org.kiwix.kiwixmobile.core.utils.LanguageUtils
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.getCurrentLocale
@ -173,7 +176,8 @@ abstract class CoreReaderFragment :
FragmentActivityExtensions,
WebViewProvider,
ReadAloudCallbacks,
NavigationHistoryClickListener {
NavigationHistoryClickListener,
ShowDonationDialogCallback {
protected val webViewList: MutableList<KiwixWebView> = ArrayList()
private val webUrlsProcessor = BehaviorProcessor.create<String>()
private var fragmentReaderBinding: FragmentReaderBinding? = null
@ -227,6 +231,10 @@ abstract class CoreReaderFragment :
@Inject
var alertDialogShower: DialogShower? = null
@JvmField
@Inject
var donationDialogHandler: DonationDialogHandler? = null
@JvmField
@Inject
var painter: DarkModeViewPainter? = null
@ -297,6 +305,7 @@ abstract class CoreReaderFragment :
private var tableDrawerAdapter: TableDrawerAdapter? = null
private var tableDrawerRight: RecyclerView? = null
private var tabCallback: ItemTouchHelper.Callback? = null
private var donationLayout: FrameLayout? = null
private var bookmarkingDisposable: Disposable? = null
private var isBookmarked = false
private lateinit var serviceConnection: ServiceConnection
@ -389,6 +398,7 @@ abstract class CoreReaderFragment :
) {
super.onViewCreated(view, savedInstanceState)
setupMenu()
donationDialogHandler?.showDonationDialogCallBack(this)
val activity = requireActivity() as AppCompatActivity?
activity?.let {
WebView(it).destroy() // Workaround for buggy webViews see #710
@ -511,6 +521,7 @@ abstract class CoreReaderFragment :
tabRecyclerView = findViewById(R.id.tab_switcher_recycler_view)
snackBarRoot = findViewById(R.id.snackbar_root)
bottomToolbarToc = findViewById(R.id.bottom_toolbar_toc)
donationLayout = findViewById(R.id.donation_layout)
}
}
}
@ -1213,6 +1224,8 @@ abstract class CoreReaderFragment :
unRegisterReadAloudService()
storagePermissionForNotesLauncher?.unregister()
storagePermissionForNotesLauncher = null
donationDialogHandler?.showDonationDialogCallBack(null)
donationDialogHandler = null
}
private fun unBindViewsAndBinding() {
@ -1243,6 +1256,8 @@ abstract class CoreReaderFragment :
closeAllTabsButton = null
tableDrawerRightContainer = null
fragmentReaderBinding = null
donationLayout?.removeAllViews()
donationLayout = null
}
private fun updateTableOfContents() {
@ -1846,6 +1861,53 @@ abstract class CoreReaderFragment :
if (tts == null) {
setUpTTS()
}
donationDialogHandler?.attemptToShowDonationPopup()
}
@Suppress("InflateParams", "MagicNumber")
protected open fun showDonationLayout() {
val donationCardView = layoutInflater.inflate(R.layout.layout_donation_bottom_sheet, null)
val layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
val bottomToolBarHeight =
requireActivity()
.findViewById<BottomAppBar>(org.kiwix.kiwixmobile.core.R.id.bottom_toolbar).measuredHeight
layoutParams.setMargins(16, 0, 16, bottomToolBarHeight + 10)
donationCardView.layoutParams = layoutParams
donationLayout?.apply {
removeAllViews()
addView(donationCardView)
setDonationLayoutVisibility(VISIBLE)
}
donationCardView.findViewById<TextView>(R.id.descriptionText).apply {
text = getString(
R.string.donation_dialog_description,
(requireActivity() as CoreMainActivity).appName
)
}
val donateButton: TextView = donationCardView.findViewById(R.id.donateButton)
donateButton.setOnClickListener {
donationDialogHandler?.updateLastDonationPopupShownTime()
setDonationLayoutVisibility(GONE)
openKiwixSupportUrl()
}
val laterButton: TextView = donationCardView.findViewById(R.id.laterButton)
laterButton.setOnClickListener {
donationDialogHandler?.donateLater()
setDonationLayoutVisibility(GONE)
}
}
protected open fun openKiwixSupportUrl() {
(requireActivity() as CoreMainActivity).openSupportKiwixExternalLink()
}
private fun setDonationLayoutVisibility(visibility: Int) {
donationLayout?.visibility = visibility
}
private fun openFullScreenIfEnabled() {
@ -2388,6 +2450,10 @@ abstract class CoreReaderFragment :
unbindService()
}
override fun showDonationDialog() {
showDonationLayout()
}
private fun bindService() {
requireActivity().bindService(
Intent(requireActivity(), ReadAloudService::class.java), serviceConnection,

View File

@ -0,0 +1,82 @@
/*
* Kiwix Android
* Copyright (c) 2024 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.utils
import android.app.Activity
import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp
import javax.inject.Inject
const val THREE_DAYS_IN_MILLISECONDS = 3 * 24 * 60 * 60 * 1000L
const val THREE_MONTHS_IN_MILLISECONDS = 90 * 24 * 60 * 60 * 1000L
class DonationDialogHandler @Inject constructor(
private val activity: Activity,
private val sharedPreferenceUtil: SharedPreferenceUtil,
private val newBookDao: NewBookDao
) {
private var showDonationDialogCallback: ShowDonationDialogCallback? = null
fun showDonationDialogCallBack(showDonationDialogCallback: ShowDonationDialogCallback?) {
this.showDonationDialogCallback = showDonationDialogCallback
}
fun attemptToShowDonationPopup() {
val currentMilliSeconds = System.currentTimeMillis()
val lastPopupMillis = sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds
val timeDifference = currentMilliSeconds - lastPopupMillis
if (shouldShowInitialPopup(lastPopupMillis) || timeDifference >= THREE_MONTHS_IN_MILLISECONDS) {
if (isZimFilesAvailableInLibrary() && isTimeToShowDonation(currentMilliSeconds)) {
showDonationDialogCallback?.showDonationDialog()
resetDonateLater()
}
}
}
private fun shouldShowInitialPopup(lastPopupMillis: Long): Boolean = lastPopupMillis == 0L
private fun isTimeToShowDonation(currentMillis: Long): Boolean =
isLaterNotClicked() || isLaterPeriodOver(currentMillis)
private fun isLaterNotClicked(): Boolean = sharedPreferenceUtil.laterClickedMilliSeconds == 0L
private fun isLaterPeriodOver(currentMillis: Long): Boolean {
val timeDifference = currentMillis - sharedPreferenceUtil.laterClickedMilliSeconds
return timeDifference >= THREE_DAYS_IN_MILLISECONDS
}
private fun isZimFilesAvailableInLibrary(): Boolean =
if (activity.isCustomApp()) true else newBookDao.getBooks().isNotEmpty()
fun updateLastDonationPopupShownTime() {
sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds = System.currentTimeMillis()
}
fun donateLater() {
sharedPreferenceUtil.laterClickedMilliSeconds = System.currentTimeMillis()
}
private fun resetDonateLater() {
sharedPreferenceUtil.laterClickedMilliSeconds = 0L
}
interface ShowDonationDialogCallback {
fun showDonationDialog()
}
}

View File

@ -271,6 +271,22 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
}
}
var lastDonationPopupShownInMilliSeconds: Long
get() = sharedPreferences.getLong(PREF_LAST_DONATION_POPUP_SHOWN_IN_MILLISECONDS, 0L)
set(value) {
sharedPreferences.edit {
putLong(PREF_LAST_DONATION_POPUP_SHOWN_IN_MILLISECONDS, value)
}
}
var laterClickedMilliSeconds: Long
get() = sharedPreferences.getLong(PREF_LATER_CLICKED_MILLIS, 0L)
set(value) {
sharedPreferences.edit {
putLong(PREF_LATER_CLICKED_MILLIS, value)
}
}
fun getPublicDirectoryPath(path: String): String =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
path
@ -321,5 +337,8 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
const val PREF_NOTES_MIGRATED = "pref_notes_migrated"
const val PREF_APP_DIRECTORY_TO_PUBLIC_MIGRATED = "pref_app_directory_to_public_migrated"
const val PREF_COPY_MOVE_PERMISSION = "pref_copy_move_permission"
private const val PREF_LATER_CLICKED_MILLIS = "pref_later_clicked_millis"
private const val PREF_LAST_DONATION_POPUP_SHOWN_IN_MILLISECONDS =
"pref_last_donation_shown_in_milliseconds"
}
}

View File

@ -57,6 +57,14 @@
android:layout_height="match_parent"
android:background="@android:color/black"
android:visibility="invisible" />
<FrameLayout
android:id="@+id/donation_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="@dimen/activity_horizontal_margin" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Kiwix Android
~ Copyright (c) 2024 Kiwix <android.kiwix.org>
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
~
-->
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:padding="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="6dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/heart_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_margin="@dimen/activity_horizontal_margin"
android:layout_marginEnd="12dp"
android:contentDescription="@string/donation_dialog_title"
android:src="@drawable/ic_support_24px"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/titleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/fullscreen_control_button_margin"
android:text="@string/donation_dialog_title"
android:textSize="16sp"
android:textAppearance="@style/TextAppearance.M3.Sys.Typescale.TitleMedium"
android:textColor="@color/mine_shaft_gray900"
app:layout_constraintStart_toEndOf="@id/heart_icon"
app:layout_constraintTop_toTopOf="@+id/heart_icon" />
<TextView
android:id="@+id/descriptionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/donation_dialog_description"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="@id/titleText"
app:layout_constraintTop_toBottomOf="@+id/titleText" />
<TextView
android:id="@+id/laterButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/activity_horizontal_margin"
android:text="@string/rate_dialog_neutral"
android:textColor="@color/denim_blue800"
app:layout_constraintEnd_toStartOf="@+id/donateButton"
app:layout_constraintTop_toBottomOf="@id/descriptionText" />
<TextView
android:id="@+id/donateButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/activity_horizontal_margin"
android:text="@string/make_donation"
android:textColor="@color/denim_blue800"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/descriptionText" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -389,4 +389,7 @@
<string name="toolbar_back_button_content_description">Go to previous screen</string>
<string name="save_or_open_unsupported_files_dialog_title">Save or Open this file?</string>
<string name="save_or_open_unsupported_files_dialog_message">Choosing Open will open this file in external reader application.</string>
<string name="donation_dialog_title">Donate Today</string>
<string name="donation_dialog_description">%s needs your help.</string>
<string name="make_donation">Make a donation</string>
</resources>

View File

@ -28,10 +28,12 @@ import android.view.View.VISIBLE
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.net.toUri
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.fragment.findNavController
import org.kiwix.kiwixmobile.core.R.dimen
import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.extensions.browserIntent
import org.kiwix.kiwixmobile.core.extensions.getResizedDrawable
import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.main.CoreReaderFragment
@ -328,6 +330,22 @@ class CustomReaderFragment : CoreReaderFragment() {
newMainPageTab()
}
/**
* Overrides the method to show the donation popup. When the "Support url" is disabled
* in a custom app, this function stop to show the donationPopup.
*/
override fun showDonationLayout() {
if (BuildConfig.SUPPORT_URL.isNotEmpty()) {
super.showDonationLayout()
}
}
override fun openKiwixSupportUrl() {
if (BuildConfig.SUPPORT_URL.isNotEmpty()) {
openExternalUrl(BuildConfig.SUPPORT_URL.toUri().browserIntent())
}
}
override fun onDestroyView() {
super.onDestroyView()
permissionRequiredDialog = null