Skip to content

Commit

Permalink
Add support for idempotency key on Stripe token API requests (#1775)
Browse files Browse the repository at this point in the history
Fixes #1025
  • Loading branch information
mshafrir-stripe authored Nov 3, 2019
1 parent a34af12 commit 2c2fbf7
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class AsyncTaskTokenController(
}

progressDialogController.show(R.string.progressMessage)
stripe.createToken(cardToSave, tokenCallback)
stripe.createToken(cardToSave, callback = tokenCallback)
}

private class TokenCallbackImpl constructor(
Expand Down
9 changes: 7 additions & 2 deletions stripe/src/main/java/com/stripe/android/ApiRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ internal class ApiRequest internal constructor(
options.stripeAccount?.let {
mapOf("Stripe-Account" to it)
}.orEmpty()
).plus(
options.idempotencyKey?.let {
mapOf("Idempotency-Key" to it)
}.orEmpty()
).plus(
languageTag?.let { mapOf("Accept-Language" to it) }.orEmpty()
)
Expand Down Expand Up @@ -67,7 +71,7 @@ internal class ApiRequest internal constructor(

@Throws(UnsupportedEncodingException::class, InvalidRequestException::class)
override fun getOutputBytes(): ByteArray {
return createQuery().toByteArray(charset(CHARSET))
return query.toByteArray(charset(CHARSET))
}

override fun toString(): String {
Expand All @@ -93,7 +97,8 @@ internal class ApiRequest internal constructor(
*/
internal data class Options internal constructor(
val apiKey: String,
val stripeAccount: String? = null
internal val stripeAccount: String? = null,
internal val idempotencyKey: String? = null
) {
init {
ApiKeyValidator().requireValid(apiKey)
Expand Down
85 changes: 71 additions & 14 deletions stripe/src/main/java/com/stripe/android/Stripe.kt
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ class Stripe internal constructor(
* `GET /v1/sources/:id`
*
* @param sourceId the [Source.id] field of the desired Source object
* @param clientSecret the [Source.getClientSecret] field of the desired Source object
* @param clientSecret the [Source.clientSecret] field of the desired Source object
* @return a [Source] if one could be found based on the input params, or `null` if
* no such Source could be found.
* @throws AuthenticationException failure to properly authenticate yourself (check your key)
Expand Down Expand Up @@ -537,18 +537,22 @@ class Stripe internal constructor(
* `POST /v1/tokens`
*
* @param accountParams the [AccountParams] used to create this token
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
* @param callback a [ApiResultCallback] to receive the result or error
*/
@UiThread
@JvmOverloads
fun createAccountToken(
accountParams: AccountParams,
idempotencyKey: String? = null,
callback: ApiResultCallback<Token>
) {
val params = accountParams.toParamMap()
.plus(stripeNetworkUtils.createUidParams())
createTokenFromParams(
params,
Token.TokenType.ACCOUNT,
idempotencyKey,
callback
)
}
Expand All @@ -561,7 +565,10 @@ class Stripe internal constructor(
* `POST /v1/tokens`
*
* @param accountParams params to use for this token.
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
*
* @return a [Token] that can be used for this account.
*
* @throws AuthenticationException failure to properly authenticate yourself (check your key)
* @throws InvalidRequestException your request has invalid parameters
* @throws APIConnectionException failure to connect to Stripe's API
Expand All @@ -571,11 +578,15 @@ class Stripe internal constructor(
@Throws(AuthenticationException::class, InvalidRequestException::class,
APIConnectionException::class, APIException::class)
@WorkerThread
fun createAccountTokenSynchronous(accountParams: AccountParams): Token? {
@JvmOverloads
fun createAccountTokenSynchronous(
accountParams: AccountParams,
idempotencyKey: String? = null
): Token? {
return try {
stripeRepository.createToken(
accountParams.toParamMap(),
ApiRequest.Options(publishableKey, stripeAccountId),
ApiRequest.Options(publishableKey, stripeAccountId, idempotencyKey),
Token.TokenType.ACCOUNT
)
} catch (exception: CardException) {
Expand All @@ -591,18 +602,22 @@ class Stripe internal constructor(
* `POST /v1/tokens`
*
* @param bankAccount the [BankAccount] used to create this token
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
* @param callback a [ApiResultCallback] to receive the result or error
*/
@UiThread
@JvmOverloads
fun createBankAccountToken(
bankAccount: BankAccount,
idempotencyKey: String? = null,
callback: ApiResultCallback<Token>
) {
val params = bankAccount.toParamMap()
.plus(stripeNetworkUtils.createUidParams())
createTokenFromParams(
params,
Token.TokenType.BANK_ACCOUNT,
idempotencyKey,
callback
)
}
Expand All @@ -615,7 +630,10 @@ class Stripe internal constructor(
* `POST /v1/tokens`
*
* @param bankAccount the [Card] to use for this token
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
*
* @return a [Token] that can be used for this [BankAccount]
*
* @throws AuthenticationException failure to properly authenticate yourself (check your key)
* @throws InvalidRequestException your request has invalid parameters
* @throws APIConnectionException failure to connect to Stripe's API
Expand All @@ -627,34 +645,41 @@ class Stripe internal constructor(
@Throws(AuthenticationException::class, InvalidRequestException::class,
APIConnectionException::class, CardException::class, APIException::class)
@WorkerThread
fun createBankAccountTokenSynchronous(bankAccount: BankAccount): Token? {
@JvmOverloads
fun createBankAccountTokenSynchronous(
bankAccount: BankAccount,
idempotencyKey: String? = null
): Token? {
val params = bankAccount.toParamMap()
.plus(stripeNetworkUtils.createUidParams())
return stripeRepository.createToken(
params,
ApiRequest.Options(publishableKey, stripeAccountId),
ApiRequest.Options(publishableKey, stripeAccountId, idempotencyKey),
Token.TokenType.BANK_ACCOUNT
)
}

/**
* Create a PII token asynchronously.
*
*
* See [Create a PII account token](https://stripe.com/docs/api/tokens/create_pii).
* `POST /v1/tokens`
*
* @param personalId the personal id used to create this token
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
* @param callback a [ApiResultCallback] to receive the result or error
*/
@UiThread
@JvmOverloads
fun createPiiToken(
personalId: String,
idempotencyKey: String? = null,
callback: ApiResultCallback<Token>
) {
createTokenFromParams(
PiiTokenParams(personalId).toParamMap(),
Token.TokenType.PII,
idempotencyKey,
callback
)
}
Expand All @@ -667,7 +692,10 @@ class Stripe internal constructor(
* `POST /v1/tokens`
*
* @param personalId the personal ID to use for this token
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
*
* @return a [Token] that can be used for this card
*
* @throws AuthenticationException failure to properly authenticate yourself (check your key)
* @throws InvalidRequestException your request has invalid parameters
* @throws APIConnectionException failure to connect to Stripe's API
Expand All @@ -677,10 +705,14 @@ class Stripe internal constructor(
@Throws(AuthenticationException::class, InvalidRequestException::class,
APIConnectionException::class, CardException::class, APIException::class)
@WorkerThread
fun createPiiTokenSynchronous(personalId: String): Token? {
@JvmOverloads
fun createPiiTokenSynchronous(
personalId: String,
idempotencyKey: String? = null
): Token? {
return stripeRepository.createToken(
PiiTokenParams(personalId).toParamMap(),
ApiRequest.Options(publishableKey, stripeAccountId),
ApiRequest.Options(publishableKey, stripeAccountId, idempotencyKey),
Token.TokenType.PII
)
}
Expand All @@ -692,13 +724,20 @@ class Stripe internal constructor(
* `POST /v1/tokens`
*
* @param card the [Card] used to create this payment token
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
* @param callback a [ApiResultCallback] to receive the result or error
*/
@UiThread
fun createToken(card: Card, callback: ApiResultCallback<Token>) {
@JvmOverloads
fun createToken(
card: Card,
idempotencyKey: String? = null,
callback: ApiResultCallback<Token>
) {
createTokenFromParams(
stripeNetworkUtils.createCardTokenParams(card),
Token.TokenType.CARD,
idempotencyKey,
callback
)
}
Expand All @@ -711,6 +750,8 @@ class Stripe internal constructor(
* `POST /v1/tokens`
*
* @param card the [Card] to use for this token
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
*
* @return a [Token] that can be used for this card
* @throws AuthenticationException failure to properly authenticate yourself (check your key)
* @throws InvalidRequestException your request has invalid parameters
Expand All @@ -722,10 +763,14 @@ class Stripe internal constructor(
@Throws(AuthenticationException::class, InvalidRequestException::class,
APIConnectionException::class, CardException::class, APIException::class)
@WorkerThread
fun createCardTokenSynchronous(card: Card): Token? {
@JvmOverloads
fun createCardTokenSynchronous(
card: Card,
idempotencyKey: String? = null
): Token? {
return stripeRepository.createToken(
stripeNetworkUtils.createCardTokenParams(card),
ApiRequest.Options(publishableKey, stripeAccountId),
ApiRequest.Options(publishableKey, stripeAccountId, idempotencyKey),
Token.TokenType.CARD
)
}
Expand All @@ -736,16 +781,20 @@ class Stripe internal constructor(
* `POST /v1/tokens`
*
* @param cvc the CVC used to create this token
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
* @param callback a [ApiResultCallback] to receive the result or error
*/
@UiThread
@JvmOverloads
fun createCvcUpdateToken(
@Size(min = 3, max = 4) cvc: String,
idempotencyKey: String? = null,
callback: ApiResultCallback<Token>
) {
createTokenFromParams(
CvcTokenParams(cvc).toParamMap(),
Token.TokenType.CVC_UPDATE,
idempotencyKey,
callback
)
}
Expand All @@ -757,7 +806,10 @@ class Stripe internal constructor(
* `POST /v1/tokens`
*
* @param cvc the CVC to use for this token
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
*
* @return a [Token] that can be used for this card
*
* @throws AuthenticationException failure to properly authenticate yourself (check your key)
* @throws InvalidRequestException your request has invalid parameters
* @throws APIConnectionException failure to connect to Stripe's API
Expand All @@ -767,22 +819,27 @@ class Stripe internal constructor(
@Throws(AuthenticationException::class, InvalidRequestException::class,
APIConnectionException::class, CardException::class, APIException::class)
@WorkerThread
fun createCvcUpdateTokenSynchronous(cvc: String): Token? {
@JvmOverloads
fun createCvcUpdateTokenSynchronous(
cvc: String,
idempotencyKey: String? = null
): Token? {
return stripeRepository.createToken(
CvcTokenParams(cvc).toParamMap(),
ApiRequest.Options(publishableKey, stripeAccountId),
ApiRequest.Options(publishableKey, stripeAccountId, idempotencyKey),
Token.TokenType.CVC_UPDATE
)
}

private fun createTokenFromParams(
tokenParams: Map<String, Any>,
@Token.TokenType tokenType: String,
idempotencyKey: String? = null,
callback: ApiResultCallback<Token>
) {
tokenCreator.create(
tokenParams,
ApiRequest.Options(publishableKey, stripeAccountId),
ApiRequest.Options(publishableKey, stripeAccountId, idempotencyKey),
tokenType, null,
callback
)
Expand Down
21 changes: 11 additions & 10 deletions stripe/src/main/java/com/stripe/android/StripeRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ internal abstract class StripeRequest(
* @return if the HTTP method is [Method.GET], return URL with query string;
* otherwise, return the URL
*/
val url: String
internal val url: String
@Throws(UnsupportedEncodingException::class, InvalidRequestException::class)
get() = if (Method.GET == method) urlWithQuery() else baseUrl

val contentType: String
internal val contentType: String
get() = "$mimeType; charset=$CHARSET"

val headers: Map<String, String>
internal val headers: Map<String, String>
get() {
return createHeaders()
.plus(HEADER_USER_AGENT to getUserAgent())
Expand All @@ -40,21 +40,22 @@ internal abstract class StripeRequest(
@Throws(UnsupportedEncodingException::class, InvalidRequestException::class)
internal abstract fun getOutputBytes(): ByteArray

val baseHashCode: Int
internal val baseHashCode: Int
get() = Objects.hash(method, baseUrl, params)

internal abstract fun createHeaders(): Map<String, String>

@Throws(InvalidRequestException::class, UnsupportedEncodingException::class)
fun createQuery(): String {
return flattenParams(params).joinToString("&") {
urlEncodePair(it.key, it.value)
internal val query: String
@Throws(InvalidRequestException::class, UnsupportedEncodingException::class)
get() {
return flattenParams(params).joinToString("&") {
urlEncodePair(it.key, it.value)
}
}
}

@Throws(InvalidRequestException::class, UnsupportedEncodingException::class)
private fun urlWithQuery(): String {
val query = createQuery()
val query = this.query
return if (query.isEmpty()) {
baseUrl
} else {
Expand Down
Loading

0 comments on commit 2c2fbf7

Please sign in to comment.