From 7088247ae5ee92dc5a218f9bcfc6a16c8bd2bc0f Mon Sep 17 00:00:00 2001 From: Isabel Martin Date: Tue, 23 Jul 2024 14:18:03 -0700 Subject: [PATCH] MBL-1618: Create alpha screen for pledge redemption (#2079) --- app/src/main/AndroidManifest.xml | 5 ++ .../ui/PledgeRedemptionActivity.kt | 66 +++++++++++++++++ .../viewmodels/PledgeRedemptionViewModel.kt | 30 ++++++++ .../libs/FirebaseAnalyticsClient.kt | 11 +++ .../libs/featureflag/FeatureFlagClient.kt | 3 +- .../libs/utils/extensions/IntentExt.kt | 10 +++ .../ui/activities/ProjectPageActivity.kt | 18 +++++ .../kickstarter/ui/extensions/ActivityExt.kt | 11 +++ .../projectpage/ProjectPageViewModel.kt | 27 ++++++- .../main/res/layout/activity_project_page.xml | 10 +++ .../viewmodels/ProjectPageViewModelTest.kt | 73 +++++++++++++++++++ 11 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/kickstarter/features/pledgeredemption/ui/PledgeRedemptionActivity.kt create mode 100644 app/src/main/java/com/kickstarter/features/pledgeredemption/viewmodels/PledgeRedemptionViewModel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7a187ceefa..fdd2bde6c1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -238,6 +238,11 @@ android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" android:label="@string/app_name" /> + diff --git a/app/src/main/java/com/kickstarter/features/pledgeredemption/ui/PledgeRedemptionActivity.kt b/app/src/main/java/com/kickstarter/features/pledgeredemption/ui/PledgeRedemptionActivity.kt new file mode 100644 index 0000000000..b80947cfc0 --- /dev/null +++ b/app/src/main/java/com/kickstarter/features/pledgeredemption/ui/PledgeRedemptionActivity.kt @@ -0,0 +1,66 @@ +package com.kickstarter.features.pledgeredemption.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import com.kickstarter.features.pledgeredemption.viewmodels.PledgeRedemptionViewModel +import com.kickstarter.libs.utils.extensions.getEnvironment +import com.kickstarter.libs.utils.extensions.isDarkModeEnabled +import com.kickstarter.mock.factories.RewardFactory +import com.kickstarter.models.Reward +import com.kickstarter.models.ShippingRule +import com.kickstarter.ui.activities.compose.projectpage.CheckoutScreen +import com.kickstarter.ui.compose.designsystem.KickstarterApp +import com.kickstarter.ui.data.PledgeReason + +class PledgeRedemptionActivity : ComponentActivity() { + private lateinit var viewModelFactory: PledgeRedemptionViewModel.Factory + private val viewModel: PledgeRedemptionViewModel by viewModels { viewModelFactory } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val env = this.getEnvironment()?.let { env -> + viewModelFactory = PledgeRedemptionViewModel.Factory(env, bundle = intent.extras) + env + } + viewModel.start() + + setContent { + setContent { + + val backing = viewModel.backing + val project = viewModel.project + val lists = mutableListOf() + val shippingAmount = backing.shippingAmount().toDouble() + val totalAmount = backing.amount() + val bonus = backing.bonusAmount() + + backing.reward()?.let { lists.add(it) } + backing.addOns()?.map { lists.add(it) } + + val darModeEnabled = this.isDarkModeEnabled(env = requireNotNull(env)) + KickstarterApp(useDarkTheme = darModeEnabled) { + CheckoutScreen( + rewardsList = lists.map { Pair(it.title() ?: "", it.pledgeAmount().toString()) }, + environment = env, + shippingAmount = shippingAmount, + selectedReward = RewardFactory.rewardWithShipping(), + currentShippingRule = ShippingRule.builder().build(), + totalAmount = totalAmount, + totalBonusSupport = bonus, + storedCards = emptyList(), + project = project, + email = "example@example.com", + pledgeReason = PledgeReason.PLEDGE, + rewardsHaveShippables = true, + onPledgeCtaClicked = { }, + newPaymentMethodClicked = { }, + onDisclaimerItemClicked = {}, + onAccountabilityLinkClicked = {} + ) + } + } + } + } +} diff --git a/app/src/main/java/com/kickstarter/features/pledgeredemption/viewmodels/PledgeRedemptionViewModel.kt b/app/src/main/java/com/kickstarter/features/pledgeredemption/viewmodels/PledgeRedemptionViewModel.kt new file mode 100644 index 0000000000..172623613d --- /dev/null +++ b/app/src/main/java/com/kickstarter/features/pledgeredemption/viewmodels/PledgeRedemptionViewModel.kt @@ -0,0 +1,30 @@ +package com.kickstarter.features.pledgeredemption.viewmodels + +import android.os.Bundle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.kickstarter.libs.Environment +import com.kickstarter.models.Backing +import com.kickstarter.models.Project +import com.kickstarter.ui.IntentKey + +class PledgeRedemptionViewModel(private val environment: Environment, private val bundle: Bundle? = null) : ViewModel() { + + lateinit var backing: Backing + lateinit var project: Project + + fun start() { + project = (bundle?.getParcelable(IntentKey.PROJECT) as Project?)?.let { + it + } ?: Project.builder().build() + + backing = project.backing() ?: Backing.builder().build() + } + + class Factory(private val environment: Environment, private val bundle: Bundle? = null) : + ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return PledgeRedemptionViewModel(environment, bundle = bundle) as T + } + } +} diff --git a/app/src/main/java/com/kickstarter/libs/FirebaseAnalyticsClient.kt b/app/src/main/java/com/kickstarter/libs/FirebaseAnalyticsClient.kt index ecd64c25e3..540f84c539 100644 --- a/app/src/main/java/com/kickstarter/libs/FirebaseAnalyticsClient.kt +++ b/app/src/main/java/com/kickstarter/libs/FirebaseAnalyticsClient.kt @@ -5,12 +5,15 @@ import android.os.Bundle import com.google.firebase.analytics.FirebaseAnalytics import com.kickstarter.libs.featureflag.FeatureFlagClientType import com.kickstarter.libs.featureflag.FlagKey +import com.kickstarter.models.User import com.kickstarter.ui.SharedPreferenceKey.CONSENT_MANAGEMENT_PREFERENCE interface FirebaseAnalyticsClientType { fun isEnabled(): Boolean fun trackEvent(eventName: String, parameters: Bundle) + + fun sendUserId(user: User) } open class FirebaseAnalyticsClient( @@ -28,4 +31,12 @@ open class FirebaseAnalyticsClient( } } } + + override fun sendUserId(user: User) { + firebaseAnalytics?.let { + if (isEnabled()) { + firebaseAnalytics.setUserId(user.id().toString()) + } + } + } } diff --git a/app/src/main/java/com/kickstarter/libs/featureflag/FeatureFlagClient.kt b/app/src/main/java/com/kickstarter/libs/featureflag/FeatureFlagClient.kt index 00b1f278f4..db592ecec1 100644 --- a/app/src/main/java/com/kickstarter/libs/featureflag/FeatureFlagClient.kt +++ b/app/src/main/java/com/kickstarter/libs/featureflag/FeatureFlagClient.kt @@ -65,7 +65,8 @@ enum class FlagKey(val key: String) { ANDROID_OAUTH("android_oauth"), ANDROID_ENCRYPT("android_encrypt_token"), ANDROID_STRIPE_LINK("android_stripe_link"), - ANDROID_PLEDGED_PROJECTS_OVERVIEW("android_pledged_projects_overview") + ANDROID_PLEDGED_PROJECTS_OVERVIEW("android_pledged_projects_overview"), + ANDROID_PLEDGE_REDEMPTION("android_pledge_redemption") } fun FeatureFlagClient.getFetchInterval(): Long = diff --git a/app/src/main/java/com/kickstarter/libs/utils/extensions/IntentExt.kt b/app/src/main/java/com/kickstarter/libs/utils/extensions/IntentExt.kt index 6bcac25763..0169f1dda4 100644 --- a/app/src/main/java/com/kickstarter/libs/utils/extensions/IntentExt.kt +++ b/app/src/main/java/com/kickstarter/libs/utils/extensions/IntentExt.kt @@ -2,6 +2,7 @@ package com.kickstarter.libs.utils.extensions import android.content.Context import android.content.Intent +import com.kickstarter.features.pledgeredemption.ui.PledgeRedemptionActivity import com.kickstarter.models.Project import com.kickstarter.ui.IntentKey import com.kickstarter.ui.activities.CommentsActivity @@ -43,6 +44,15 @@ fun Intent.getPaymentMethodsIntent(context: Context): Intent { return this.setClass(context, PaymentMethodsSettingsActivity::class.java) } +fun Intent.getPledgeRedemptionIntent( + context: Context, + project: Project +): Intent { + this.setClass(context, PledgeRedemptionActivity::class.java) + .putExtra(IntentKey.PROJECT, project) + return this +} + fun Intent.getRootCommentsActivityIntent( context: Context, projectData: ProjectData, diff --git a/app/src/main/java/com/kickstarter/ui/activities/ProjectPageActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/ProjectPageActivity.kt index 3039b11c9c..f8f9f871a0 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/ProjectPageActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/ProjectPageActivity.kt @@ -61,6 +61,7 @@ import com.kickstarter.libs.utils.extensions.showLatePledgeFlow import com.kickstarter.libs.utils.extensions.toVisibility import com.kickstarter.models.Project import com.kickstarter.models.Reward +import com.kickstarter.models.User import com.kickstarter.models.chrome.ChromeTabsHelperActivity import com.kickstarter.ui.IntentKey import com.kickstarter.ui.activities.compose.projectpage.ProjectPledgeButtonAndFragmentContainer @@ -80,6 +81,7 @@ import com.kickstarter.ui.extensions.setUpConnectivityStatusCheck import com.kickstarter.ui.extensions.showErrorToast import com.kickstarter.ui.extensions.showSnackbar import com.kickstarter.ui.extensions.startDisclaimerChromeTab +import com.kickstarter.ui.extensions.startPledgeRedemption import com.kickstarter.ui.extensions.startRootCommentsActivity import com.kickstarter.ui.extensions.startUpdatesActivity import com.kickstarter.ui.extensions.startVideoActivity @@ -466,6 +468,22 @@ class ProjectPageActivity : } }.addToDisposable(disposables) + var pBacking: Project? = null + var user: User? = null + viewModel.outputs.showPledgeRedemptionScreen() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + pBacking = it.first + user = it.second + binding.pledgeRedemptionAlpha.visibility = View.VISIBLE + }.addToDisposable(disposables) + + binding.pledgeRedemptionAlpha.setOnClickListener { + pBacking?.let { + startPledgeRedemption(it) + } + } + binding.backIcon.setOnClickListener { if (binding.pledgeContainerLayout.pledgeContainerRoot.visibility == View.GONE) { onBackPressedDispatcher.onBackPressed() diff --git a/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt b/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt index 1b71147c7a..05bd990baf 100644 --- a/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt +++ b/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt @@ -24,6 +24,7 @@ import com.kickstarter.libs.utils.Secrets import com.kickstarter.libs.utils.TransitionUtils import com.kickstarter.libs.utils.UrlUtils import com.kickstarter.libs.utils.extensions.getCreatorBioWebViewActivityIntent +import com.kickstarter.libs.utils.extensions.getPledgeRedemptionIntent import com.kickstarter.libs.utils.extensions.getPreLaunchProjectActivity import com.kickstarter.libs.utils.extensions.getProjectUpdatesActivityIntent import com.kickstarter.libs.utils.extensions.getReportProjectActivityIntent @@ -143,6 +144,16 @@ fun Activity.showRatingDialogWidget() { } } +fun Activity.startPledgeRedemption(project: Project) { + startActivity( + Intent().getPledgeRedemptionIntent(this, project) + ) + + this.let { + TransitionUtils.transition(it, TransitionUtils.slideInFromRight()) + } +} + /** * This function starts the RootCommentActivity with Transition animation included * @param projectData diff --git a/app/src/main/java/com/kickstarter/viewmodels/projectpage/ProjectPageViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/projectpage/ProjectPageViewModel.kt index b7d4185ce9..85aa926399 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/projectpage/ProjectPageViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/projectpage/ProjectPageViewModel.kt @@ -268,6 +268,8 @@ interface ProjectPageViewModel { fun updateVideoCloseSeekPosition(): Observable fun showLatePledgeFlow(): Observable + + fun showPledgeRedemptionScreen(): Observable> } class ProjectPageViewModel(val environment: Environment) : @@ -356,6 +358,7 @@ interface ProjectPageViewModel { private val onOpenVideoInFullScreen = PublishSubject.create>() private val updateVideoCloseSeekPosition = BehaviorSubject.create() private val showLatePledgeFlow = BehaviorSubject.create() + private val showPledgeRedemptionScreen = BehaviorSubject.create>() val inputs: Inputs = this val outputs: Outputs = this @@ -789,9 +792,27 @@ interface ProjectPageViewModel { val backedProject = currentProject .filter { it.isBacking() } - val backing = backedProject + val projectBacking = backedProject .filter { it.backing().isNotNull() } - .map { requireNotNull(it.backing()) } + .map { requireNotNull(it) } + + val backing = projectBacking.map { requireNotNull(it.backing()) } + + val isAdmin = this.currentUser.observable() + .filter { it.isPresent() } + .map { requireNotNull(it.getValue()) } + .filter { it.isAdmin() && ffClient.getBoolean(FlagKey.ANDROID_PLEDGE_REDEMPTION) } + .map { it } + + Observable.combineLatest(projectBacking, isAdmin) { pBacking, adminUser -> + Pair(pBacking, adminUser) + } + .subscribe { + // remove userId tracking when removing feature flag or giving access to all users + this.environment.firebaseAnalyticsClient()?.sendUserId(it.second) + this.showPledgeRedemptionScreen.onNext(it) + } + .addToDisposable(disposables) // - Update fragments with the backing data currentProjectData @@ -1272,6 +1293,8 @@ interface ProjectPageViewModel { override fun showLatePledgeFlow(): Observable = this.showLatePledgeFlow + override fun showPledgeRedemptionScreen(): Observable> = this.showPledgeRedemptionScreen + private fun backingDetailsSubtitle(project: Project): Either? { return project.backing()?.let { backing -> return if (backing.status() == Backing.STATUS_ERRORED) { diff --git a/app/src/main/res/layout/activity_project_page.xml b/app/src/main/res/layout/activity_project_page.xml index 9a5fa5c107..159eb4e0c6 100644 --- a/app/src/main/res/layout/activity_project_page.xml +++ b/app/src/main/res/layout/activity_project_page.xml @@ -196,4 +196,14 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="@dimen/reward_fragment_guideline_constraint_end" /> + + \ No newline at end of file diff --git a/app/src/test/java/com/kickstarter/viewmodels/ProjectPageViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/ProjectPageViewModelTest.kt index 63c0d3acb4..04782923a2 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/ProjectPageViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/ProjectPageViewModelTest.kt @@ -30,6 +30,7 @@ import com.kickstarter.models.Backing import com.kickstarter.models.EnvironmentalCommitment import com.kickstarter.models.Project import com.kickstarter.models.Urls +import com.kickstarter.models.User import com.kickstarter.models.Web import com.kickstarter.ui.IntentKey import com.kickstarter.ui.SharedPreferenceKey @@ -99,6 +100,7 @@ class ProjectPageViewModelTest : KSRobolectricTestCase() { private val onOpenVideoInFullScreen = TestSubscriber>() private val updateVideoCloseSeekPosition = TestSubscriber() private val postCampaignPledgingEnabled = TestSubscriber() + private val pledgeRedemptionIsVisible = TestSubscriber>() private val disposables = CompositeDisposable() @@ -152,6 +154,7 @@ class ProjectPageViewModelTest : KSRobolectricTestCase() { this.vm.outputs.onOpenVideoInFullScreen().subscribe { this.onOpenVideoInFullScreen.onNext(it) }.addToDisposable(disposables) this.vm.outputs.updateVideoCloseSeekPosition().subscribe { this.updateVideoCloseSeekPosition.onNext(it) }.addToDisposable(disposables) this.vm.outputs.showLatePledgeFlow().subscribe { this.postCampaignPledgingEnabled.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.showPledgeRedemptionScreen().subscribe { this.pledgeRedemptionIsVisible.onNext(it) }.addToDisposable(disposables) } @Test @@ -2214,6 +2217,76 @@ class ProjectPageViewModelTest : KSRobolectricTestCase() { this.showSavedPromptTest.assertValueCount(0) } + @Test + fun `Test Pledge Redemption button is visible for admin users when the project is backed and feature flag enabled`() { + val user = UserFactory.user().toBuilder().isAdmin(true).build() + val project = ProjectFactory.backedProject() + val currentUserMock = MockCurrentUserV2(user) + val mockFeatureFlagClient = object : MockFeatureFlagClient() { + override fun getBoolean(FlagKey: FlagKey): Boolean { + return true + } + } + setUpEnvironment( + environment().toBuilder() + .currentUserV2(currentUserMock) + .featureFlagClient(mockFeatureFlagClient) + .build() + ) + + this.vm.configureWith(Intent().putExtra(IntentKey.PROJECT, project)) + + pledgeRedemptionIsVisible.assertValueCount(2) + this.vm.outputs.showPledgeRedemptionScreen().subscribe { + assertEquals(it.first, project) + assertEquals(it.second, user) + }.addToDisposable(disposables) + } + + @Test + fun `Test Pledge Redemption button is NOT visible for admin users when the project is backed and feature flag disabled`() { + val user = UserFactory.user().toBuilder().isAdmin(true).build() + val project = ProjectFactory.backedProject() + val currentUserMock = MockCurrentUserV2(user) + val mockFeatureFlagClient = object : MockFeatureFlagClient() { + override fun getBoolean(FlagKey: FlagKey): Boolean { + return false + } + } + setUpEnvironment( + environment().toBuilder() + .currentUserV2(currentUserMock) + .featureFlagClient(mockFeatureFlagClient) + .build() + ) + + this.vm.configureWith(Intent().putExtra(IntentKey.PROJECT, project)) + + pledgeRedemptionIsVisible.assertNoValues() + } + + @Test + fun `Test Pledge Redemption button is NOT visible for users (not admin) when the project is backed and feature flag enabled`() { + val user = UserFactory.user().toBuilder().isAdmin(false).build() + val project = ProjectFactory.backedProject() + val currentUserMock = MockCurrentUserV2(user) + val mockFeatureFlagClient = object : MockFeatureFlagClient() { + override fun getBoolean(FlagKey: FlagKey): Boolean { + return true + } + } + setUpEnvironment( + environment().toBuilder() + .currentUserV2(currentUserMock) + .featureFlagClient(mockFeatureFlagClient) + .build() + ) + + this.vm.configureWith(Intent().putExtra(IntentKey.PROJECT, project)) + + pledgeRedemptionIsVisible.assertNoValues() + } + private fun deepLinkIntent(): Intent { val uri = Uri.parse("https://www.kickstarter.com/projects/1186238668/skull-graphic-tee") return Intent(Intent.ACTION_VIEW, uri)