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

Move PaymentSheet example to /checkout endpoint. #3633

Merged
merged 7 commits into from
May 6, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions example/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
id 'kotlin-android'
brnunes-stripe marked this conversation as resolved.
Show resolved Hide resolved
id 'checkstyle'
id 'org.jetbrains.kotlin.plugin.parcelize'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.30'
}

assemble.dependsOn('lint')
Expand Down Expand Up @@ -69,13 +70,15 @@ dependencies {
/* Used to make Retrofit easier and GSON & Rx-compatible*/
implementation 'com.google.code.gson:gson:2.8.6'
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"

implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"

ktlint "com.pinterest:ktlint:$ktlintVersion"

Expand Down
13 changes: 9 additions & 4 deletions example/res/xml/payment_sheet_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@
app:title="Set merchant name"
app:defaultValue="Widget Store" />

<SwitchPreferenceCompat
app:key="setup_intent"
app:title="Use SetupIntent"
app:defaultValue="false" />
<!-- <SwitchPreferenceCompat-->
<!-- app:key="setup_intent"-->
<!-- app:title="Use SetupIntent"-->
<!-- app:defaultValue="false" />-->
brnunes-stripe marked this conversation as resolved.
Show resolved Hide resolved

<SwitchPreferenceCompat
app:key="enable_customer"
app:title="Use Customer API"
app:defaultValue="true" />

<SwitchPreferenceCompat
app:key="returning_customer"
app:title="Returning Customer"
app:defaultValue="true" />

brnunes-stripe marked this conversation as resolved.
Show resolved Hide resolved
<SwitchPreferenceCompat
app:key="enable_googlepay"
app:title="Enable Google Pay"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.stripe.android.PaymentConfiguration
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.PaymentSheetResult
import com.stripe.example.R
Expand All @@ -30,6 +31,9 @@ internal abstract class BasePaymentSheetActivity : AppCompatActivity() {
protected val isCustomerEnabled: Boolean
get() = prefsManager.getBoolean("enable_customer", true)

protected val isReturningCustomer: Boolean
get() = prefsManager.getBoolean("returning_customer", true)

protected val isSetupIntent: Boolean
get() = prefsManager.getBoolean("setup_intent", false)

Expand All @@ -46,6 +50,20 @@ internal abstract class BasePaymentSheetActivity : AppCompatActivity() {
}
}

protected val customer: String
brnunes-stripe marked this conversation as resolved.
Show resolved Hide resolved
get() = if (isCustomerEnabled && isReturningCustomer) {
"returning"
} else if (isCustomerEnabled) {
temporaryCustomerId ?: "new"
} else {
"new"
}

protected val mode: String
get() = if (isSetupIntent) "setup" else "payment"

protected var temporaryCustomerId: String? = null
brnunes-stripe marked this conversation as resolved.
Show resolved Hide resolved

override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.payment_sheet, menu)
Expand All @@ -66,6 +84,33 @@ internal abstract class BasePaymentSheetActivity : AppCompatActivity() {
return super.onOptionsItemSelected(item)
}

protected fun prepareCheckout(
onSuccess: (PaymentSheet.CustomerConfiguration, String) -> Unit
) {
viewModel.prepareCheckout(customer, mode)
.observe(this) { checkoutResponse ->
if (checkoutResponse != null) {
temporaryCustomerId = if (isCustomerEnabled && !isReturningCustomer) {
checkoutResponse.customerId
} else {
null
}

// Re-initing here because the ExampleApplication inits with the key from
// gradle properties
PaymentConfiguration.init(this, checkoutResponse.publishableKey)

onSuccess(
PaymentSheet.CustomerConfiguration(
id = checkoutResponse.customerId,
ephemeralKeySecret = checkoutResponse.customerEphemeralKeySecret
),
checkoutResponse.intentClientSecret
)
}
}
}

