Show price

This commit is contained in:
Lucas Lima 2020-12-16 16:43:01 -03:00
parent 4edb1b0a61
commit dd0cc81a42
No known key found for this signature in database
GPG key ID: 049CCC5A365B00D2
14 changed files with 124 additions and 50 deletions

View file

@ -10,6 +10,7 @@ import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
@ -68,6 +69,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.singleOrNull
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.lang.Exception
@ -121,6 +123,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
bindNavigationMenu()
bindSwitchControlButton()
bindAds()
bindPrice()
findViewById<FrameLayout>(R.id.levelContainer).doOnLayout {
if (!isFinishing) {
@ -488,14 +491,11 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
findItem(R.id.share_now).isVisible = isNotInstant
findItem(R.id.remove_ads).isVisible = !preferencesRepository.isPremiumEnabled() && isNotInstant
findItem(R.id.previous_games).isVisible = featureFlagManager.isGameHistoryEnabled()
findItem(R.id.rate).isVisible = featureFlagManager.isRateUsEnabled()
if (!playGamesManager.hasGooglePlayGames()) {
removeGroup(R.id.play_games_group)
}
// New Features
findItem(R.id.themes).setActionView(R.layout.new_icon)
findItem(R.id.tutorial).setActionView(R.layout.new_icon)
}
}
@ -899,6 +899,36 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
}
private fun bindPrice() {
if (billingManager.isEnabled() && !preferencesRepository.isPremiumEnabled()) {
billingManager.start()
lifecycleScope.launchWhenResumed {
billingManager.getPrice().collect { price ->
if (price.isNotBlank()) {
try {
navigationView.menu.findItem(R.id.remove_ads).apply {
actionView = TextView(baseContext).apply {
text = price
gravity = Gravity.CENTER_VERTICAL
setTextColor(ContextCompat.getColor(context, R.color.mines_around_2))
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
).apply {
gravity = Gravity.CENTER
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Fail to create price text")
}
}
}
}
}
}
private fun refreshAds() {
val isTutorialComplete = preferencesRepository.isTutorialCompleted()
if (isTutorialComplete && !preferencesRepository.isPremiumEnabled() && billingManager.isEnabled()) {
@ -946,15 +976,19 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null &&
!instantAppManager.isEnabled(this)
) {
lifecycleScope.launch {
if (billingManager.isEnabled()) {
SupportAppDialogFragment.newRemoveAdsSupportDialog()
.show(supportFragmentManager, SupportAppDialogFragment.TAG)
SupportAppDialogFragment.newRemoveAdsSupportDialog(
applicationContext,
billingManager.getPrice().singleOrNull()
).show(supportFragmentManager, SupportAppDialogFragment.TAG)
} else {
SupportAppDialogFragment.newRequestSupportDialog()
SupportAppDialogFragment.newRequestSupportDialog(applicationContext)
.show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

View file

@ -53,7 +53,7 @@ class AboutActivity : ThematicActivity(R.layout.activity_empty) {
private fun showSupportAppDialog() {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) {
SupportAppDialogFragment.newRequestSupportDialog()
SupportAppDialogFragment.newRequestSupportDialog(applicationContext)
.show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}

View file

@ -10,6 +10,7 @@ import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.about.viewmodel.AboutEvent
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.external.IBillingManager
import kotlinx.android.synthetic.main.fragment_about_info.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@ -17,6 +18,7 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class AboutInfoFragment : Fragment(R.layout.fragment_about_info) {
private val aboutViewModel: AboutViewModel by sharedViewModel()
private val preferencesRepository: IPreferencesRepository by inject()
private val billingManager: IBillingManager by inject()
override fun onResume() {
super.onResume()
@ -38,12 +40,12 @@ class AboutInfoFragment : Fragment(R.layout.fragment_about_info) {
}
}
if (preferencesRepository.showSupport()) {
if (preferencesRepository.isPremiumEnabled() && billingManager.isEnabled()) {
supportUs.visibility = View.GONE
} else {
supportUs.setOnClickListener {
aboutViewModel.sendEvent(AboutEvent.SupportUs)
}
} else {
supportUs.visibility = View.GONE
}
thirdsParties.setOnClickListener {

View file

@ -2,6 +2,7 @@ package dev.lucasnlm.antimine.splash.viewmodel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.lucasnlm.antimine.common.level.database.models.Stats
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.core.control.ControlStyle
@ -9,6 +10,7 @@ import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.support.IapHandler
import dev.lucasnlm.external.ICloudStorageManager
import dev.lucasnlm.external.model.CloudSave
import kotlinx.coroutines.launch
class SplashViewModel(
private val preferencesRepository: IPreferencesRepository,
@ -17,8 +19,10 @@ class SplashViewModel(
private val iapHandler: IapHandler,
) : ViewModel() {
fun startIap() {
viewModelScope.launch {
iapHandler.start()
}
}
suspend fun migrateCloudSave(playGamesId: String) {
saveCloudStorageManager.getSave(playGamesId)?.let { cloudSave ->

View file

@ -7,9 +7,7 @@ import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.external.IBillingManager
import dev.lucasnlm.external.model.PurchaseInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class IapHandler(
@ -17,8 +15,7 @@ class IapHandler(
private val preferencesManager: IPreferencesRepository,
private val billingManager: IBillingManager,
) {
fun start() {
GlobalScope.launch {
suspend fun start() {
billingManager.listenPurchases().collect {
if (it is PurchaseInfo.PurchaseResult) {
onLockStatusChanged(it.unlockStatus)
@ -27,7 +24,6 @@ class IapHandler(
}
}
}
}
private fun onLockStatusChanged(status: Boolean) {
preferencesManager.setPremiumFeatures(status)

View file

@ -1,6 +1,8 @@
package dev.lucasnlm.antimine.support
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
@ -27,7 +29,7 @@ class SupportAppDialogFragment : AppCompatDialogFragment() {
private val adsManager: IAdsManager by inject()
private val instantAppManager: InstantAppManager by inject()
private var unlockMessage: Int = R.string.support_action
private lateinit var unlockMessage: String
private var targetThemeId: Long = -1L
private var isInstantMode: Boolean = true
@ -40,11 +42,13 @@ class SupportAppDialogFragment : AppCompatDialogFragment() {
analyticsManager.sentEvent(Analytics.ShowIapDialog)
unlockMessage =
(arguments?.getInt(UNLOCK_LABEL) ?: savedInstanceState?.getInt(UNLOCK_LABEL)) ?: R.string.support_action
(arguments?.getString(UNLOCK_LABEL) ?: savedInstanceState?.getString(UNLOCK_LABEL))
?: getString(R.string.support_action)
targetThemeId =
(arguments?.getLong(TARGET_THEME_ID, -1L)) ?: -1L
}
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).apply {
val view = LayoutInflater
@ -148,26 +152,30 @@ class SupportAppDialogFragment : AppCompatDialogFragment() {
private const val UNLOCK_LABEL = "support_unlock_label"
private const val TARGET_THEME_ID = "target_theme_id"
fun newRequestSupportDialog(): SupportAppDialogFragment {
fun newRequestSupportDialog(context: Context): SupportAppDialogFragment {
return SupportAppDialogFragment().apply {
arguments = Bundle().apply {
putInt(UNLOCK_LABEL, R.string.support_action)
putString(UNLOCK_LABEL, context.getString(R.string.support_action))
}
}
}
fun newRemoveAdsSupportDialog(): SupportAppDialogFragment {
fun newRemoveAdsSupportDialog(context: Context, price: String?): SupportAppDialogFragment {
return SupportAppDialogFragment().apply {
val label = context.getString(R.string.remove_ad)
val unlockLabel = price?.let { "$label - $it" } ?: label
arguments = Bundle().apply {
putInt(UNLOCK_LABEL, R.string.remove_ad)
putString(UNLOCK_LABEL, unlockLabel)
}
}
}
fun newChangeThemeDialog(targetThemeId: Long): SupportAppDialogFragment {
fun newChangeThemeDialog(context: Context, targetThemeId: Long, price: String?): SupportAppDialogFragment {
return SupportAppDialogFragment().apply {
val label = context.getString(R.string.unlock_all)
val unlockLabel = price?.let { "$label - $it" } ?: label
arguments = Bundle().apply {
putInt(UNLOCK_LABEL, R.string.unlock_all)
putString(UNLOCK_LABEL, unlockLabel)
putLong(TARGET_THEME_ID, targetThemeId)
}
}

View file

@ -15,8 +15,11 @@ import dev.lucasnlm.antimine.support.SupportAppDialogFragment
import dev.lucasnlm.antimine.theme.view.ThemeAdapter
import dev.lucasnlm.antimine.theme.viewmodel.ThemeEvent
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
import dev.lucasnlm.external.IBillingManager
import kotlinx.android.synthetic.main.activity_game.*
import kotlinx.android.synthetic.main.activity_theme.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@ -30,6 +33,8 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
private val preferencesRepository: IPreferencesRepository by inject()
private val billingManager: IBillingManager by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated {
@ -90,9 +95,15 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
private fun showUnlockDialog(themeId: Long) {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) {
SupportAppDialogFragment.newChangeThemeDialog(themeId).apply {
lifecycleScope.launch {
SupportAppDialogFragment.newChangeThemeDialog(
applicationContext,
themeId,
billingManager.getPrice().singleOrNull()
).apply {
show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}
}
}
}

View file

@ -4,7 +4,7 @@
<string name="tutorial_completed">Tutorial Concluído</string>
<string name="tutorial_0_bottom">Para começar dê um %1$s em qualquer quadrado aleatório.</string>
<string name="tutorial_1_top">Os números indicam quantas minas existem adjacentes ao quadrado destacado.</string>
<string name="tutorial_1_bottom">Portanto, se o canto 1⃣ tem apenas um quadrado adjacente, ele deve ter uma minha. Use %1$s para sinalizá-lo.</string>
<string name="tutorial_1_bottom">Portanto, se o canto 1⃣ tem apenas um quadrado adjacente, ele deve ter uma mina. Use %1$s para sinalizá-lo.</string>
<string name="tutorial_2_top">O próximo quadrado fechado não tem uma mina porque o número é 1⃣ , não 2⃣ .</string>
<string name="tutorial_2_bottom">Você pode abri-lo com %1$s.</string>
<string name="tutorial_3_top">O mesmo acontece nesse 1⃣ . Você sabe onde está a mina adjacente.</string>

View file

@ -8,5 +8,6 @@ interface IBillingManager {
fun start()
fun isEnabled(): Boolean
suspend fun charge(activity: Activity)
fun getPrice(): Flow<String>
fun listenPurchases(): Flow<PurchaseInfo>
}

View file

@ -2,4 +2,5 @@ package dev.lucasnlm.external
interface IFeatureFlagManager {
fun isGameHistoryEnabled(): Boolean
fun isRateUsEnabled(): Boolean
}

View file

@ -23,6 +23,8 @@ class BillingManager(
activity.startActivity(intent)
}
override fun getPrice(): Flow<String> = flowOf()
override fun listenPurchases(): Flow<PurchaseInfo> = flowOf()
companion object {

View file

@ -2,4 +2,5 @@ package dev.lucasnlm.external
class FeatureFlagManager : IFeatureFlagManager {
override fun isGameHistoryEnabled(): Boolean = true
override fun isRateUsEnabled(): Boolean = false
}

View file

@ -14,7 +14,10 @@ import com.android.billingclient.api.querySkuDetails
import dev.lucasnlm.external.model.PurchaseInfo
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.filterNotNull
class BillingManager(
private val context: Context,
@ -22,6 +25,8 @@ class BillingManager(
private val purchaseBroadcaster = ConflatedBroadcastChannel<PurchaseInfo>()
private var unlockPrice = MutableStateFlow<String?>(null)
private val billingClient by lazy {
BillingClient.newBuilder(context)
.setListener(this)
@ -29,6 +34,8 @@ class BillingManager(
.build()
}
override fun getPrice(): Flow<String> = unlockPrice.asSharedFlow().filterNotNull()
override fun listenPurchases(): Flow<PurchaseInfo> = purchaseBroadcaster.asFlow()
private fun handlePurchases(purchases: List<Purchase>) {
@ -61,18 +68,24 @@ class BillingManager(
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
val purchasesList: List<Purchase> = if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(listOf(BASIC_SUPPORT))
.setType(BillingClient.SkuType.INAPP)
.build()
billingClient.queryPurchases(BillingClient.SkuType.INAPP).purchasesList.let {
it?.toList() ?: listOf()
}
} else {
listOf()
billingClient
.querySkuDetailsAsync(skuDetailsParams) { _, list ->
unlockPrice.tryEmit(list?.firstOrNull()?.price)
}
val purchasesList: List<Purchase> = billingClient
.queryPurchases(BillingClient.SkuType.INAPP)
.purchasesList.let { it?.toList() ?: listOf() }
handlePurchases(purchasesList)
}
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
val purchasesList: List<Purchase> = if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
@ -94,12 +107,12 @@ class BillingManager(
}
override suspend fun charge(activity: Activity) {
if (billingClient.isReady) {
val skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(listOf(BASIC_SUPPORT))
.setType(BillingClient.SkuType.INAPP)
.build()
if (billingClient.isReady) {
val details = billingClient.querySkuDetails(skuDetailsParams)
details.skuDetailsList?.firstOrNull()?.let {
val flowParams = BillingFlowParams.newBuilder()

View file

@ -2,4 +2,5 @@ package dev.lucasnlm.external
class FeatureFlagManager : IFeatureFlagManager {
override fun isGameHistoryEnabled(): Boolean = false
override fun isRateUsEnabled(): Boolean = true
}