Skip to content

Commit

Permalink
Add support for uploading a file to Stripe (#2066)
Browse files Browse the repository at this point in the history
Summary
- Add `Stripe#createFile()` and `Stripe#createFileSynchronous()`
- Add `StripeFile#url`
- Move `StripeApiRequestExecutor` to `ApiRequestExecutor.Default`
- Make `StripeFileParams#fileLink` private until it's ready for usage

Testing
Add end-to-end tests and unit tests
  • Loading branch information
mshafrir-stripe authored Jan 15, 2020
1 parent 6f9ca14 commit 2da9bca
Show file tree
Hide file tree
Showing 14 changed files with 248 additions and 67 deletions.
38 changes: 38 additions & 0 deletions stripe/src/main/java/com/stripe/android/ApiRequestExecutor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,47 @@ package com.stripe.android

import com.stripe.android.exception.APIConnectionException
import com.stripe.android.exception.InvalidRequestException
import java.io.IOException
import java.net.UnknownHostException

internal interface ApiRequestExecutor {
@Throws(APIConnectionException::class, InvalidRequestException::class, UnknownHostException::class)
fun execute(request: ApiRequest): StripeResponse

@Throws(APIConnectionException::class, InvalidRequestException::class, UnknownHostException::class)
fun execute(request: FileUploadRequest): StripeResponse

/**
* Used by [StripeApiRepository] to make Stripe API requests
*/
class Default internal constructor(
private val logger: Logger = Logger.noop()
) : ApiRequestExecutor {
private val connectionFactory: ConnectionFactory = ConnectionFactory()

@Throws(APIConnectionException::class, InvalidRequestException::class, UnknownHostException::class)
override fun execute(request: ApiRequest): StripeResponse {
return executeInternal(request)
}

@Throws(APIConnectionException::class, InvalidRequestException::class, UnknownHostException::class)
override fun execute(request: FileUploadRequest): StripeResponse {
return executeInternal(request)
}

private fun executeInternal(request: StripeRequest): StripeResponse {
logger.info(request.toString())

connectionFactory.create(request).use {
try {
val stripeResponse = it.response
logger.info(stripeResponse.toString())
return stripeResponse
} catch (e: IOException) {
logger.error("Exception while making Stripe API request", e)
throw APIConnectionException.create(e, request.baseUrl)
}
}
}
}
}
62 changes: 62 additions & 0 deletions stripe/src/main/java/com/stripe/android/Stripe.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import com.stripe.android.model.PiiTokenParams
import com.stripe.android.model.SetupIntent
import com.stripe.android.model.Source
import com.stripe.android.model.SourceParams
import com.stripe.android.model.StripeFile
import com.stripe.android.model.StripeFileParams
import com.stripe.android.model.Token
import com.stripe.android.view.AuthActivityStarter
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -1233,6 +1235,53 @@ class Stripe internal constructor(
).execute()
}

/**
* [Create a file](https://stripe.com/docs/api/files/create) asynchronously
*
* @param fileParams the [StripeFileParams] used to create the [StripeFile]
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
* @param callback a [ApiResultCallback] to receive the result or error
*/
@JvmOverloads
fun createFile(
fileParams: StripeFileParams,
idempotencyKey: String? = null,
callback: ApiResultCallback<StripeFile>
) {
CreateFileTask(
stripeRepository,
fileParams,
ApiRequest.Options(
apiKey = publishableKey,
stripeAccount = stripeAccountId,
idempotencyKey = idempotencyKey
),
workScope,
callback
).execute()
}

/**
* [Create a file](https://stripe.com/docs/api/files/create) synchronously
*
* @param fileParams the [StripeFileParams] used to create the [StripeFile]
* @param idempotencyKey optional, see [Idempotent Requests](https://stripe.com/docs/api/idempotent_requests)
*/
@JvmOverloads
fun createFileSynchronous(
fileParams: StripeFileParams,
idempotencyKey: String? = null
): StripeFile {
return stripeRepository.createFile(
fileParams,
ApiRequest.Options(
apiKey = publishableKey,
stripeAccount = stripeAccountId,
idempotencyKey = idempotencyKey
)
)
}

