Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.paypal.android.cardpayments

import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.corepayments.graphql.GraphQLError
import kotlinx.serialization.InternalSerializationApi

internal object CardError {

Expand Down Expand Up @@ -29,6 +30,7 @@ internal object CardError {
errorDescription = cause.message ?: "Unable to Browser Switch"
)

@OptIn(InternalSerializationApi::class)
fun updateSetupTokenResponseBodyMissing(errors: List<GraphQLError>?, correlationId: String?) = PayPalSDKError(
0,
"Error updating setup token: $errors",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ package com.paypal.android.cardpayments

import android.content.Context
import androidx.annotation.RawRes
import com.paypal.android.cardpayments.api.UpdateSetupTokenResponse
import com.paypal.android.cardpayments.api.UpdateSetupTokenVariables
import com.paypal.android.cardpayments.api.VaultBillingAddress
import com.paypal.android.cardpayments.api.VaultCard
import com.paypal.android.cardpayments.api.VaultPaymentSource
import com.paypal.android.corepayments.CoreConfig
import com.paypal.android.corepayments.LoadRawResourceResult
import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.corepayments.ResourceLoader
import com.paypal.android.corepayments.graphql.GraphQLClient
import com.paypal.android.corepayments.graphql.GraphQLRequest
import com.paypal.android.corepayments.graphql.GraphQLResult
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import kotlinx.serialization.InternalSerializationApi

internal class DataVaultPaymentMethodTokensAPI internal constructor(
private val coreConfig: CoreConfig,
Expand All @@ -36,6 +40,7 @@ internal class DataVaultPaymentMethodTokensAPI internal constructor(
}
}

@OptIn(InternalSerializationApi::class)
private suspend fun sendUpdateSetupTokenGraphQLRequest(
query: String,
setupTokenId: String,
Expand All @@ -45,47 +50,48 @@ internal class DataVaultPaymentMethodTokensAPI internal constructor(
val cardNumber = card.number.replace("\\s".toRegex(), "")
val cardExpiry = "${card.expirationYear}-${card.expirationMonth}"

val cardJSON = JSONObject()
.put("number", cardNumber)
.put("expiry", cardExpiry)

card.cardholderName?.let { cardJSON.put("name", it) }
cardJSON.put("securityCode", card.securityCode)

card.billingAddress?.apply {
val billingAddressJSON = JSONObject()
.put("addressLine1", streetAddress)
.put("addressLine2", extendedAddress)
.put("adminArea1", region)
.put("adminArea2", locality)
.put("postalCode", postalCode)
.put("countryCode", countryCode)
cardJSON.put("billingAddress", billingAddressJSON)
val vaultBillingAddress = card.billingAddress?.let {
VaultBillingAddress(
addressLine1 = it.streetAddress,
addressLine2 = it.extendedAddress,
adminArea1 = it.region,
adminArea2 = it.locality,
postalCode = it.postalCode,
countryCode = it.countryCode
)
}

val paymentSourceJSON = JSONObject()
paymentSourceJSON.put("card", cardJSON)
val vaultCard = VaultCard(
number = cardNumber,
expiry = cardExpiry,
name = card.cardholderName,
securityCode = card.securityCode,
billingAddress = vaultBillingAddress
Copy link
Collaborator

@KunJeongPark KunJeongPark Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have billingAddress for Card in iOS for vaulting.
I wonder if that's a requirement.
Maybe we need to add it as an optional field.
The endpoint works without it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is an optional field, nullable in kotlin

)

val paymentSource = VaultPaymentSource(card = vaultCard)

val variables = JSONObject()
.put("clientId", coreConfig.clientId)
.put("vaultSetupToken", setupTokenId)
.put("paymentSource", paymentSourceJSON)
val variables = UpdateSetupTokenVariables(
clientId = coreConfig.clientId,
vaultSetupToken = setupTokenId,
paymentSource = paymentSource
)

val graphQLRequest = JSONObject()
.put("query", query)
.put("variables", variables)
val graphQLRequest = GraphQLRequest(query, variables, "UpdateVaultSetupToken")
val graphQLResponse =
graphQLClient.send(graphQLRequest, queryName = "UpdateVaultSetupToken")
graphQLClient.send<UpdateSetupTokenResponse, UpdateSetupTokenVariables>(graphQLRequest)
return when (graphQLResponse) {
is GraphQLResult.Success -> {
val responseJSON = graphQLResponse.data
if (responseJSON == null) {
val error = graphQLResponse.run {
CardError.updateSetupTokenResponseBodyMissing(errors, correlationId)
}
val response = graphQLResponse.response
val responseData = response.data
if (responseData == null) {
val error = CardError.updateSetupTokenResponseBodyMissing(
response.errors,
graphQLResponse.correlationId
)
UpdateSetupTokenResult.Failure(error)
} else {
parseSuccessfulUpdateSuccessJSON(responseJSON, graphQLResponse.correlationId)
parseSuccessfulUpdateSuccess(responseData, graphQLResponse.correlationId)
}
}

Expand All @@ -95,38 +101,28 @@ internal class DataVaultPaymentMethodTokensAPI internal constructor(
}
}

private fun parseSuccessfulUpdateSuccessJSON(
responseBody: JSONObject,
@OptIn(InternalSerializationApi::class)
private fun parseSuccessfulUpdateSuccess(
responseBody: UpdateSetupTokenResponse,
correlationId: String?
): UpdateSetupTokenResult {
return try {
val setupTokenJSON = responseBody.getJSONObject("updateVaultSetupToken")
val status = setupTokenJSON.getString("status")
return runCatching {
val setupToken = responseBody.updateVaultSetupToken
val status = setupToken.status
val approveHref = if (status == "PAYER_ACTION_REQUIRED") {
findLinkHref(setupTokenJSON, "approve")
setupToken.links?.find { it.rel == "approve" }?.href
} else {
null
}
UpdateSetupTokenResult.Success(
setupTokenId = setupTokenJSON.getString("id"),
setupTokenId = setupToken.id,
status = status,
approveHref = approveHref
)
} catch (jsonError: JSONException) {
val message = "Update Setup Token Failed: GraphQL JSON body was invalid."
val error = PayPalSDKError(0, message, correlationId, reason = jsonError)
UpdateSetupTokenResult.Failure(error)
}
}

private fun findLinkHref(responseJSON: JSONObject, rel: String): String? {
val linksJSON = responseJSON.optJSONArray("links") ?: JSONArray()
for (i in 0 until linksJSON.length()) {
val link = linksJSON.getJSONObject(i)
if (link.optString("rel") == rel) {
return link.optString("href")
}
}.getOrElse { error ->
val message = "Update Setup Token Failed: Response parsing failed."
val sdkError = PayPalSDKError(0, message, correlationId, reason = error)
UpdateSetupTokenResult.Failure(sdkError)
}
return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.paypal.android.cardpayments.api

import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Data classes for Kotlin serialization of update vault setup token GraphQL request
*/
@InternalSerializationApi
@Serializable
internal data class UpdateSetupTokenVariables(
@SerialName("clientId")
val clientId: String,
@SerialName("vaultSetupToken")
val vaultSetupToken: String,
@SerialName("paymentSource")
val paymentSource: VaultPaymentSource
)

@InternalSerializationApi
@Serializable
internal data class VaultPaymentSource(
val card: VaultCard
)

@InternalSerializationApi
@Serializable
internal data class VaultCard(
val number: String,
val expiry: String,
val name: String? = null,
@SerialName("securityCode")
val securityCode: String,
@SerialName("billingAddress")
val billingAddress: VaultBillingAddress? = null
)

@InternalSerializationApi
@Serializable
internal data class VaultBillingAddress(
@SerialName("addressLine1")
val addressLine1: String? = null,
@SerialName("addressLine2")
val addressLine2: String? = null,
@SerialName("adminArea1")
val adminArea1: String? = null,
@SerialName("adminArea2")
val adminArea2: String? = null,
@SerialName("postalCode")
val postalCode: String? = null,
@SerialName("countryCode")
val countryCode: String
)

/**
* Data classes for update vault setup token GraphQL response
*/
@InternalSerializationApi
@Serializable
internal data class UpdateSetupTokenResponse(
@SerialName("updateVaultSetupToken")
val updateVaultSetupToken: SetupTokenData
)

@InternalSerializationApi
@Serializable
internal data class SetupTokenData(
val id: String,
val status: String,
val links: List<LinkData>? = null
)

@InternalSerializationApi
@Serializable
internal data class LinkData(
val rel: String,
val href: String
)
Loading