protected fun fetchEphemeralKey(
onSuccess: (PaymentSheet.CustomerConfiguration) -> Unit = {}
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.stripe.example.activity

import android.os.Bundle
import android.view.View
import androidx.core.view.isInvisible
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.example.databinding.ActivityPaymentSheetCompleteBinding

Expand All @@ -17,94 +17,40 @@ internal class LaunchPaymentSheetCompleteActivity : BasePaymentSheetActivity() {
val paymentSheet = PaymentSheet(this, ::onPaymentSheetResult)

viewModel.inProgress.observe(this) {
viewBinding.progressBar.visibility = if (it) View.VISIBLE else View.INVISIBLE
viewBinding.progressBar.isInvisible = !it
viewBinding.launch.isEnabled = !it
}

viewModel.status.observe(this) {
viewBinding.status.text = it
}

viewBinding.launch.setOnClickListener {
if (isCustomerEnabled) {
fetchEphemeralKey { customerConfig ->
createIntent(paymentSheet, customerConfig)
}
} else {
createIntent(paymentSheet, null)
}
}
}

private fun createIntent(
paymentSheet: PaymentSheet,
customerConfig: PaymentSheet.CustomerConfiguration?
) {
if (isSetupIntent) {
createSetupIntent(paymentSheet, customerConfig)
} else {
createPaymentIntent(paymentSheet, customerConfig)
}
}

private fun createPaymentIntent(
paymentSheet: PaymentSheet,
customerConfig: PaymentSheet.CustomerConfiguration?
) {
viewModel.createPaymentIntent(
COUNTRY_CODE,
customerId = customerConfig?.id
).observe(this) {
it.fold(
onSuccess = { json ->
val clientSecret = json.getString("secret")
viewModel.inProgress.postValue(false)

paymentSheet.presentWithPaymentIntent(
prepareCheckout { customerConfig, clientSecret ->
if (isSetupIntent) {
paymentSheet.presentWithSetupIntent(
clientSecret,
PaymentSheet.Configuration(
merchantDisplayName = merchantName,
customer = customerConfig,
googlePay = googlePayConfig,
)
)
},
onFailure = ::onError
)
}
}

private fun createSetupIntent(
paymentSheet: PaymentSheet,
customerConfig: PaymentSheet.CustomerConfiguration?
) {
viewModel.createSetupIntent(
COUNTRY_CODE,
customerId = customerConfig?.id
).observe(this) {
it.fold(
onSuccess = { json ->
val clientSecret = json.getString("secret")
viewModel.inProgress.postValue(false)

paymentSheet.presentWithSetupIntent(
} else {
paymentSheet.presentWithPaymentIntent(
clientSecret,
PaymentSheet.Configuration(
merchantDisplayName = merchantName,
customer = customerConfig,
googlePay = googlePayConfig,
)
)
},
onFailure = ::onError
)
}
}
}
}

override fun onRefreshEphemeralKey() {
fetchEphemeralKey()
}

private companion object {
private const val COUNTRY_CODE = "us"
// fetchEphemeralKey()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.stripe.example.R
import com.stripe.example.databinding.PaymentSheetConfigBottomSheetBinding
Expand Down Expand Up @@ -39,8 +40,23 @@ class PaymentSheetConfigBottomSheet : BottomSheetDialogFragment() {
}

internal class PreferenceFragment : PreferenceFragmentCompat() {
var enableCustomerApi: SwitchPreferenceCompat? = null
var isReturningCustomer: SwitchPreferenceCompat? = null

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.payment_sheet_preferences, rootKey)

enableCustomerApi = findPreference("enable_customer")
isReturningCustomer = findPreference("returning_customer")
enableCustomerApi?.setOnPreferenceClickListener {
updatePreferencesVisibility()
false
}
updatePreferencesVisibility()
}

private fun updatePreferencesVisibility() {
isReturningCustomer?.isVisible = enableCustomerApi?.isChecked == true
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ package com.stripe.example.module

import android.content.Context
import com.google.gson.GsonBuilder
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.stripe.example.Settings
import com.stripe.example.service.BackendApi
import com.stripe.example.service.CheckoutBackendApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
Expand Down Expand Up @@ -42,6 +47,25 @@ internal class BackendApiFactory internal constructor(private val backendUrl: St
.create(BackendApi::class.java)
}

@OptIn(ExperimentalSerializationApi::class)
fun createCheckout(): CheckoutBackendApi {
val logging = HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY)

val httpClient = OkHttpClient.Builder()
.connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.addInterceptor(logging)
.build()

return Retrofit.Builder()
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
brnunes-stripe marked this conversation as resolved.
Show resolved Hide resolved
.baseUrl("https://complex-various-kryptops.glitch.me/")
.client(httpClient)
.build()
.create(CheckoutBackendApi::class.java)
}

private companion object {
private const val TIMEOUT_SECONDS = 15L
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.stripe.example.paymentsheet

import android.content.SharedPreferences
import com.stripe.example.service.BackendApi
import com.stripe.example.service.CheckoutBackendApi
import com.stripe.example.service.CheckoutRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
Expand All @@ -10,6 +12,7 @@ import kotlin.coroutines.CoroutineContext

internal class DefaultRepository(
private val backendApi: BackendApi,
private val checkoutBackendApi: CheckoutBackendApi,
private val sharedPrefs: SharedPreferences,
private val workContext: CoroutineContext
) : Repository {
Expand Down Expand Up @@ -49,6 +52,18 @@ internal class DefaultRepository(
)
}

override suspend fun checkout(
customer: String,
currency: String,
mode: String
) = withContext(workContext) {
flowOf(
kotlin.runCatching {
checkoutBackendApi.checkout(CheckoutRequest(customer, currency, mode))
}
)
}

private companion object {
private const val PREF_EK = "pref_ek"
private const val PREF_CUSTOMER = "pref_customer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,43 @@ internal class PaymentSheetViewModel(
}
}

fun prepareCheckout(customer: String, mode: String) = liveData {
inProgress.postValue(true)
status.postValue("Preparing checkout...")

val checkoutResponse = repository.checkout(
customer, CURRENCY, mode
).single()

checkoutResponse.fold(
onSuccess = { response ->
status.postValue(
"${status.value}\n\nReady to checkout: $response"
)
},
onFailure = {
status.postValue(
"${status.value}\n\nPreparing checkout failed\n${it.message}"
)
}
)

inProgress.postValue(false)
emit(checkoutResponse.getOrNull())
}

internal class Factory(
private val application: Application,
private val sharedPrefs: SharedPreferences,
private val workContext: CoroutineContext = Dispatchers.IO
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val backendApi = BackendApiFactory(application).create()
val checkoutBackendApi = BackendApiFactory(application).createCheckout()

val repository = DefaultRepository(
backendApi,
checkoutBackendApi,
sharedPrefs,
workContext
)
Expand All @@ -70,4 +97,8 @@ internal class PaymentSheetViewModel(
) as T
}
}

private companion object {
private const val CURRENCY = "usd"
}
}
Loading