From a570f3a3b4ea23c941aab4fcfba474f329c8f9c9 Mon Sep 17 00:00:00 2001 From: Isabel Martin Date: Wed, 3 Jul 2024 10:31:40 -0700 Subject: [PATCH] MBL-1543: Pledge Redemption payments prototype (#2067) --- app/build.gradle | 2 +- .../ui/activities/PlaygroundActivity.kt | 175 +++++++++++++++--- .../viewmodels/PlaygroundViewModel.java | 26 --- .../viewmodels/PlaygroundViewModel.kt | 77 ++++++++ .../internal/res/layout/playground_layout.xml | 15 ++ app/src/main/graphql/checkout.graphql | 7 + .../mock/services/MockApolloClient.kt | 6 + .../com/kickstarter/models/CompleteOrder.kt | 23 +++ .../kickstarter/services/KSApolloClientV2.kt | 40 +++- 9 files changed, 317 insertions(+), 54 deletions(-) delete mode 100644 app/src/internal/java/com/kickstarter/viewmodels/PlaygroundViewModel.java create mode 100644 app/src/internal/java/com/kickstarter/viewmodels/PlaygroundViewModel.kt create mode 100644 app/src/main/java/com/kickstarter/models/CompleteOrder.kt diff --git a/app/build.gradle b/app/build.gradle index 844e7304c3..afad308411 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -234,7 +234,7 @@ dependencies { implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7:$rx_binding_version" implementation "com.jakewharton.rxbinding:rxbinding-support-v4:$rx_binding_version" implementation "com.jakewharton.timber:timber:5.0.1" - implementation 'com.stripe:stripe-android:20.42.0' + implementation 'com.stripe:stripe-android:20.47.3' final okhttp_version = '4.10.+' implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" implementation "com.squareup.okhttp3:okhttp-urlconnection:$okhttp_version" diff --git a/app/src/internal/java/com/kickstarter/ui/activities/PlaygroundActivity.kt b/app/src/internal/java/com/kickstarter/ui/activities/PlaygroundActivity.kt index d89db977f9..91981c9469 100644 --- a/app/src/internal/java/com/kickstarter/ui/activities/PlaygroundActivity.kt +++ b/app/src/internal/java/com/kickstarter/ui/activities/PlaygroundActivity.kt @@ -5,25 +5,45 @@ import android.os.Build import android.os.Bundle import android.util.Pair import android.view.View +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.viewModels import androidx.annotation.RequiresApi import com.kickstarter.R import com.kickstarter.databinding.PlaygroundLayoutBinding -import com.kickstarter.libs.BaseActivity import com.kickstarter.libs.RefTag -import com.kickstarter.libs.htmlparser.HTMLParser -import com.kickstarter.libs.htmlparser.TextViewElement -import com.kickstarter.libs.htmlparser.getStyledComponents -import com.kickstarter.libs.qualifiers.RequiresActivityViewModel +import com.kickstarter.libs.utils.extensions.addToDisposable +import com.kickstarter.libs.utils.extensions.getEnvironment +import com.kickstarter.libs.utils.extensions.getPaymentSheetConfiguration import com.kickstarter.mock.factories.ProjectFactory import com.kickstarter.models.Project import com.kickstarter.ui.extensions.showSnackbar import com.kickstarter.viewmodels.PlaygroundViewModel +import com.kickstarter.viewmodels.PlaygroundViewModel.Factory +import com.stripe.android.ApiResultCallback +import com.stripe.android.PaymentIntentResult +import com.stripe.android.Stripe +import com.stripe.android.StripeIntentResult +import com.stripe.android.paymentsheet.CreateIntentResult +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.PaymentSheetResult +import com.stripe.android.paymentsheet.model.PaymentOption import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.rx2.asObservable +import timber.log.Timber -@RequiresActivityViewModel(PlaygroundViewModel.ViewModel::class) -class PlaygroundActivity : BaseActivity() { +class PlaygroundActivity : ComponentActivity() { private lateinit var binding: PlaygroundLayoutBinding private lateinit var view: View + private lateinit var viewModelFactory: Factory + private var stripePaymentMethod: String = "" + val viewModel: PlaygroundViewModel by viewModels { viewModelFactory } + + private lateinit var flowController: PaymentSheet.FlowController + private lateinit var stripeSDK: Stripe + + private val compositeDisposable = CompositeDisposable() @RequiresApi(Build.VERSION_CODES.P) override fun onCreate(savedInstanceState: Bundle?) { @@ -32,25 +52,128 @@ class PlaygroundActivity : BaseActivity() { view = binding.root setContentView(view) - val html2 = "

This is heading 1

\n" + - "

This is heading 2

\n" + - "

This is heading 3

\n" + - "

This is heading 4

\n" + - "
This is heading 5
\n" + - "
This is heading 6
" - - val listOfElements = HTMLParser().parse(html2) - - // - The parser detects 6 elements and applies the style to each one - binding.h1.text = (listOfElements[0] as TextViewElement).getStyledComponents(this) - binding.h2.text = (listOfElements[1] as TextViewElement).getStyledComponents(this) - binding.h3.text = (listOfElements[2] as TextViewElement).getStyledComponents(this) - binding.h4.text = (listOfElements[3] as TextViewElement).getStyledComponents(this) - binding.h5.text = (listOfElements[4] as TextViewElement).getStyledComponents(this) - binding.h6.text = (listOfElements[5] as TextViewElement).getStyledComponents(this) - - setStepper() - setStartActivity() + this.getEnvironment()?.let { env -> + viewModelFactory = Factory(env) + stripeSDK = requireNotNull(env.stripe()) + } + + viewModel.payloadUIState.asObservable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.status == "succeeded") + Toast.makeText(this, "complete_order status: ${it.status}", Toast.LENGTH_LONG) + .show() + if (it.trigger3ds && it.stripePaymentMethodId.isNotEmpty()) { + Toast.makeText( + this, + "complete_order status: ${it.status} triggering 3DS flow", + Toast.LENGTH_LONG + ).show() + stripeSDK.handleNextActionForPayment( + this, + clientSecret = it.clientSecret, + ) + } + } + .addToDisposable(compositeDisposable) + + flowController = createFlowController() + configureFlowController() + + this.binding.newMethodButton.setOnClickListener { + flowController.presentPaymentOptions() + } + + this.binding.pledgeButton.setOnClickListener { + flowController.confirm() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { + super.onActivityResult(requestCode, resultCode, intent) + stripeSDK.onPaymentResult( + requestCode, intent, + object : ApiResultCallback { + override fun onSuccess(result: PaymentIntentResult) { + if (result.outcome == StripeIntentResult.Outcome.SUCCEEDED) { + if (result.intent.requiresAction() == false) { + Toast.makeText(this@PlaygroundActivity, "PaymentIntent: ${result.intent.id} requiresAction: ${result.intent.requiresAction()} 3DS flow Outcome = SUCCEEDED", Toast.LENGTH_LONG) + .show() + } + } + } + + override fun onError(e: Exception) { + Toast.makeText(this@PlaygroundActivity, " 3DS flow StripeIntentResult.Outcome = ERRORED", Toast.LENGTH_LONG) + .show() + } + } + ) + } + + private fun createFlowController() = PaymentSheet.FlowController.create( + activity = this, + paymentOptionCallback = ::onPaymentOption, + createIntentCallback = { paymentMethod, _ -> + // - createIntentCallback is triggered with flowController.confirm() + // - Make a request to complete to create a PaymentIntent and return its client secret + try { + viewModel.completeOrder(paymentMethod.id ?: "") + viewModel.payloadUIState.value.apply { + if (this.status == "succeeded") { + CreateIntentResult.Success(this.clientSecret) + } + } + viewModel.payloadUIState.collect { + } + } catch (e: Exception) { + Toast.makeText(this, "error when calling complete_order", Toast.LENGTH_LONG).show() + CreateIntentResult.Failure( + cause = e, + displayMessage = e.message + ) + } + }, + paymentResultCallback = ::onPaymentSheetResult, + ) + + private fun onPaymentOption(paymentOption: PaymentOption?) { + paymentOption?.let { + val toast = Toast.makeText(this, "new payment added: ${paymentOption.label}", Toast.LENGTH_LONG) // in Activity + toast.show() + Timber.d("paymentOption: $paymentOption") + } + } + + private fun configureFlowController() { + flowController.configureWithIntentConfiguration( + intentConfiguration = PaymentSheet.IntentConfiguration( + mode = PaymentSheet.IntentConfiguration.Mode.Payment( + amount = 1099, + currency = "usd", + ), + ), + // onBehalfOf = "acct_1Ir6hZ4NJG33TWAg", + configuration = this.getPaymentSheetConfiguration("arkariang@gmail.com"), + callback = { success, error -> + }, + ) + } + + fun onPaymentSheetResult(paymentSheetResult: PaymentSheetResult) { + when (paymentSheetResult) { + is PaymentSheetResult.Canceled -> { + // Customer canceled - you should probably do nothing. + } + is PaymentSheetResult.Failed -> { + print("Error: ${paymentSheetResult.error}") + // PaymentSheet encountered an unrecoverable error. You can display the error to the user, log it, etc. + } + is PaymentSheetResult.Completed -> { + // Display, for example, an order confirmation screen + print("Completed") + } + } } /** diff --git a/app/src/internal/java/com/kickstarter/viewmodels/PlaygroundViewModel.java b/app/src/internal/java/com/kickstarter/viewmodels/PlaygroundViewModel.java deleted file mode 100644 index dddde2f113..0000000000 --- a/app/src/internal/java/com/kickstarter/viewmodels/PlaygroundViewModel.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.kickstarter.viewmodels; - -import com.kickstarter.libs.ActivityViewModel; -import com.kickstarter.libs.Environment; -import com.kickstarter.ui.activities.PlaygroundActivity; - -import androidx.annotation.NonNull; - -public interface PlaygroundViewModel { - - interface Inputs { - } - - interface Outputs { - } - - final class ViewModel extends ActivityViewModel implements Inputs, Outputs { - - public ViewModel(final @NonNull Environment environment) { - super(environment); - } - - public final Inputs inputs = this; - public final Outputs outputs = this; - } -} diff --git a/app/src/internal/java/com/kickstarter/viewmodels/PlaygroundViewModel.kt b/app/src/internal/java/com/kickstarter/viewmodels/PlaygroundViewModel.kt new file mode 100644 index 0000000000..e63e8bd1d8 --- /dev/null +++ b/app/src/internal/java/com/kickstarter/viewmodels/PlaygroundViewModel.kt @@ -0,0 +1,77 @@ +package com.kickstarter.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.kickstarter.libs.Environment +import com.kickstarter.models.CompleteOrderInput +import com.kickstarter.models.CompleteOrderPayload +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx2.asFlow + +class PlaygroundViewModel(environment: Environment) : ViewModel() { + + private val apolloClient = requireNotNull(environment.apolloClientV2()) + + private var _payloadFlow = MutableStateFlow(CompleteOrderPayload()) + val payloadUIState: StateFlow = _payloadFlow.asStateFlow() + + private val _stripePaymentMethodId = MutableStateFlow("") + val stripePaymentMethodId: StateFlow = _stripePaymentMethodId.asStateFlow() + + fun completeOrder(stripeId: String) { + viewModelScope.launch { + val input = CompleteOrderInput( + projectId = "UHJvamVjdC01NzYyNDQ0OTk=", + stripePaymentMethodId = stripeId, + ) + + apolloClient.completeOrder(input).asFlow() + .collect { + val response = when (it.status) { + "requires_action" -> { + _payloadFlow.emit( + CompleteOrderPayload( + status = it.status, + clientSecret = it.clientSecret, + trigger3ds = true, + stripePaymentMethodId = stripeId + ) + ) + } + + "succeeded" -> { + _payloadFlow.emit( + CompleteOrderPayload( + status = it.status, + clientSecret = it.clientSecret, + trigger3ds = false, + stripePaymentMethodId = stripeId + ) + ) + } + + else -> { + _payloadFlow.emit( + CompleteOrderPayload( + status = "error", + clientSecret = it.clientSecret, + trigger3ds = false, + stripePaymentMethodId = stripeId + ) + ) + } + } + } + } + } + + class Factory(private val environment: Environment) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return PlaygroundViewModel(environment) as T + } + } +} diff --git a/app/src/internal/res/layout/playground_layout.xml b/app/src/internal/res/layout/playground_layout.xml index e43f1899d1..33778481aa 100644 --- a/app/src/internal/res/layout/playground_layout.xml +++ b/app/src/internal/res/layout/playground_layout.xml @@ -44,6 +44,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/appBar"> @@ -80,6 +81,20 @@ app:layout_constraintTop_toBottomOf="@id/h5" android:layout_width="match_parent" android:layout_height="wrap_content"/> + + + +