Skip to content

Commit

Permalink
Merge pull request #128 from airwallex/feature/Integrate-risk-SDK
Browse files Browse the repository at this point in the history
[APAM-17]integrate risk sdk
  • Loading branch information
passyruan authored Jul 12, 2024
2 parents 89ac284 + 18e1f6e commit e510bc5
Show file tree
Hide file tree
Showing 14 changed files with 165 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.airwallex.android.core.model.CardScheme
import com.airwallex.android.core.model.Shipping
import com.airwallex.android.databinding.ActivityAddCardBinding
import com.airwallex.android.ui.extension.getExtraArgs
import com.airwallex.risk.AirwallexRisk
import kotlinx.coroutines.launch

/**
Expand Down Expand Up @@ -102,6 +103,7 @@ internal class AddPaymentMethodActivity : AirwallexCheckoutBaseActivity(), Track

private fun onSaveCard() {
AnalyticsLogger.logAction("tap_pay_button")
AirwallexRisk.log(event = "click_payment_button", screen = "page_create_card")

val card = viewBinding.cardWidget.paymentMethodCard ?: return
val resultHandler: (AirwallexPaymentStatus) -> Unit = { result ->
Expand Down Expand Up @@ -241,5 +243,23 @@ internal class AddPaymentMethodActivity : AirwallexCheckoutBaseActivity(), Track

viewBinding.billingGroup.visibility =
if (session.isBillingInformationRequired) View.VISIBLE else View.GONE

airwallexRiskLog()
}

private fun airwallexRiskLog() {
AirwallexRisk.log(event = "show_create_card", screen = "page_create_card")
viewBinding.cardWidget.cardNumberClickCallback = {
AirwallexRisk.log(event = "input_card_number", screen = "page_create_card")
}
viewBinding.cardWidget.holderNameClickCallback = {
AirwallexRisk.log(event = "input_card_holder_name", screen = "page_create_card")
}
viewBinding.cardWidget.expiresClickCallback = {
AirwallexRisk.log(event = "input_card_expiry", screen = "page_create_card")
}
viewBinding.cardWidget.cvcClickCallback = {
AirwallexRisk.log(event = "input_card_cvc", screen = "page_create_card")
}
}
}
20 changes: 20 additions & 0 deletions airwallex/src/main/java/com/airwallex/android/view/CardWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class CardWidget(context: Context, attrs: AttributeSet?) : LinearLayout(context,
}

var cardChangeCallback: () -> Unit = {}
var cardNumberClickCallback: () -> Unit = {}
var holderNameClickCallback: () -> Unit = {}
var expiresClickCallback: () -> Unit = {}
var cvcClickCallback: () -> Unit = {}

val paymentMethodCard: PaymentMethod.Card?
get() {
Expand Down Expand Up @@ -96,6 +100,22 @@ class CardWidget(context: Context, attrs: AttributeSet?) : LinearLayout(context,
listenTextChanged()
listenFocusChanged()
listenCompletionCallback()
listenClick()
}

private fun listenClick() {
cardNumberTextInputLayout.setOnInputEditTextClickListener {
cardNumberClickCallback.invoke()
}
cardNameTextInputLayout.setOnInputEditTextClickListener {
holderNameClickCallback.invoke()
}
expiryTextInputLayout.setOnInputEditTextClickListener {
expiresClickCallback.invoke()
}
cvcTextInputLayout.setOnInputEditTextClickListener {
cvcClickCallback.invoke()
}
}

private fun listenTextChanged() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.airwallex.android.databinding.ActivityPaymentMethodsBinding
import com.airwallex.android.ui.extension.getExtraArgs
import com.airwallex.android.view.PaymentMethodsViewModel.Companion.COUNTRY_CODE
import com.airwallex.android.view.util.findWithType
import com.airwallex.risk.AirwallexRisk
import kotlinx.coroutines.launch

class PaymentMethodsActivity : AirwallexCheckoutBaseActivity(), TrackablePage {
Expand Down Expand Up @@ -80,6 +81,7 @@ class PaymentMethodsActivity : AirwallexCheckoutBaseActivity(), TrackablePage {
availablePaymentMethodTypes: List<AvailablePaymentMethodType>,
availablePaymentConsents: List<PaymentConsent>
) {
AirwallexRisk.log(event = "show_payment_method_list", screen = "page_payment_method_list")
val viewManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
paymentMethodsAdapter = PaymentMethodsAdapter(
availablePaymentMethodTypes = availablePaymentMethodTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ internal class PaymentMethodsViewModel(

else -> null
}?.let { clientSecret ->
TokenManager.updateClientSecret(clientSecret)
coroutineScope {
val retrieveConsents = async {
customerId?.takeIf { needRequestConsent() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.airwallex.android.view.inputs

import android.annotation.SuppressLint
import android.content.Context
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.text.method.KeyListener
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
Expand Down Expand Up @@ -96,6 +98,16 @@ open class AirwallexTextInputLayout @JvmOverloads constructor(
teInput.keyListener = input
}

@SuppressLint("ClickableViewAccessibility")
fun setOnInputEditTextClickListener(listener: OnClickListener) {
teInput.setOnTouchListener { v, event ->
if (event?.action == MotionEvent.ACTION_UP) {
listener.onClick(v)
}
false
}
}

fun setInputType(type: Int) {
teInput.inputType = type
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ class PaymentMethodsViewModelTest {
.setRequireCvc(true)
.setPaymentMethods(paymentMethods)
.build()

mockkObject(TokenManager)
coEvery { TokenManager.updateClientSecret(any()) } returns Unit
val oneOffSession = AirwallexPaymentSession.Builder(
PaymentIntent(
id = "id",
Expand Down
1 change: 1 addition & 0 deletions components-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutinesVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.2"
implementation "io.github.airwallex:AirTracker:1.0.3"
api "io.github.airwallex:RiskSdk:1.0.6"

// test
testImplementation 'org.json:json:20231013'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import com.airwallex.android.core.extension.confirmGooglePayIntent
import com.airwallex.android.core.extension.createCardPaymentMethod
import com.airwallex.android.core.log.AnalyticsLogger
import com.airwallex.android.core.model.*
import com.airwallex.risk.AirwallexRisk
import com.airwallex.risk.RiskConfiguration
import com.airwallex.risk.Tenant
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.math.BigDecimal
Expand Down Expand Up @@ -1188,6 +1191,14 @@ class Airwallex internal constructor(
clientSecretProvider?.let {
ClientSecretRepository.init(it)
}
AirwallexRisk.start(
applicationContext = application,
accountId = null,
configuration = RiskConfiguration(
isProduction = configuration.environment == Environment.PRODUCTION,
tenant = Tenant.PA
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ class AirwallexPaymentSession internal constructor(
private var hidePaymentConsents: Boolean = false
private var paymentMethods: List<String>? = null

init {
paymentIntent.clientSecret?.apply {
TokenManager.updateClientSecret(this)
}
}

fun setRequireBillingInformation(requiresBillingInformation: Boolean): Builder = apply {
this.isBillingInformationRequired = requiresBillingInformation
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ class AirwallexRecurringWithIntentSession internal constructor(
private var autoCapture: Boolean = true
private var paymentMethods: List<String>? = null

init {
paymentIntent.clientSecret?.apply {
TokenManager.updateClientSecret(this)
}
}

fun setRequireBillingInformation(requiresBillingInformation: Boolean): Builder = apply {
this.isBillingInformationRequired = requiresBillingInformation
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ class ClientSecretRepository(private val provider: ClientSecretProvider) {
suspend fun retrieveClientSecret(customerId: String): ClientSecret {
return withContext(Dispatchers.IO) {
try {
provider.provideClientSecret(customerId)
val clientSecret = provider.provideClientSecret(customerId)
TokenManager.updateClientSecret(clientSecret.value)
clientSecret
} catch (throwable: Throwable) {
throw AirwallexCheckoutException(
message = "Could not retrieve client secret from the given provider", e = throwable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,36 @@ package com.airwallex.android.core

import android.util.Base64
import com.airwallex.android.core.log.AnalyticsLogger
import com.airwallex.risk.AirwallexRisk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject
import kotlin.properties.Delegates

object TokenManager {
private var clientSecret: String? = null
var accountId: String? by Delegates.observable(null) { _, _, newId ->
AnalyticsLogger.updateAccountId(newId)
AirwallexRisk.setAccountId(newId)
}

fun updateClientSecret(clientSecret: String) {
if (clientSecret != this.clientSecret) {
this.clientSecret = clientSecret
val body = clientSecret.split(".").getOrNull(1)
if (body != null) {
runCatching {
accountId = JSONObject(
String(
Base64.decode(
body,
Base64.DEFAULT
CoroutineScope(Dispatchers.Main).launch {
if (clientSecret != this@TokenManager.clientSecret) {
this@TokenManager.clientSecret = clientSecret
val body = clientSecret.split(".").getOrNull(1)
if (body != null) {
runCatching {
accountId = JSONObject(
String(
Base64.decode(
body,
Base64.DEFAULT
)
)
)
).getString("account_id")
).getString("account_id")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.airwallex.android.core

import android.util.Log
import com.airwallex.android.core.log.AnalyticsLogger
import com.airwallex.android.core.model.ClientSecret
import com.airwallex.risk.AirwallexRisk
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkConstructor
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import org.junit.After
import org.junit.Before
import java.util.Date
import kotlin.test.Test
import kotlin.test.assertEquals

@ExperimentalCoroutinesApi
class ClientSecretRepositoryTest {

@Before
fun setup() {
mockkObject(TokenManager)
mockkObject(AnalyticsLogger)
mockkObject(AirwallexRisk)
mockkStatic(Log::class)
every { Log.d(any(), any()) } returns 0
}

@After
fun tearDown() {
unmockkAll()
}

@Test
fun retrieveClientSecretTest() = runBlocking {
val provider = mockk<ClientSecretProvider>()
val secret = ClientSecret(
value = "eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2ODAwMzcsImV4cCI6MTcyMDY4MzYzNywidHlwZSI6ImNsaWVudC1zZWNyZXQiLCJwYWRjIjoiTkwiLCJhY2NvdW50X2lkIjoiMjFmZjBiZTctMjMyOS00MzA1LWJiNDItNzQxNjJlOTNiZGNhIiwiaW50ZW50X2lkIjoiaW50X25sc3RibGh4cGd4eGhodm53ZWQiLCJjdXN0b21lcl9pZCI6ImN1c19ubHN0OHQ0NGZneHFwbzVrd2htIiwiYnVzaW5lc3NfbmFtZSI6Ikhvd2UtQWJib3R0In0.fnun8MSu0YrBzBLywk5k7_E0qRSr4DFp7c_GAnn1Ubw",
expiredTime = Date()
)
mockkConstructor(JSONObject::class)
every { provider.provideClientSecret(any()) } returns secret
every { TokenManager.updateClientSecret(any()) } answers {
TokenManager.accountId = "21ff0be7-2329-4305-bb42-74162e93bdca"
}

val repository = ClientSecretRepository(provider)
repository.retrieveClientSecret("")

assertEquals("21ff0be7-2329-4305-bb42-74162e93bdca", TokenManager.accountId)
verify { AnalyticsLogger.updateAccountId("21ff0be7-2329-4305-bb42-74162e93bdca") }
verify { AirwallexRisk.setAccountId("21ff0be7-2329-4305-bb42-74162e93bdca") }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.airwallex.android.core.exception.AirwallexCheckoutException
import com.airwallex.android.core.extension.putIfNotNull
import com.airwallex.android.core.log.AnalyticsLogger
import com.airwallex.android.ui.extension.getExtraArgs
import com.airwallex.risk.AirwallexRisk
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.wallet.AutoResolveHelper
import com.google.android.gms.wallet.PaymentData
Expand All @@ -33,7 +34,7 @@ class GooglePayLauncherActivity : ComponentActivity() {

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

AirwallexRisk.log(event = "show_google_pay", screen = "page_google_pay")
val task = viewModel.getLoadPaymentDataTask()
task.addOnCompleteListener(googlePayLauncher::launch)
}
Expand Down

0 comments on commit e510bc5

Please sign in to comment.