From 7d505bf90e7e6e244cc5b67222f639041bc7362b Mon Sep 17 00:00:00 2001 From: Leigh Douglas Date: Thu, 8 Aug 2024 16:08:21 -0400 Subject: [PATCH] MBL-1481: 3DS Validation Card - 3DS validation CTA flow (#2092) * Add infinite scroll to PPO screen * Attempting to fix tests * Test paging source * linter * Merge conflicts * Linter * Fixes * Linter * Testing * Fix * update graphql schema * fix * Hooked up requery and tests * lint * Cleanup * trying to fix tests * Fixed flaky tests * Cleanup * lint * cleanup * linter * 3ds validation * remove reference to stripe card ID, no longer needed * Linter --------- Co-authored-by: Yun Co-authored-by: Leigh Douglas --- app/src/main/graphql/fragments.graphql | 1 + .../pledgedprojectsoverview/data/PPOCard.kt | 7 +++ .../data/PPOCardFactory.kt | 21 +++++++ .../pledgedprojectsoverview/ui/PPOCardView.kt | 2 +- .../ui/PledgedProjectsOverviewActivity.kt | 63 ++++++++++++++++++- .../ui/PledgedProjectsOverviewScreen.kt | 23 ++++--- .../PledgedProjectsOverviewViewModel.kt | 12 +++- .../transformers/GraphQLTransformers.kt | 34 +++++----- app/src/main/res/values/strings.xml | 1 + 9 files changed, 132 insertions(+), 32 deletions(-) diff --git a/app/src/main/graphql/fragments.graphql b/app/src/main/graphql/fragments.graphql index e6b985171c..a812ba9c22 100644 --- a/app/src/main/graphql/fragments.graphql +++ b/app/src/main/graphql/fragments.graphql @@ -366,6 +366,7 @@ fragment shippingRule on ShippingRule { fragment ppoCard on Backing { id + clientSecret amount { ...amount } diff --git a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/data/PPOCard.kt b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/data/PPOCard.kt index d869f6df47..58135c808c 100644 --- a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/data/PPOCard.kt +++ b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/data/PPOCard.kt @@ -11,6 +11,7 @@ class PPOCard private constructor( val address: String?, val addressID: String?, val amount: String?, + val clientSecret: String?, val currencyCode: CurrencyCode?, val currencySymbol: String?, val projectName: String?, @@ -30,6 +31,7 @@ class PPOCard private constructor( fun address() = this.address fun addressID() = this.addressID fun amount() = this.amount + fun clientSecret() = this.clientSecret fun currencyCode() = this.currencyCode fun currencySymbol() = this.currencySymbol fun projectName() = this.projectName @@ -49,6 +51,7 @@ class PPOCard private constructor( var address: String? = null, var addressID: String? = null, var amount: String? = null, + var clientSecret: String? = null, var currencyCode: CurrencyCode? = null, var currencySymbol: String? = null, var projectName: String? = null, @@ -67,6 +70,7 @@ class PPOCard private constructor( fun address(address: String?) = apply { this.address = address } fun addressID(addressID: String?) = apply { this.addressID = addressID } fun amount(amount: String?) = apply { this.amount = amount } + fun clientSecret(clientSecret: String?) = apply { this.clientSecret = clientSecret } fun currencyCode(currencyCode: CurrencyCode?) = apply { this.currencyCode = currencyCode } fun currencySymbol(currencySymbol: String?) = apply { this.currencySymbol = currencySymbol } fun projectName(projectName: String?) = apply { this.projectName = projectName } @@ -85,6 +89,7 @@ class PPOCard private constructor( address = address, addressID = addressID, amount = amount, + clientSecret = clientSecret, currencyCode = currencyCode, currencySymbol = currencySymbol, projectName = projectName, @@ -105,6 +110,7 @@ class PPOCard private constructor( address = address, addressID = addressID, amount = amount, + clientSecret = clientSecret, currencyCode = currencyCode, currencySymbol = currencySymbol, projectName = projectName, @@ -131,6 +137,7 @@ class PPOCard private constructor( address() == other.address() && addressID() == other.addressID() && amount() == other.amount() && + clientSecret() == other.clientSecret() && currencyCode() == other.currencyCode() && currencySymbol() == other.currencySymbol() && projectName() == other.projectName() && diff --git a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/data/PPOCardFactory.kt b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/data/PPOCardFactory.kt index edcdef53cc..97358e22c9 100644 --- a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/data/PPOCardFactory.kt +++ b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/data/PPOCardFactory.kt @@ -81,5 +81,26 @@ class PPOCardFactory private constructor() { viewType = PPOCardViewType.FIX_PAYMENT ) } + + fun authenticationRequiredCard(): PPOCard { + // 3ds card + return ppoCard( + backingID = "1234", + amount = "$12.00", + address = "Firsty Lasty\n123 First Street, Apt #5678\nLos Angeles, CA 90025-1234\nUnited States", + addressID = "12234", + currencySymbol = "$", + currencyCode = CurrencyCode.USD, + projectName = "Super Duper Project", + projectId = "12345", + projectSlug = "project/slug", + imageUrl = "image/url", + creatorName = "Creator Name", + backingDetailsUrl = "backing/details/url", + timeNumberForAction = 7, + showBadge = false, + viewType = PPOCardViewType.AUTHENTICATE_CARD + ) + } } } diff --git a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PPOCardView.kt b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PPOCardView.kt index 370cdf56b7..857ccbcf95 100644 --- a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PPOCardView.kt +++ b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PPOCardView.kt @@ -113,7 +113,7 @@ fun PPOCardPreview() { item { PPOCardView( viewType = PPOCardViewType.AUTHENTICATE_CARD, - onCardClick = {}, + onCardClick = { }, projectName = "Sugardew Island - Your cozy farm shop let’s pretend this is a longer title let’s pretend this is a longer title", pledgeAmount = "$60.00", creatorName = "Some really really really really really really really long name", diff --git a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewActivity.kt b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewActivity.kt index 2e361a3857..e11e1dceca 100644 --- a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewActivity.kt +++ b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewActivity.kt @@ -19,6 +19,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.kickstarter.R import com.kickstarter.features.pledgedprojectsoverview.viewmodel.PledgedProjectsOverviewViewModel import com.kickstarter.libs.MessagePreviousScreenType import com.kickstarter.libs.RefTag @@ -31,13 +33,21 @@ import com.kickstarter.ui.SharedPreferenceKey import com.kickstarter.ui.activities.AppThemes import com.kickstarter.ui.activities.ProfileActivity import com.kickstarter.ui.compose.designsystem.KickstarterApp +import com.kickstarter.ui.extensions.setUpConnectivityStatusCheck +import com.kickstarter.ui.extensions.showSnackbar import com.kickstarter.ui.extensions.startCreatorMessageActivity import com.kickstarter.ui.extensions.transition +import com.stripe.android.ApiResultCallback +import com.stripe.android.PaymentIntentResult +import com.stripe.android.Stripe +import com.stripe.android.StripeIntentResult import kotlinx.coroutines.launch class PledgedProjectsOverviewActivity : AppCompatActivity() { private lateinit var viewModelFactory: PledgedProjectsOverviewViewModel.Factory + private lateinit var snackbarHostState: SnackbarHostState + private lateinit var stripe: Stripe private val viewModel: PledgedProjectsOverviewViewModel by viewModels { viewModelFactory } private var theme = AppThemes.MATCH_SYSTEM.ordinal private var startForResult = @@ -61,10 +71,13 @@ class PledgedProjectsOverviewActivity : AppCompatActivity() { ?.getInt(SharedPreferenceKey.APP_THEME, AppThemes.MATCH_SYSTEM.ordinal) ?: AppThemes.MATCH_SYSTEM.ordinal + stripe = requireNotNull(env.stripe()) + snackbarHostState = remember { SnackbarHostState() } + setUpConnectivityStatusCheck(lifecycle) + val ppoUIState by viewModel.ppoUIState.collectAsStateWithLifecycle() val lazyListState = rememberLazyListState() - val snackbarHostState = remember { SnackbarHostState() } val totalAlerts = viewModel.totalAlertsState.collectAsStateWithLifecycle().value val ppoCardPagingSource = viewModel.ppoCardsState.collectAsLazyPagingItems() @@ -102,6 +115,13 @@ class PledgedProjectsOverviewActivity : AppCompatActivity() { }, onPrimaryActionButtonClicked = { PPOCard -> when (PPOCard.viewType()) { + PPOCardViewType.AUTHENTICATE_CARD -> { + lifecycleScope.launch { + viewModel.showLoadingState(true) + } + stripeNextAction(PPOCard.clientSecret() ?: "", stripe) + } + PPOCardViewType.FIX_PAYMENT -> { openManagePledge( PPOCard.projectSlug ?: "", @@ -201,4 +221,45 @@ class PledgedProjectsOverviewActivity : AppCompatActivity() { TransitionUtils.transition(it, TransitionUtils.slideInFromRight()) } } + + private fun stripeNextAction(it: String, stripe: Stripe) { + try { + // - PaymentIntent format + if (it.contains("pi_")) { + stripe.handleNextActionForPayment(this, it) + } else { + // - SetupIntent format + stripe.handleNextActionForSetupIntent(this, it) + } + } catch (exception: Exception) { + FirebaseCrashlytics.getInstance().recordException(exception) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { + super.onActivityResult(requestCode, resultCode, intent) + stripe.onPaymentResult( + requestCode, intent, + object : ApiResultCallback { + override fun onSuccess(result: PaymentIntentResult) { + lifecycleScope.launch { + viewModel.showLoadingState(false) + } + if (result.outcome == StripeIntentResult.Outcome.SUCCEEDED) { + viewModel.showHeadsUpSnackbar(R.string.successful_validation_please_pull_to_refresh_fpo) + viewModel.getPledgedProjects() + } else if (result.outcome == StripeIntentResult.Outcome.FAILED || + result.outcome == StripeIntentResult.Outcome.TIMEDOUT || + result.outcome == StripeIntentResult.Outcome.UNKNOWN + ) viewModel.showErrorSnackbar(R.string.general_error_something_wrong) + } + override fun onError(e: Exception) { + lifecycleScope.launch { + viewModel.showLoadingState(false) + } + viewModel.showErrorSnackbar(R.string.general_error_something_wrong) + } + } + ) + } } diff --git a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreen.kt b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreen.kt index 8bef116624..024f539643 100644 --- a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreen.kt +++ b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreen.kt @@ -276,6 +276,17 @@ fun PledgedProjectsOverviewScreen( } } } + if (isLoading) { + Box( + modifier = Modifier + .fillMaxSize() + .background(KSTheme.colors.backgroundAccentGraySubtle.copy(alpha = 0.5f)) + .clickable(enabled = false) { }, + contentAlignment = Alignment.Center + ) { + KSCircularProgressIndicator() + } + } } } @@ -295,18 +306,6 @@ fun PledgedProjectsOverviewScreen( ) } } - - if (isLoading) { - Box( - modifier = Modifier - .fillMaxSize() - .background(KSTheme.colors.backgroundAccentGraySubtle.copy(alpha = 0.5f)) - .clickable(enabled = false) { }, - contentAlignment = Alignment.Center - ) { - KSCircularProgressIndicator() - } - } } @Composable diff --git a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModel.kt b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModel.kt index eaff6851fc..fc5f8fdb5e 100644 --- a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModel.kt +++ b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModel.kt @@ -100,6 +100,10 @@ class PledgedProjectsOverviewViewModel( private val mutablePPOUIState = MutableStateFlow(PledgedProjectsOverviewUIState()) val ppoCardsState: StateFlow> = mutablePpoCards.asStateFlow() + private var mutablePaymentRequiresAction = MutableSharedFlow() + val paymentRequiresAction: SharedFlow + get() = mutablePaymentRequiresAction.asSharedFlow() + private var pagingSource = PledgedProjectsPagingSource(apolloClient, mutableTotalAlerts, PAGE_LIMIT) val ppoUIState: StateFlow @@ -196,6 +200,10 @@ class PledgedProjectsOverviewViewModel( this.snackbarMessage = snackBarMessage } + suspend fun showLoadingState(isLoading: Boolean) { + emitCurrentState(isLoading = isLoading) + } + private suspend fun emitCurrentState(isLoading: Boolean = false, isErrored: Boolean = false) { mutablePPOUIState.emit( PledgedProjectsOverviewUIState( @@ -205,11 +213,11 @@ class PledgedProjectsOverviewViewModel( ) } - private fun showHeadsUpSnackbar(messageId: Int) { + fun showHeadsUpSnackbar(messageId: Int) { snackbarMessage.invoke(messageId, KSSnackbarTypes.KS_HEADS_UP.name) } - private fun showErrorSnackbar(messageId: Int) { + fun showErrorSnackbar(messageId: Int) { snackbarMessage.invoke(messageId, KSSnackbarTypes.KS_ERROR.name) } diff --git a/app/src/main/java/com/kickstarter/services/transformers/GraphQLTransformers.kt b/app/src/main/java/com/kickstarter/services/transformers/GraphQLTransformers.kt index 0783a7db8d..65f85d73aa 100644 --- a/app/src/main/java/com/kickstarter/services/transformers/GraphQLTransformers.kt +++ b/app/src/main/java/com/kickstarter/services/transformers/GraphQLTransformers.kt @@ -917,22 +917,24 @@ fun getPledgedProjectsOverviewQuery(queryInput: PledgedProjectsOverviewQueryData } fun pledgedProjectsOverviewEnvelopeTransformer(ppoResponse: PledgedProjectsOverviewQuery.PledgeProjectsOverview): PledgedProjectsOverviewEnvelope { - val ppoCards = ppoResponse.pledges()?.edges()?.map { - val ppoBackingData = it.node()?.backing()?.fragments()?.ppoCard() - PPOCard.builder() - .backingId(ppoBackingData?.id()) - .amount(ppoBackingData?.amount()?.fragments()?.amount()?.amount()) - .currencyCode(ppoBackingData?.amount()?.fragments()?.amount()?.currency()) - .currencySymbol(ppoBackingData?.amount()?.fragments()?.amount()?.symbol()) - .projectName(ppoBackingData?.project()?.name()) - .projectId(ppoBackingData?.project()?.id()) - .projectSlug(ppoBackingData?.project()?.slug()) - .imageUrl(ppoBackingData?.project()?.fragments()?.full()?.image()?.url()) - .creatorName(ppoBackingData?.project()?.creator()?.name()) - .viewType(getTierType(it.node()?.tierType())) - .addressID(ppoBackingData?.deliveryAddress()?.id()) - .build() - } + val ppoCards = + ppoResponse.pledges()?.edges()?.map { + val ppoBackingData = it.node()?.backing()?.fragments()?.ppoCard() + PPOCard.builder() + .backingId(ppoBackingData?.id()) + .clientSecret(ppoBackingData?.clientSecret()) + .amount(ppoBackingData?.amount()?.fragments()?.amount()?.amount()) + .currencyCode(ppoBackingData?.amount()?.fragments()?.amount()?.currency()) + .currencySymbol(ppoBackingData?.amount()?.fragments()?.amount()?.symbol()) + .projectName(ppoBackingData?.project()?.name()) + .projectId(ppoBackingData?.project()?.id()) + .projectSlug(ppoBackingData?.project()?.slug()) + .imageUrl(ppoBackingData?.project()?.fragments()?.full()?.image()?.url()) + .creatorName(ppoBackingData?.project()?.creator()?.name()) + .viewType(getTierType(it.node()?.tierType())) + .addressID(ppoBackingData?.deliveryAddress()?.id()) + .build() + } val pageInfoEnvelope = ppoResponse.pledges()?.pageInfo().let { PageInfoEnvelope.builder() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1688a256f9..e678a6cf97 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -97,5 +97,6 @@ Address confirmed! Need to change your address before it locks? Visit your backing details on our website. Backing details Something went wrong - Pull to refresh + Validation successful! Please pull to refresh