Skip to content

Commit

Permalink
Add feature to pass through payment credentials w/o saving (APPS-1648) (
Browse files Browse the repository at this point in the history
  • Loading branch information
cmaier authored Oct 29, 2024
1 parent f6f5fd6 commit 020d377
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 50 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ All notable changes to this project will be documented in this file.
### Removed
### Fixed

## [0.77.0]
### Added
* core/ui: Add a feature to pass payment credentials w/o saving it into storage

## [0.76.0]
* Maintenance update w/ dependency updates

## [0.75.8]
### Changed
* ui: Refactor RemoteThemingExtensions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@file:JvmName("PaymentCredentialsFlow")

package io.snabble.sdk.payment

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow

internal interface MutableCredentialsFlow {

fun tryEmitCredentials(credentials: PaymentCredentials): Boolean
}

/**
* This flow is emitting payment credentials if adding a payment method has been configured to be added without being
* saved.
*
* Payment method being added the regular way, won't be emitted here.
* The [PaymentCredentialsStore.OnPaymentCredentialsAddedListener] is called in that case.
*/
object PaymentCredentialsFlow : MutableCredentialsFlow {

private val _credentialsFlow = MutableStateFlow<PaymentCredentials?>(null)

/**
* Collect this flow to be notified if payment credentials has been created successfully.
* If there are no collectors, it will be discarded.
*/
val credentialsFlow: SharedFlow<PaymentCredentials?> = _credentialsFlow.asSharedFlow()

override fun tryEmitCredentials(credentials: PaymentCredentials): Boolean = _credentialsFlow.tryEmit(credentials)
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ public synchronized void add(PaymentCredentials credentials) {
notifyChanged();
}

public synchronized void justEmitCredentials(final PaymentCredentials credentials) {
((MutableCredentialsFlow) PaymentCredentialsFlow.INSTANCE).tryEmitCredentials(credentials);
}

/**
* Remove and persist payment credentials
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -108,7 +107,6 @@ private fun PasswordField(
onPasswordChange: (String) -> Unit,
onAction: () -> Unit,
) {
@OptIn(ExperimentalMaterial3Api::class)
OutlinedTextField(
value = password,
onValueChange = onPasswordChange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ import io.snabble.sdk.utils.Logger

object PaymentInputViewHelper {

@JvmOverloads
@JvmStatic
fun openPaymentInputView(context: Context, paymentMethod: PaymentMethod?, projectId: String) {
fun openPaymentInputView(
context: Context,
paymentMethod: PaymentMethod?,
projectId: String,
saveMethod: Boolean = true
) {
if (KeyguardUtils.isDeviceSecure()) {
val project = Snabble.getProjectById(projectId) ?: return
if (paymentMethod == null) {
Expand All @@ -44,11 +50,12 @@ object PaymentInputViewHelper {
SnabbleUI.executeAction(context, SnabbleUI.Event.SHOW_DATATRANS_INPUT, args)
}

usePayone -> Payone.registerCard(activity, project, paymentMethod, Snabble.formPrefillData)
usePayone -> Payone.registerCard(activity, project, paymentMethod, saveMethod, Snabble.formPrefillData)

useFiserv -> {
args.putString(FiservInputView.ARG_PROJECT_ID, projectId)
args.putSerializable(FiservInputView.ARG_PAYMENT_TYPE, paymentMethod.name)
args.putBoolean(FiservInputView.ARG_SAVE_PAYMENT_CREDENTIALS, saveMethod)
SnabbleUI.executeAction(context, SnabbleUI.Event.SHOW_FISERV_INPUT, args)
}

Expand Down
20 changes: 14 additions & 6 deletions ui/src/main/java/io/snabble/sdk/ui/payment/Payone.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.snabble.sdk.ui.payment

import android.os.Bundle
import android.os.Parcelable
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentActivity
import com.google.gson.annotations.SerializedName
import io.snabble.sdk.PaymentMethod
Expand All @@ -11,6 +11,11 @@ import io.snabble.sdk.Snabble
import io.snabble.sdk.payment.data.FormPrefillData
import io.snabble.sdk.ui.R
import io.snabble.sdk.ui.SnabbleUI
import io.snabble.sdk.ui.payment.PayoneInputView.Companion.ARG_FORM_PREFILL_DATA
import io.snabble.sdk.ui.payment.PayoneInputView.Companion.ARG_PAYMENT_TYPE
import io.snabble.sdk.ui.payment.PayoneInputView.Companion.ARG_PROJECT_ID
import io.snabble.sdk.ui.payment.PayoneInputView.Companion.ARG_SAVE_PAYMENT_CREDENTIALS
import io.snabble.sdk.ui.payment.PayoneInputView.Companion.ARG_TOKEN_DATA
import io.snabble.sdk.utils.Dispatch
import io.snabble.sdk.utils.Logger
import io.snabble.sdk.utils.SimpleJsonCallback
Expand Down Expand Up @@ -77,6 +82,7 @@ object Payone {
activity: FragmentActivity,
project: Project,
paymentMethod: PaymentMethod,
saveCredentials: Boolean,
formPrefillData: FormPrefillData?
) {
val descriptor = project.paymentMethodDescriptors.find { it.paymentMethod == paymentMethod }
Expand Down Expand Up @@ -108,11 +114,13 @@ object Payone {
project.okHttpClient.newCall(request)
.enqueue(object : SimpleJsonCallback<PayoneTokenizationData>(PayoneTokenizationData::class.java), Callback {
override fun success(response: PayoneTokenizationData) {
val args = Bundle()
args.putString(PayoneInputView.ARG_PROJECT_ID, project.id)
args.putSerializable(PayoneInputView.ARG_PAYMENT_TYPE, paymentMethod)
args.putParcelable(PayoneInputView.ARG_TOKEN_DATA, response)
args.putParcelable(PayoneInputView.ARG_FORM_PREFILL_DATA, formPrefillData)
val args = bundleOf(
ARG_PROJECT_ID to project.id,
ARG_SAVE_PAYMENT_CREDENTIALS to saveCredentials,
ARG_PAYMENT_TYPE to paymentMethod,
ARG_TOKEN_DATA to response,
ARG_FORM_PREFILL_DATA to formPrefillData
)
Dispatch.mainThread {
SnabbleUI.executeAction(activity, SnabbleUI.Event.SHOW_PAYONE_INPUT, args)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ open class PayoneInputFragment : BaseFragment(
companion object {
const val ARG_PROJECT_ID = PayoneInputView.ARG_PROJECT_ID
const val ARG_PAYMENT_TYPE = PayoneInputView.ARG_PAYMENT_TYPE
const val ARG_SAVE_PAYMENT_CREDENTIALS = PayoneInputView.ARG_SAVE_PAYMENT_CREDENTIALS
const val ARG_TOKEN_DATA = PayoneInputView.ARG_TOKEN_DATA
const val ARG_FORM_PREFILL_DATA = PayoneInputView.ARG_FORM_PREFILL_DATA
}
Expand All @@ -24,18 +25,20 @@ open class PayoneInputFragment : BaseFragment(
private lateinit var paymentMethod: PaymentMethod
private lateinit var tokenizationData: Payone.PayoneTokenizationData
private var formPrefillData: FormPrefillData? = null
private var saveCredentials: Boolean = true

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

projectId = requireNotNull(arguments?.getString(ARG_PROJECT_ID, null))
paymentMethod = requireNotNull(arguments?.serializableExtra(ARG_PAYMENT_TYPE) as? PaymentMethod)
saveCredentials = arguments?.getBoolean(ARG_SAVE_PAYMENT_CREDENTIALS, true) ?: true
tokenizationData = requireNotNull(arguments?.parcelableExtra(ARG_TOKEN_DATA))
formPrefillData = arguments?.parcelableExtra(ARG_FORM_PREFILL_DATA)
}

override fun onActualViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<PayoneInputView>(R.id.user_payment_method_view)
.load(projectId, paymentMethod, tokenizationData, formPrefillData)
.load(projectId, paymentMethod, saveCredentials, tokenizationData, formPrefillData)
}
}
52 changes: 40 additions & 12 deletions ui/src/main/java/io/snabble/sdk/ui/payment/PayoneInputView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,17 @@ class PayoneInputView @JvmOverloads constructor(context: Context, attrs: Attribu
when (it.status) {
Payone.AuthStatus.pending -> doLater(1000)
Payone.AuthStatus.successful -> {
creditCardInfo?.let { cardInfo ->
val cardInfo = creditCardInfo
if (cardInfo != null) {
cardInfo.userId = it.userID
authenticateAndSave(cardInfo)
} ?: finishWithError()
if (savePaymentCredentials) {
authenticateAndSave(cardInfo)
} else {
justEmitCredentials(cardInfo)
}
} else {
finishWithError()
}
}
Payone.AuthStatus.failed -> finishWithError()
}
Expand All @@ -97,6 +104,7 @@ class PayoneInputView @JvmOverloads constructor(context: Context, attrs: Attribu
})
}
}
private var savePaymentCredentials: Boolean = true

@SuppressLint("InlinedApi", "SetJavaScriptEnabled", "AddJavascriptInterface")
private fun inflateView() {
Expand Down Expand Up @@ -171,11 +179,13 @@ class PayoneInputView @JvmOverloads constructor(context: Context, attrs: Attribu
fun load(
projectId: String,
paymentType: PaymentMethod,
saveCredentials: Boolean,
tokenizationData: PayoneTokenizationData,
formPrefillData: FormPrefillData?
) {
this.project = Snabble.projects.first { it.id == projectId }
this.paymentType = paymentType
this.savePaymentCredentials = saveCredentials
this.tokenizationData = tokenizationData
this.formPrefillData = formPrefillData
inflateView()
Expand Down Expand Up @@ -301,13 +311,38 @@ class PayoneInputView @JvmOverloads constructor(context: Context, attrs: Attribu

private fun save(info: CreditCardInfo) {
creditCardInfo = null
val pc: PaymentCredentials? = createPaymentCredentials(info)
if (pc == null) {
Toast.makeText(context, "Could not verify payment credentials", Toast.LENGTH_LONG)
.show()
} else {
Snabble.paymentCredentialsStore.add(pc)
Telemetry.event(Telemetry.Event.PaymentMethodAdded, pc.type?.name)
}
finish()
}

private fun justEmitCredentials(info: CreditCardInfo) {
creditCardInfo = null
val credentials: PaymentCredentials? = createPaymentCredentials(info)
if (credentials == null) {
Toast.makeText(context, "Could not verify payment credentials", Toast.LENGTH_LONG)
.show()
} else {
Snabble.paymentCredentialsStore.justEmitCredentials(credentials)
Telemetry.event(Telemetry.Event.PaymentMethodAdded, credentials.type?.name)
}
finish()
}

private fun createPaymentCredentials(info: CreditCardInfo): PaymentCredentials? {
val ccBrand = when (info.cardType) {
"V" -> PaymentCredentials.Brand.VISA
"M" -> PaymentCredentials.Brand.MASTERCARD
"A" -> PaymentCredentials.Brand.AMEX
else -> PaymentCredentials.Brand.UNKNOWN
}
val pc: PaymentCredentials? = PaymentCredentials.fromPayone(
return PaymentCredentials.fromPayone(
info.pseudoCardPan,
info.truncatedCardPan,
ccBrand,
Expand All @@ -322,14 +357,6 @@ class PayoneInputView @JvmOverloads constructor(context: Context, attrs: Attribu
info.userId,
project.id
)
if (pc == null) {
Toast.makeText(context, "Could not verify payment credentials", Toast.LENGTH_LONG)
.show()
} else {
Snabble.paymentCredentialsStore.add(pc)
Telemetry.event(Telemetry.Event.PaymentMethodAdded, pc.type?.name)
}
finish()
}

private fun finish() {
Expand Down Expand Up @@ -476,6 +503,7 @@ class PayoneInputView @JvmOverloads constructor(context: Context, attrs: Attribu
const val ARG_PROJECT_ID = "projectId"
const val ARG_PAYMENT_TYPE = "paymentType"
const val ARG_TOKEN_DATA = "tokenData"
const val ARG_SAVE_PAYMENT_CREDENTIALS = "savePaymentCredentials"
const val ARG_FORM_PREFILL_DATA = "formPrefillData"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ open class FiservInputFragment : Fragment() {
private lateinit var paymentMethod: String
private lateinit var projectId: String

private var savePaymentCredentials: Boolean = true

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand All @@ -33,6 +35,8 @@ open class FiservInputFragment : Fragment() {
projectId = arguments?.serializableExtra<String>(FiservInputView.ARG_PROJECT_ID)
?: kotlin.run { activity?.onBackPressedDispatcher?.onBackPressed(); return }

savePaymentCredentials = arguments?.getBoolean(FiservInputView.ARG_SAVE_PAYMENT_CREDENTIALS, true) ?: true

(requireActivity() as? AppCompatActivity)?.supportActionBar?.title =
PaymentMethodMetaDataHelper(requireContext()).labelFor(PaymentMethod.valueOf(paymentMethod))
}
Expand All @@ -43,6 +47,7 @@ open class FiservInputFragment : Fragment() {
FiservScreen(
projectId = projectId,
paymentMethod = PaymentMethod.valueOf(paymentMethod),
savePaymentCredentials = savePaymentCredentials,
onBackNavigationClick = { activity?.onBackPressedDispatcher?.onBackPressed(); }
)
}
Expand All @@ -53,6 +58,7 @@ open class FiservInputFragment : Fragment() {
fun FiservScreen(
projectId: String,
paymentMethod: PaymentMethod,
savePaymentCredentials: Boolean,
onBackNavigationClick: () -> Unit,
) {
val context = LocalContext.current
Expand All @@ -76,6 +82,7 @@ fun FiservScreen(
factory = { ctx ->
FiservInputView(ctx)
.apply {
setSavePaymentCredentials(savePaymentCredentials)
load(
projectId,
paymentMethod,
Expand Down
Loading

0 comments on commit 020d377

Please sign in to comment.