Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Project] Up Next Shuffle - Display Unlock Dialog for Shuffle #3131

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ private fun Content(
OnboardingUpgradeSource.SETTINGS,
OnboardingUpgradeSource.SLUMBER_STUDIOS,
OnboardingUpgradeSource.WHATS_NEW_SKIP_CHAPTERS,
OnboardingUpgradeSource.UP_NEXT_SHUFFLE,
OnboardingUpgradeSource.UNKNOWN,
-> false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class UpNextAdapter(
notifyDataSetChanged()
}

private var isSignedInAsPaidUser: Boolean = false

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
Expand All @@ -100,8 +102,7 @@ class UpNextAdapter(
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
when (item) {
when (val item = getItem(position)) {
is BaseEpisode -> bindEpisodeRow(holder as UpNextEpisodeViewHolder, item)
is PlayerViewModel.UpNextSummary -> (holder as HeaderViewHolder).bind(item)
is UpNextPlaying -> (holder as PlayingViewHolder).bind(item)
Expand Down Expand Up @@ -158,6 +159,10 @@ class UpNextAdapter(
(holder as? UpNextEpisodeViewHolder)?.clearDisposable()
}

fun updateUserSignInState(isSignedInAsPaidUser: Boolean) {
this.isSignedInAsPaidUser = isSignedInAsPaidUser
}

inner class HeaderViewHolder(val binding: AdapterUpNextFooterBinding) : RecyclerView.ViewHolder(binding.root) {

fun bind(header: PlayerViewModel.UpNextSummary) {
Expand All @@ -175,11 +180,13 @@ class UpNextAdapter(
shuffle.updateShuffleButton()

shuffle.setOnClickListener {
val newValue = !settings.upNextShuffle.value
analyticsTracker.track(AnalyticsEvent.UP_NEXT_SHUFFLE_ENABLED, mapOf("value" to newValue, SOURCE_KEY to upNextSource.analyticsValue))

settings.upNextShuffle.set(newValue, updateModifiedAt = false)

if (isSignedInAsPaidUser) {
val newValue = !settings.upNextShuffle.value
analyticsTracker.track(AnalyticsEvent.UP_NEXT_SHUFFLE_ENABLED, mapOf("value" to newValue, SOURCE_KEY to upNextSource.analyticsValue))
settings.upNextShuffle.set(newValue, updateModifiedAt = false)
} else {
UpNextShufflePaywallBottomSheet().show(fragmentManager, "up_next_shuffle_paywall_dialog")
}
shuffle.updateShuffleButton()
}

Expand All @@ -190,19 +197,29 @@ class UpNextAdapter(
private fun ImageButton.updateShuffleButton() {
if (!FeatureFlag.isEnabled(Feature.UP_NEXT_SHUFFLE)) return

val isEnabled = settings.upNextShuffle.value

this.setImageResource(if (isEnabled) IR.drawable.shuffle_enabled else IR.drawable.shuffle)
this.setImageResource(
when {
!isSignedInAsPaidUser -> IR.drawable.shuffle_plus_feature_icon
settings.upNextShuffle.value -> IR.drawable.shuffle_enabled
else -> IR.drawable.shuffle
},
)

this.contentDescription = context.getString(
if (isEnabled) LR.string.up_next_shuffle_disable_button_content_description else LR.string.up_next_shuffle_button_content_description,
when {
isSignedInAsPaidUser -> LR.string.up_next_shuffle_button_content_description
settings.upNextShuffle.value -> LR.string.up_next_shuffle_disable_button_content_description
else -> LR.string.up_next_shuffle_button_content_description
},
)

this.setImageTintList(
ColorStateList.valueOf(
if (isEnabled) ThemeColor.primaryIcon01(theme) else ThemeColor.primaryIcon02(theme),
),
)
if (isSignedInAsPaidUser) {
this.setImageTintList(
ColorStateList.valueOf(
if (settings.upNextShuffle.value) ThemeColor.primaryIcon01(theme) else ThemeColor.primaryIcon02(theme),
),
)
}

TooltipCompat.setTooltipText(
this,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
Expand All @@ -27,6 +28,7 @@ import au.com.shiftyjelly.pocketcasts.models.type.EpisodeViewSource
import au.com.shiftyjelly.pocketcasts.player.R
import au.com.shiftyjelly.pocketcasts.player.databinding.FragmentUpnextBinding
import au.com.shiftyjelly.pocketcasts.player.viewmodel.PlayerViewModel
import au.com.shiftyjelly.pocketcasts.player.viewmodel.UpNextViewModel
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager
import au.com.shiftyjelly.pocketcasts.repositories.playback.UpNextSource
Expand Down Expand Up @@ -93,6 +95,7 @@ class UpNextFragment : BaseFragment(), UpNextListener, UpNextTouchCallback.ItemT
lateinit var adapter: UpNextAdapter
private val sourceView = SourceView.UP_NEXT
private val playerViewModel: PlayerViewModel by activityViewModels()
private val upNextViewModel: UpNextViewModel by viewModels<UpNextViewModel>()
private val swipeButtonLayoutViewModel: SwipeButtonLayoutViewModel by activityViewModels()
private var userRearrangingFrom: Int? = null
private var userDraggingStart: Int? = null
Expand Down Expand Up @@ -309,6 +312,13 @@ class UpNextFragment : BaseFragment(), UpNextListener, UpNextTouchCallback.ItemT
toolbar.menu.findItem(R.id.clear_up_next)?.isVisible = it.upNextEpisodes.isNotEmpty()
}

viewLifecycleOwner.lifecycleScope.launch {
upNextViewModel.isSignedInAsPaidUser.collect { isSignedInAsPaidUser ->
adapter.updateUserSignInState(isSignedInAsPaidUser)
adapter.notifyDataSetChanged()
}
}

view.isClickable = true

val callback = UpNextTouchCallback(adapter = this)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package au.com.shiftyjelly.pocketcasts.player.view

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.doOnLayout
import au.com.shiftyjelly.pocketcasts.compose.AppTheme
import au.com.shiftyjelly.pocketcasts.player.view.dialog.UpNextShuffleDialog
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingFlow
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingLauncher
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingUpgradeSource
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import jakarta.inject.Inject

@AndroidEntryPoint
class UpNextShufflePaywallBottomSheet : BottomSheetDialogFragment() {

@Inject
lateinit var theme: Theme

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
AppTheme(theme.activeTheme) {
UpNextShuffleDialog(
onTryPlusNowClick = {
OnboardingLauncher.openOnboardingFlow(activity, OnboardingFlow.Upsell(OnboardingUpgradeSource.UP_NEXT_SHUFFLE))
},
onNotNowClick = {
dismiss()
},
)
}
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

view.doOnLayout {
val dialog = dialog as BottomSheetDialog
val bottomSheet = dialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout?
if (bottomSheet != null) {
BottomSheetBehavior.from(bottomSheet).run {
state = BottomSheetBehavior.STATE_EXPANDED
peekHeight = 0
skipCollapsed = true
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package au.com.shiftyjelly.pocketcasts.player.view.dialog

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground
import au.com.shiftyjelly.pocketcasts.compose.buttons.GradientRowButton
import au.com.shiftyjelly.pocketcasts.compose.buttons.RowOutlinedButton
import au.com.shiftyjelly.pocketcasts.compose.components.TextH30
import au.com.shiftyjelly.pocketcasts.compose.components.TextH50
import au.com.shiftyjelly.pocketcasts.compose.extensions.plusBackgroundBrush
import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvider
import au.com.shiftyjelly.pocketcasts.compose.theme
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
import au.com.shiftyjelly.pocketcasts.images.R as IR
import au.com.shiftyjelly.pocketcasts.localization.R as LR

@Composable
fun UpNextShuffleDialog(
modifier: Modifier = Modifier,
onTryPlusNowClick: () -> Unit,
onNotNowClick: () -> Unit,

) {
Box(
modifier = modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxWidth(),
) {
Image(
painterResource(IR.drawable.swipe_affordance),
contentDescription = stringResource(LR.string.swipe_affordance_content_description),
modifier = modifier
.width(56.dp)
.padding(top = 8.dp, bottom = 35.dp),
)

Image(
painterResource(IR.drawable.shuffle_plus_feature_icon),
contentDescription = stringResource(LR.string.up_next_shuffle_button_content_description),
modifier = modifier
.padding(top = 8.dp, bottom = 12.dp)
.size(96.dp)
)

TextH30(
text = stringResource(LR.string.up_next_shuffle_your_episodes_with_plus),
textAlign = TextAlign.Center,
fontWeight = FontWeight.W600,
modifier = modifier.padding(bottom = 12.dp, start = 21.dp, end = 21.dp),
)

TextH50(
text = stringResource(LR.string.up_next_shuffle_unlock_feature_description),
fontWeight = FontWeight.W500,
color = MaterialTheme.theme.colors.primaryText02,
textAlign = TextAlign.Center,
modifier = modifier.padding(bottom = 12.dp, start = 21.dp, end = 21.dp),
)

GradientRowButton(
primaryText = stringResource(LR.string.try_plus_now),
textColor = Color.Black,
gradientBackgroundColor = plusBackgroundBrush,
modifier = modifier.padding(bottom = 12.dp).padding(horizontal = 20.dp),
onClick = { onTryPlusNowClick.invoke() },
)

RowOutlinedButton(
text = stringResource(LR.string.not_now),
onClick = { onNotNowClick.invoke() },
includePadding = false,
modifier = modifier.padding(bottom = 12.dp).padding(horizontal = 20.dp),
border = null,
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.theme.colors.primaryText01),
)
}
}
}

@Preview(showBackground = true)
@Composable
fun PreviewUpNextShuffleDialog(@PreviewParameter(ThemePreviewParameterProvider::class) themeType: Theme.ThemeType) {
AppThemeWithBackground(themeType) {
UpNextShuffleDialog(onTryPlusNowClick = {}, onNotNowClick = {})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package au.com.shiftyjelly.pocketcasts.player.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import au.com.shiftyjelly.pocketcasts.repositories.di.IoDispatcher
import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow

@HiltViewModel
class UpNextViewModel @Inject constructor(
val userManager: UserManager,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {

private val _isSignedInAsPaidUser = MutableStateFlow(false)
val isSignedInAsPaidUser: StateFlow<Boolean> get() = _isSignedInAsPaidUser

init {
viewModelScope.launch(ioDispatcher) {
userManager.getSignInState().asFlow().collect { signInState ->
_isSignedInAsPaidUser.value = signInState.isSignedInAsPlusOrPatron
}
}
}
}
Loading