diff --git a/Habitica/res/drawable/subscription_banner_image_left.xml b/Habitica/res/drawable/subscription_banner_image_left.xml
new file mode 100644
index 000000000..0261df640
--- /dev/null
+++ b/Habitica/res/drawable/subscription_banner_image_left.xml
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Habitica/res/drawable/subscription_banner_image_right.xml b/Habitica/res/drawable/subscription_banner_image_right.xml
new file mode 100644
index 000000000..d13de7c95
--- /dev/null
+++ b/Habitica/res/drawable/subscription_banner_image_right.xml
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Habitica/res/layout/fragment_bottomsheet_subscription.xml b/Habitica/res/layout/fragment_bottomsheet_subscription.xml
new file mode 100644
index 000000000..e0e7dc288
--- /dev/null
+++ b/Habitica/res/layout/fragment_bottomsheet_subscription.xml
@@ -0,0 +1,370 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml
index 5b17f4a0f..1c0400aac 100644
--- a/Habitica/res/values/strings.xml
+++ b/Habitica/res/values/strings.xml
@@ -1432,6 +1432,11 @@
"If you’re struggling to complete your Dailies, you can pause damage from Settings for a break. This will prevent the Boss from hurting you or others, but also pause your own damage. Other members missed Dailies will still cause damage, so be careful out there! "
Rage meters
Some difficult bosses have an orange Rage meter under their HP. This meter fills up when participants miss Dailies. When it fills completely, the boss will let out a fearsome attack that does extra damage!
+ Buy Gems with Gold
+ Subscribe to buy Gems with Gold and receive these other exclusive benefits!
+ Each time you open the Armoire, you can open again for free!
+ Earn Hourglasses over time to purchase items in the Time Traveler\'s Shop
+ See more subscription options
- You
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/SubscriptionBottomSheetFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/SubscriptionBottomSheetFragment.kt
new file mode 100644
index 000000000..bdfd8c55b
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/SubscriptionBottomSheetFragment.kt
@@ -0,0 +1,237 @@
+package com.habitrpg.android.habitica.ui.fragments.purchases
+
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import androidx.compose.foundation.layout.padding
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import com.android.billingclient.api.ProductDetails
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.habitrpg.android.habitica.R
+import com.habitrpg.android.habitica.data.InventoryRepository
+import com.habitrpg.android.habitica.data.UserRepository
+import com.habitrpg.android.habitica.databinding.DialogHabiticaAccountBinding
+import com.habitrpg.android.habitica.databinding.FragmentBottomsheetSubscriptionBinding
+import com.habitrpg.android.habitica.databinding.FragmentSubscriptionBinding
+import com.habitrpg.android.habitica.extensions.addCancelButton
+import com.habitrpg.android.habitica.helpers.Analytics
+import com.habitrpg.android.habitica.helpers.AppConfigManager
+import com.habitrpg.android.habitica.helpers.PurchaseHandler
+import com.habitrpg.android.habitica.helpers.PurchaseTypes
+import com.habitrpg.android.habitica.models.user.User
+import com.habitrpg.android.habitica.ui.activities.GiftSubscriptionActivity
+import com.habitrpg.android.habitica.ui.fragments.PromoInfoFragment
+import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
+import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
+import com.habitrpg.android.habitica.ui.views.promo.BirthdayBanner
+import com.habitrpg.android.habitica.ui.views.subscriptions.SubscriptionOptionView
+import com.habitrpg.common.habitica.extensions.isUsingNightModeResources
+import com.habitrpg.common.habitica.extensions.layoutInflater
+import com.habitrpg.common.habitica.extensions.loadImage
+import com.habitrpg.common.habitica.helpers.ExceptionHandler
+import com.habitrpg.common.habitica.helpers.MainNavigationController
+import com.habitrpg.common.habitica.helpers.launchCatching
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class SubscriptionBottomSheetFragment : BottomSheetDialogFragment() {
+
+ private var _binding: FragmentBottomsheetSubscriptionBinding? = null
+ private val binding get() = _binding!!
+
+ @Inject
+ lateinit var userRepository: UserRepository
+
+ @Inject
+ lateinit var appConfigManager: AppConfigManager
+
+ @Inject
+ lateinit var inventoryRepository: InventoryRepository
+
+ @Inject
+ lateinit var purchaseHandler: PurchaseHandler
+
+ private var selectedSubscriptionSku: ProductDetails? = null
+ private var skus: List = emptyList()
+
+ private var user: User? = null
+ private var hasLoadedSubscriptionOptions: Boolean = false
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ _binding = FragmentBottomsheetSubscriptionBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.subscriptionOptions.visibility = View.GONE
+ binding.seeMoreOptions.setOnClickListener {
+ dismiss()
+ MainNavigationController.navigate(R.id.gemPurchaseActivity)
+
+ }
+ binding.subscribeButton.setOnClickListener { purchaseSubscription() }
+
+ lifecycleScope.launchCatching {
+ userRepository.getUser().collect { user ->
+ user?.let { setUser(it) }
+ }
+ }
+
+ lifecycleScope.launchCatching {
+ inventoryRepository.getLatestMysteryItem().collect {
+ binding.subBenefitsMysteryItemIcon.loadImage(
+ "shop_set_mystery_${
+ it.key?.split(
+ "_"
+ )?.last()
+ }"
+ )
+ binding.subBenefitsMysteryItemText.text =
+ context?.getString(R.string.subscribe_listitem3_description_new, it.text)
+ }
+ }
+
+ binding.benefitArmoireWrapper.isVisible = appConfigManager.enableArmoireSubs()
+ binding.benefitFaintWrapper.isVisible = appConfigManager.enableFaintSubs()
+
+ Analytics.sendNavigationEvent("subscription screen")
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val bottomSheetDialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
+ bottomSheetDialog.setOnShowListener { dialog: DialogInterface ->
+ val notificationDialog = dialog as BottomSheetDialog
+ notificationDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ notificationDialog.behavior.isDraggable = true
+ }
+ return bottomSheetDialog
+ }
+
+ override fun onResume() {
+ super.onResume()
+ lifecycleScope.launchCatching {
+ purchaseHandler.queryPurchases()
+ }
+ refresh()
+ loadInventory()
+ }
+
+ private fun refresh() {
+ lifecycleScope.launch(ExceptionHandler.coroutine()) {
+ userRepository.retrieveUser(false, true)
+ }
+ }
+
+ private fun loadInventory() {
+ CoroutineScope(Dispatchers.IO).launchCatching {
+ val subscriptions = purchaseHandler.getAllSubscriptionProducts()
+ skus = subscriptions
+ withContext(Dispatchers.Main) {
+ for (sku in subscriptions) {
+ updateButtonLabel(sku, sku.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.formattedPrice ?: "")
+ }
+ subscriptions.minByOrNull { it.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.priceAmountMicros ?: 0 }?.let { selectSubscription(it) }
+ hasLoadedSubscriptionOptions = true
+ updateSubscriptionInfo()
+ }
+ }
+ }
+
+ private fun updateButtonLabel(sku: ProductDetails, price: String) {
+ val matchingView = buttonForSku(sku)
+ if (matchingView != null) {
+ matchingView.setPriceText(price)
+ matchingView.sku = sku.productId
+ matchingView.setOnPurchaseClickListener {
+ selectSubscription(sku)
+ }
+ }
+ }
+
+ private fun selectSubscription(sku: ProductDetails) {
+ if (this.selectedSubscriptionSku != null) {
+ val oldButton = buttonForSku(this.selectedSubscriptionSku)
+ oldButton?.setIsSelected(false)
+ }
+ this.selectedSubscriptionSku = sku
+ val subscriptionOptionButton = buttonForSku(this.selectedSubscriptionSku)
+ subscriptionOptionButton?.setIsSelected(true)
+ binding.subscribeButton.isEnabled = true
+ }
+
+ private fun buttonForSku(sku: ProductDetails?): SubscriptionOptionView? {
+ return buttonForSku(sku?.productId)
+ }
+
+ private fun buttonForSku(sku: String?): SubscriptionOptionView? {
+ return when (sku) {
+ PurchaseTypes.Subscription1Month -> binding.subscription1month
+ PurchaseTypes.Subscription12Month -> binding.subscription12month
+ else -> null
+ }
+ }
+
+ private fun purchaseSubscription() {
+ selectedSubscriptionSku?.let { sku ->
+ activity?.let {
+ purchaseHandler.purchase(it, sku)
+ dismiss()
+ }
+ }
+ }
+
+ fun setUser(newUser: User) {
+ user = newUser
+ this.updateSubscriptionInfo()
+ checkIfNeedsCancellation()
+ }
+
+ private fun updateSubscriptionInfo() {
+ if (hasLoadedSubscriptionOptions) {
+ binding.subscriptionOptions.visibility = View.VISIBLE
+ binding.loadingIndicator.visibility = View.GONE
+ }
+ if (user != null) {
+ binding.loadingIndicator.visibility = View.GONE
+ }
+ }
+
+ private fun checkIfNeedsCancellation() {
+ CoroutineScope(Dispatchers.IO).launch(ExceptionHandler.coroutine()) {
+ val newestSubscription = purchaseHandler.checkForSubscription()
+ if (user?.purchased?.plan?.paymentMethod == "Google" &&
+ user?.purchased?.plan?.isActive == true &&
+ user?.purchased?.plan?.dateTerminated == null &&
+ (newestSubscription?.isAutoRenewing != true)
+ ) {
+ lifecycleScope.launch(ExceptionHandler.coroutine()) {
+ purchaseHandler.cancelSubscription()
+ }
+ }
+ }
+ }
+
+ companion object {
+ const val TAG = "SubscriptionBottomSheet"
+ }
+}
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt
index c158fec83..ecec27ad0 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt
@@ -8,6 +8,7 @@ import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.os.bundleOf
@@ -30,6 +31,8 @@ import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.activities.ArmoireActivityDirections
+import com.habitrpg.android.habitica.ui.fragments.preferences.HabiticaAccountDialog
+import com.habitrpg.android.habitica.ui.fragments.purchases.SubscriptionBottomSheetFragment
import com.habitrpg.android.habitica.ui.views.CurrencyView
import com.habitrpg.android.habitica.ui.views.CurrencyViews
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
@@ -60,7 +63,7 @@ class PurchaseDialog(
private val userRepository: UserRepository,
private val inventoryRepository: InventoryRepository,
val item: ShopItem,
- val parentActivity: Activity? = null
+ val parentActivity: AppCompatActivity? = null
) : HabiticaAlertDialog(context) {
private val customHeader: View by lazy {
@@ -355,19 +358,22 @@ class PurchaseDialog(
when {
"gems" == shopItem.purchaseType -> {
if (shopItem.canAfford(user, purchaseQuantity)) {
- InsufficientSubscriberGemsDialog(context)
+ InsufficientSubscriberGemsDialog(context).show()
} else {
- InsufficientGoldDialog(context)
+ InsufficientGoldDialog(context).show()
}
}
- "gold" == shopItem.currency -> InsufficientGoldDialog(context)
+ "gold" == shopItem.currency -> InsufficientGoldDialog(context).show()
"gems" == shopItem.currency -> {
Analytics.sendEvent("show insufficient gems modal", EventCategory.BEHAVIOUR, HitType.EVENT, mapOf("reason" to "purchase modal", "item" to shopItem.key))
- parentActivity?.let { activity -> InsufficientGemsDialog(activity, shopItem.value) }
+ val subscriptionBottomSheet = SubscriptionBottomSheetFragment()
+ parentActivity?.let { activity ->
+ subscriptionBottomSheet.show(activity.supportFragmentManager, SubscriptionBottomSheetFragment.TAG)
+ }
}
- "hourglasses" == shopItem.currency -> InsufficientHourglassesDialog(context)
+ "hourglasses" == shopItem.currency -> InsufficientHourglassesDialog(context).show()
else -> null
- }?.show()
+ }
return
}
}