Skip to content

Commit

Permalink
MBL-1543: Pledge Redemption payments prototype (#2067)
Browse files Browse the repository at this point in the history
  • Loading branch information
Arkariang authored Jul 3, 2024
1 parent b94677f commit a570f3a
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 54 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlaygroundViewModel.ViewModel?>() {
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?) {
Expand All @@ -32,25 +52,128 @@ class PlaygroundActivity : BaseActivity<PlaygroundViewModel.ViewModel?>() {
view = binding.root
setContentView(view)

val html2 = "<h1>This is heading 1</h1>\n" +
"<h2>This is heading 2</h2>\n" +
"<h3>This is heading 3</h3>\n" +
"<h4>This is heading 4</h4>\n" +
"<h5>This is heading 5</h5>\n" +
"<h6>This is heading 6</h6>"

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<PaymentIntentResult> {
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("[email protected]"),
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")
}
}
}

/**
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<CompleteOrderPayload> = _payloadFlow.asStateFlow()

private val _stripePaymentMethodId = MutableStateFlow<String>("")
val stripePaymentMethodId: StateFlow<String> = _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 <T : ViewModel> create(modelClass: Class<T>): T {
return PlaygroundViewModel(environment) as T
}
}
}
15 changes: 15 additions & 0 deletions app/src/internal/res/layout/playground_layout.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBar">
<com.kickstarter.ui.views.Stepper
android:visibility="invisible"
android:id="@+id/stepper"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Expand Down Expand Up @@ -80,6 +81,20 @@
app:layout_constraintTop_toBottomOf="@id/h5"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/new_method_button"
android:text="Add new payment method"
app:layout_constraintTop_toBottomOf="@id/h6"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/pledge_button"
android:text="Pledge"
app:layout_constraintTop_toBottomOf="@id/new_method_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>

<Button
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/graphql/checkout.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,10 @@ mutation CompleteOnSessionCheckout($checkoutId: ID!, $paymentIntentClientSecret:
}
}
}

mutation completeOrder($projectId: ID! $stripePaymentMethodId: String, $paymentSourceId: String, $paymentSourceReusable: Boolean, $paymentMethodTypes: [String!]) {
completeOrder(input:{ projectId: $projectId, stripePaymentMethodId: $stripePaymentMethodId, paymentSourceId: $paymentSourceId, paymentSourceReusable: $paymentSourceReusable, paymentMethodTypes: $paymentMethodTypes }) {
status
clientSecret
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import com.kickstarter.models.Category
import com.kickstarter.models.Checkout
import com.kickstarter.models.CheckoutPayment
import com.kickstarter.models.Comment
import com.kickstarter.models.CompleteOrderInput
import com.kickstarter.models.CompleteOrderPayload
import com.kickstarter.models.CreatePaymentIntentInput
import com.kickstarter.models.CreatorDetails
import com.kickstarter.models.ErroredBacking
Expand Down Expand Up @@ -307,6 +309,10 @@ open class MockApolloClientV2 : ApolloClientTypeV2 {
return io.reactivex.Observable.empty()
}

override fun completeOrder(orderInput: CompleteOrderInput): io.reactivex.Observable<CompleteOrderPayload> {
return io.reactivex.Observable.empty()
}

override fun getPledgedProjectsOverviewPledges(inputData: PledgedProjectsOverviewQueryData): io.reactivex.Observable<PledgedProjectsOverviewEnvelope> {
return io.reactivex.Observable.empty()
}
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/com/kickstarter/models/CompleteOrder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.kickstarter.models

/**
* Data model for complete order mutation request
*/
data class CompleteOrderInput(
val projectId: String,
val orderId: String? = null,
val stripePaymentMethodId: String? = null,
val paymentSourceId: String? = null,
val paymentSourceReusable: Boolean? = null,
val paymentMethodTypes: List<String>? = null
)

/**
* Data model for complete order mutation response
*/
data class CompleteOrderPayload(
val status: String = "",
val clientSecret: String = "",
val trigger3ds: Boolean = false,
val stripePaymentMethodId: String = ""
)
Loading

0 comments on commit a570f3a

Please sign in to comment.