private class CreateSourceTask internal constructor(
private val stripeRepository: StripeRepository,
private val sourceParams: SourceParams,
Expand Down Expand Up @@ -1287,6 +1336,19 @@ class Stripe internal constructor(
}
}

private class CreateFileTask internal constructor(
private val stripeRepository: StripeRepository,
private val fileParams: StripeFileParams,
private val options: ApiRequest.Options,
workScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
callback: ApiResultCallback<StripeFile>
) : ApiOperation<StripeFile>(workScope, callback) {
@Throws(StripeException::class)
override suspend fun getResult(): StripeFile {
return stripeRepository.createFile(fileParams, options)
}
}

companion object {
@JvmField
val API_VERSION: String = ApiVersion.get().code
Expand Down
36 changes: 35 additions & 1 deletion stripe/src/main/java/com/stripe/android/StripeApiRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.stripe.android.model.ShippingInformation
import com.stripe.android.model.Source
import com.stripe.android.model.SourceParams
import com.stripe.android.model.Stripe3ds2AuthResult
import com.stripe.android.model.StripeFile
import com.stripe.android.model.StripeFileParams
import com.stripe.android.model.StripeIntent
import com.stripe.android.model.StripeModel
import com.stripe.android.model.Token
Expand All @@ -33,6 +35,7 @@ import com.stripe.android.model.parsers.PaymentMethodJsonParser
import com.stripe.android.model.parsers.SetupIntentJsonParser
import com.stripe.android.model.parsers.SourceJsonParser
import com.stripe.android.model.parsers.Stripe3ds2AuthResultJsonParser
import com.stripe.android.model.parsers.StripeFileJsonParser
import com.stripe.android.model.parsers.TokenJsonParser
import java.io.IOException
import java.net.HttpURLConnection
Expand All @@ -48,7 +51,7 @@ internal class StripeApiRepository @JvmOverloads internal constructor(
context: Context,
private val appInfo: AppInfo? = null,
private val logger: Logger = Logger.noop(),
private val stripeApiRequestExecutor: ApiRequestExecutor = StripeApiRequestExecutor(logger),
private val stripeApiRequestExecutor: ApiRequestExecutor = ApiRequestExecutor.Default(logger),
private val fireAndForgetRequestExecutor: FireAndForgetRequestExecutor =
StripeFireAndForgetRequestExecutor(logger),
private val fingerprintRequestFactory: FingerprintRequestFactory =
Expand Down Expand Up @@ -792,6 +795,16 @@ internal class StripeApiRepository @JvmOverloads internal constructor(
.execute()
}

override fun createFile(
fileParams: StripeFileParams,
requestOptions: ApiRequest.Options
): StripeFile {
val response = makeFileUploadRequest(
FileUploadRequest(fileParams, requestOptions, appInfo)
)
return StripeFileJsonParser().parse(response.responseJson)
}

/**
* @return `https://api.stripe.com/v1/payment_methods/:id/detach`
*/
Expand Down Expand Up @@ -872,6 +885,27 @@ internal class StripeApiRepository @JvmOverloads internal constructor(
return response
}

@VisibleForTesting
@Throws(AuthenticationException::class, InvalidRequestException::class,
APIConnectionException::class, CardException::class, APIException::class)
internal fun makeFileUploadRequest(fileUploadRequest: FileUploadRequest): StripeResponse {
val dnsCacheData = disableDnsCache()

val response = try {
stripeApiRequestExecutor.execute(fileUploadRequest)
} catch (ex: IOException) {
throw APIConnectionException.create(ex, fileUploadRequest.baseUrl)
}

if (response.hasErrorCode()) {
handleApiError(response)
}

resetDnsCacheTtl(dnsCacheData)

return response
}

private fun makeFireAndForgetRequest(request: StripeRequest) {
fireAndForgetRequestExecutor.executeAsync(request)
}
Expand Down

This file was deleted.

7 changes: 7 additions & 0 deletions stripe/src/main/java/com/stripe/android/StripeRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import com.stripe.android.model.ShippingInformation
import com.stripe.android.model.Source
import com.stripe.android.model.SourceParams
import com.stripe.android.model.Stripe3ds2AuthResult
import com.stripe.android.model.StripeFile
import com.stripe.android.model.StripeFileParams
import com.stripe.android.model.StripeIntent
import com.stripe.android.model.Token
import org.json.JSONException
Expand Down Expand Up @@ -234,4 +236,9 @@ internal interface StripeRepository {
requestOptions: ApiRequest.Options,
callback: ApiResultCallback<Boolean>
)

fun createFile(
fileParams: StripeFileParams,
requestOptions: ApiRequest.Options
): StripeFile
}
11 changes: 9 additions & 2 deletions stripe/src/main/java/com/stripe/android/model/StripeFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlinx.android.parcel.Parcelize
* [The file object](https://stripe.com/docs/api/files/object)
*/
@Parcelize
internal data class StripeFile internal constructor(
data class StripeFile internal constructor(
/**
* Unique identifier for the object.
*
Expand Down Expand Up @@ -56,5 +56,12 @@ internal data class StripeFile internal constructor(
*
* [type](https://stripe.com/docs/api/files/object#file_object-type)
*/
val type: String? = null
val type: String? = null,

/**
* The URL from which the file can be downloaded using your live secret API key.
*
* [url](https://stripe.com/docs/api/files/object#file_object-url)
*/
val url: String? = null
) : StripeModel
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import kotlinx.android.parcel.Parcelize
* The request should contain the file you would like to upload, as well as the parameters for
* creating a file.
*/
internal data class StripeFileParams(
data class StripeFileParams constructor(
/**
* A file to upload. The file should follow the specifications of RFC 2388 (which defines file
* transfers for the `multipart/form-data` protocol).
Expand All @@ -25,24 +25,24 @@ internal data class StripeFileParams(
*
* [purpose](https://stripe.com/docs/api/files/create#create_file-purpose)
*/
internal val purpose: StripeFilePurpose,

internal val purpose: StripeFilePurpose
) {
/**
* Optional parameters to automatically create a
* [file link](https://stripe.com/docs/api/files/create#file_links) for the newly created file.
*
* [file_link_data]](https://stripe.com/docs/api/files/create#create_file-file_link_data)
*/
private val fileLink: FileLink? = null
) {

/**
* Optional parameters to automatically create a
* [file link](https://stripe.com/docs/api/files/create#file_links) for the newly created file.
*
* [file_link_data]](https://stripe.com/docs/api/files/create#create_file-file_link_data)
*/
@Parcelize
data class FileLink(
data class FileLink @JvmOverloads constructor(
/**
* Set this to `true` to create a file link for the newly created file. Creating a link is
* only possible when the file’s `purpose` is one of the following: `business_icon`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package com.stripe.android.model
*
* [purpose](https://stripe.com/docs/api/files/create#create_file-purpose)
*/
internal enum class StripeFilePurpose(internal val code: String) {
enum class StripeFilePurpose(internal val code: String) {
BusinessIcon("business_icon"),
BusinessLogo("business_logo"),
CustomerSignature("customer_signature"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ internal class StripeFileJsonParser : ModelJsonParser<StripeFile> {
purpose = StripeFilePurpose.fromCode(optString(json, FIELD_PURPOSE)),
size = optInteger(json, FIELD_SIZE),
title = optString(json, FIELD_TITLE),
type = optString(json, FIELD_TYPE)
type = optString(json, FIELD_TYPE),
url = optString(json, FIELD_URL)
)
}

Expand All @@ -28,5 +29,6 @@ internal class StripeFileJsonParser : ModelJsonParser<StripeFile> {
private const val FIELD_SIZE = "size"
private const val FIELD_TITLE = "title"
private const val FIELD_TYPE = "type"
private const val FIELD_URL = "url"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.stripe.android.model.ShippingInformation
import com.stripe.android.model.Source
import com.stripe.android.model.SourceParams
import com.stripe.android.model.Stripe3ds2AuthResult
import com.stripe.android.model.StripeFile
import com.stripe.android.model.StripeFileParams
import com.stripe.android.model.StripeIntent
import com.stripe.android.model.Token

Expand Down Expand Up @@ -235,4 +237,11 @@ internal abstract class AbsFakeStripeRepository : StripeRepository {
callback: ApiResultCallback<Boolean>
) {
}

override fun createFile(
fileParams: StripeFileParams,
requestOptions: ApiRequest.Options
): StripeFile {
return StripeFile()
}
}
Loading

0 comments on commit 2da9bca

Please sign in to comment.