kopia lustrzana https://github.com/ryukoposting/Signal-Android
558 wiersze
16 KiB
Kotlin
558 wiersze
16 KiB
Kotlin
package org.signal.donations
|
|
|
|
import android.net.Uri
|
|
import android.os.Parcelable
|
|
import androidx.annotation.WorkerThread
|
|
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
|
|
import com.fasterxml.jackson.module.kotlin.jsonMapper
|
|
import com.fasterxml.jackson.module.kotlin.kotlinModule
|
|
import com.fasterxml.jackson.module.kotlin.readValue
|
|
import io.reactivex.rxjava3.core.Single
|
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
import kotlinx.parcelize.Parcelize
|
|
import okhttp3.FormBody
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Request
|
|
import okhttp3.Response
|
|
import okio.ByteString
|
|
import org.json.JSONObject
|
|
import org.signal.core.util.logging.Log
|
|
import org.signal.core.util.money.FiatMoney
|
|
import org.signal.donations.json.StripePaymentIntent
|
|
import org.signal.donations.json.StripeSetupIntent
|
|
import java.math.BigDecimal
|
|
import java.util.Locale
|
|
|
|
class StripeApi(
|
|
private val configuration: Configuration,
|
|
private val paymentIntentFetcher: PaymentIntentFetcher,
|
|
private val setupIntentHelper: SetupIntentHelper,
|
|
private val okHttpClient: OkHttpClient
|
|
) {
|
|
|
|
private val objectMapper = jsonMapper {
|
|
addModule(kotlinModule())
|
|
}
|
|
|
|
companion object {
|
|
private val TAG = Log.tag(StripeApi::class.java)
|
|
|
|
private val CARD_NUMBER_KEY = "card[number]"
|
|
private val CARD_MONTH_KEY = "card[exp_month]"
|
|
private val CARD_YEAR_KEY = "card[exp_year]"
|
|
private val CARD_CVC_KEY = "card[cvc]"
|
|
|
|
private const val RETURN_URL_3DS = "sgnlpay://3DS"
|
|
}
|
|
|
|
sealed class CreatePaymentIntentResult {
|
|
data class AmountIsTooSmall(val amount: FiatMoney) : CreatePaymentIntentResult()
|
|
data class AmountIsTooLarge(val amount: FiatMoney) : CreatePaymentIntentResult()
|
|
data class CurrencyIsNotSupported(val currencyCode: String) : CreatePaymentIntentResult()
|
|
data class Success(val paymentIntent: StripeIntentAccessor) : CreatePaymentIntentResult()
|
|
}
|
|
|
|
data class CreateSetupIntentResult(val setupIntent: StripeIntentAccessor)
|
|
|
|
sealed class CreatePaymentSourceFromCardDataResult {
|
|
data class Success(val paymentSource: PaymentSource) : CreatePaymentSourceFromCardDataResult()
|
|
data class Failure(val reason: Throwable) : CreatePaymentSourceFromCardDataResult()
|
|
}
|
|
|
|
fun createSetupIntent(): Single<CreateSetupIntentResult> {
|
|
return setupIntentHelper
|
|
.fetchSetupIntent()
|
|
.map { CreateSetupIntentResult(it) }
|
|
.subscribeOn(Schedulers.io())
|
|
}
|
|
|
|
fun confirmSetupIntent(paymentSource: PaymentSource, setupIntent: StripeIntentAccessor): Single<Secure3DSAction> {
|
|
return Single.fromCallable {
|
|
val paymentMethodId = createPaymentMethodAndParseId(paymentSource)
|
|
|
|
val parameters = mapOf(
|
|
"client_secret" to setupIntent.intentClientSecret,
|
|
"payment_method" to paymentMethodId,
|
|
"return_url" to RETURN_URL_3DS
|
|
)
|
|
|
|
val (nextActionUri, returnUri) = postForm("setup_intents/${setupIntent.intentId}/confirm", parameters).use { response ->
|
|
getNextAction(response)
|
|
}
|
|
|
|
Secure3DSAction.from(nextActionUri, returnUri, paymentMethodId)
|
|
}
|
|
}
|
|
|
|
fun createPaymentIntent(price: FiatMoney, level: Long): Single<CreatePaymentIntentResult> {
|
|
@Suppress("CascadeIf")
|
|
return if (Validation.isAmountTooSmall(price)) {
|
|
Single.just(CreatePaymentIntentResult.AmountIsTooSmall(price))
|
|
} else if (Validation.isAmountTooLarge(price)) {
|
|
Single.just(CreatePaymentIntentResult.AmountIsTooLarge(price))
|
|
} else if (!Validation.supportedCurrencyCodes.contains(price.currency.currencyCode.uppercase(Locale.ROOT))) {
|
|
Single.just<CreatePaymentIntentResult>(CreatePaymentIntentResult.CurrencyIsNotSupported(price.currency.currencyCode))
|
|
} else {
|
|
paymentIntentFetcher
|
|
.fetchPaymentIntent(price, level)
|
|
.map<CreatePaymentIntentResult> { CreatePaymentIntentResult.Success(it) }
|
|
}.subscribeOn(Schedulers.io())
|
|
}
|
|
|
|
/**
|
|
* Confirm a PaymentIntent
|
|
*
|
|
* This method will create a PaymentMethod with the given PaymentSource and then confirm the
|
|
* PaymentIntent.
|
|
*
|
|
* @return A Secure3DSAction
|
|
*/
|
|
fun confirmPaymentIntent(paymentSource: PaymentSource, paymentIntent: StripeIntentAccessor): Single<Secure3DSAction> {
|
|
return Single.fromCallable {
|
|
val paymentMethodId = createPaymentMethodAndParseId(paymentSource)
|
|
|
|
val parameters = mutableMapOf(
|
|
"client_secret" to paymentIntent.intentClientSecret,
|
|
"payment_method" to paymentMethodId,
|
|
"return_url" to RETURN_URL_3DS
|
|
)
|
|
|
|
val (nextActionUri, returnUri) = postForm("payment_intents/${paymentIntent.intentId}/confirm", parameters).use { response ->
|
|
getNextAction(response)
|
|
}
|
|
|
|
Secure3DSAction.from(nextActionUri, returnUri)
|
|
}.subscribeOn(Schedulers.io())
|
|
}
|
|
|
|
/**
|
|
* Retrieve the setup intent pointed to by the given accessor.
|
|
*/
|
|
fun getSetupIntent(stripeIntentAccessor: StripeIntentAccessor): StripeSetupIntent {
|
|
return when (stripeIntentAccessor.objectType) {
|
|
StripeIntentAccessor.ObjectType.SETUP_INTENT -> get("setup_intents/${stripeIntentAccessor.intentId}?client_secret=${stripeIntentAccessor.intentClientSecret}").use {
|
|
val body = it.body()?.string()
|
|
try {
|
|
objectMapper.readValue(body!!)
|
|
} catch (e: InvalidDefinitionException) {
|
|
Log.w(TAG, "Failed to parse JSON for StripeSetupIntent.")
|
|
ResponseFieldLogger.logFields(objectMapper, body)
|
|
throw StripeError.FailedToParseSetupIntentResponseError(e)
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, "Failed to read value from JSON.", e, true)
|
|
throw StripeError.FailedToParseSetupIntentResponseError(null)
|
|
}
|
|
}
|
|
else -> error("Unsupported type")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the payment intent pointed to by the given accessor.
|
|
*/
|
|
fun getPaymentIntent(stripeIntentAccessor: StripeIntentAccessor): StripePaymentIntent {
|
|
return when (stripeIntentAccessor.objectType) {
|
|
StripeIntentAccessor.ObjectType.PAYMENT_INTENT -> get("payment_intents/${stripeIntentAccessor.intentId}?client_secret=${stripeIntentAccessor.intentClientSecret}").use {
|
|
val body = it.body()?.string()
|
|
try {
|
|
objectMapper.readValue(body!!)
|
|
} catch (e: InvalidDefinitionException) {
|
|
Log.w(TAG, "Failed to parse JSON for StripePaymentIntent.")
|
|
ResponseFieldLogger.logFields(objectMapper, body)
|
|
throw StripeError.FailedToParsePaymentIntentResponseError(e)
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, "Failed to read value from JSON.", e, true)
|
|
throw StripeError.FailedToParsePaymentIntentResponseError(null)
|
|
}
|
|
}
|
|
else -> error("Unsupported type")
|
|
}
|
|
}
|
|
|
|
private fun getNextAction(response: Response): Pair<Uri, Uri> {
|
|
val responseBody = response.body()?.string()
|
|
val bodyJson = responseBody?.let { JSONObject(it) }
|
|
return if (bodyJson?.has("next_action") == true && !bodyJson.isNull("next_action")) {
|
|
val nextAction = bodyJson.getJSONObject("next_action")
|
|
if (BuildConfig.DEBUG) {
|
|
Log.d(TAG, "[getNextAction] Next Action found:\n$nextAction")
|
|
}
|
|
|
|
val redirectToUrl = nextAction.getJSONObject("redirect_to_url")
|
|
val nextActionUri = redirectToUrl.getString("url")
|
|
val returnUri = redirectToUrl.getString("return_url")
|
|
|
|
Uri.parse(nextActionUri) to Uri.parse(returnUri)
|
|
} else {
|
|
Uri.EMPTY to Uri.EMPTY
|
|
}
|
|
}
|
|
|
|
fun createPaymentSourceFromCardData(cardData: CardData): Single<CreatePaymentSourceFromCardDataResult> {
|
|
return Single.fromCallable<CreatePaymentSourceFromCardDataResult> {
|
|
CreatePaymentSourceFromCardDataResult.Success(createPaymentSourceFromCardDataSync(cardData))
|
|
}.onErrorReturn {
|
|
CreatePaymentSourceFromCardDataResult.Failure(it)
|
|
}.subscribeOn(Schedulers.io())
|
|
}
|
|
|
|
@WorkerThread
|
|
private fun createPaymentSourceFromCardDataSync(cardData: CardData): PaymentSource {
|
|
val parameters: Map<String, String> = mutableMapOf(
|
|
CARD_NUMBER_KEY to cardData.number,
|
|
CARD_MONTH_KEY to cardData.month.toString(),
|
|
CARD_YEAR_KEY to cardData.year.toString(),
|
|
CARD_CVC_KEY to cardData.cvc.toString()
|
|
)
|
|
|
|
postForm("tokens", parameters).use { response ->
|
|
val body = response.body()
|
|
if (body != null) {
|
|
return CreditCardPaymentSource(JSONObject(body.string()))
|
|
} else {
|
|
throw StripeError.FailedToCreatePaymentSourceFromCardData
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun createPaymentMethodAndParseId(paymentSource: PaymentSource): String {
|
|
return createPaymentMethod(paymentSource).use { response ->
|
|
val body = response.body()
|
|
if (body != null) {
|
|
val paymentMethodObject = body.string().replace("\n", "").let { JSONObject(it) }
|
|
paymentMethodObject.getString("id")
|
|
} else {
|
|
throw StripeError.FailedToParsePaymentMethodResponseError
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun createPaymentMethod(paymentSource: PaymentSource): Response {
|
|
val tokenId = paymentSource.getTokenId()
|
|
val parameters = mutableMapOf(
|
|
"card[token]" to tokenId,
|
|
"type" to "card",
|
|
)
|
|
|
|
return postForm("payment_methods", parameters)
|
|
}
|
|
|
|
private fun get(endpoint: String): Response {
|
|
val request = getRequestBuilder(endpoint).get().build()
|
|
val response = okHttpClient.newCall(request).execute()
|
|
return checkResponseForErrors(response)
|
|
}
|
|
|
|
private fun postForm(endpoint: String, parameters: Map<String, String>): Response {
|
|
val formBodyBuilder = FormBody.Builder()
|
|
parameters.forEach { (k, v) ->
|
|
formBodyBuilder.add(k, v)
|
|
}
|
|
|
|
val request = getRequestBuilder(endpoint)
|
|
.post(formBodyBuilder.build())
|
|
.build()
|
|
|
|
val response = okHttpClient.newCall(request).execute()
|
|
|
|
return checkResponseForErrors(response)
|
|
}
|
|
|
|
private fun getRequestBuilder(endpoint: String): Request.Builder {
|
|
return Request.Builder()
|
|
.url("${configuration.baseUrl}/$endpoint")
|
|
.addHeader("Authorization", "Basic ${ByteString.encodeUtf8("${configuration.publishableKey}:").base64()}")
|
|
}
|
|
|
|
private fun checkResponseForErrors(response: Response): Response {
|
|
if (response.isSuccessful) {
|
|
return response
|
|
} else {
|
|
val body = response.body()?.string()
|
|
val errorCode = parseErrorCode(body)
|
|
val declineCode = parseDeclineCode(body) ?: StripeDeclineCode.getFromCode(errorCode)
|
|
|
|
throw StripeError.PostError(
|
|
response.code(),
|
|
errorCode,
|
|
declineCode
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun parseErrorCode(body: String?): String? {
|
|
if (body == null) {
|
|
Log.d(TAG, "parseErrorCode: No body.", true)
|
|
return null
|
|
}
|
|
|
|
return try {
|
|
JSONObject(body).getJSONObject("error").getString("code")
|
|
} catch (e: Exception) {
|
|
Log.d(TAG, "parseErrorCode: Failed to parse error.", e, true)
|
|
null
|
|
}
|
|
}
|
|
|
|
private fun parseDeclineCode(body: String?): StripeDeclineCode? {
|
|
if (body == null) {
|
|
Log.d(TAG, "parseDeclineCode: No body.", true)
|
|
return null
|
|
}
|
|
|
|
return try {
|
|
StripeDeclineCode.getFromCode(JSONObject(body).getJSONObject("error").getString("decline_code"))
|
|
} catch (e: Exception) {
|
|
Log.d(TAG, "parseDeclineCode: Failed to parse decline code.", e, true)
|
|
null
|
|
}
|
|
}
|
|
|
|
object Validation {
|
|
private val MAX_AMOUNT = BigDecimal(99_999_999)
|
|
|
|
fun isAmountTooLarge(fiatMoney: FiatMoney): Boolean {
|
|
return fiatMoney.minimumUnitPrecisionString.toBigDecimal() > MAX_AMOUNT
|
|
}
|
|
|
|
fun isAmountTooSmall(fiatMoney: FiatMoney): Boolean {
|
|
return fiatMoney.minimumUnitPrecisionString.toBigDecimal() < BigDecimal(minimumIntegralChargePerCurrencyCode[fiatMoney.currency.currencyCode] ?: 50)
|
|
}
|
|
|
|
private val minimumIntegralChargePerCurrencyCode: Map<String, Int> = mapOf(
|
|
"USD" to 50,
|
|
"AED" to 200,
|
|
"AUD" to 50,
|
|
"BGN" to 100,
|
|
"BRL" to 50,
|
|
"CAD" to 50,
|
|
"CHF" to 50,
|
|
"CZK" to 1500,
|
|
"DKK" to 250,
|
|
"EUR" to 50,
|
|
"GBP" to 30,
|
|
"HKD" to 400,
|
|
"HUF" to 17500,
|
|
"INR" to 50,
|
|
"JPY" to 50,
|
|
"MXN" to 10,
|
|
"MYR" to 2,
|
|
"NOK" to 300,
|
|
"NZD" to 50,
|
|
"PLN" to 200,
|
|
"RON" to 200,
|
|
"SEK" to 300,
|
|
"SGD" to 50
|
|
)
|
|
|
|
val supportedCurrencyCodes: List<String> = listOf(
|
|
"USD",
|
|
"AED",
|
|
"AFN",
|
|
"ALL",
|
|
"AMD",
|
|
"ANG",
|
|
"AOA",
|
|
"ARS",
|
|
"AUD",
|
|
"AWG",
|
|
"AZN",
|
|
"BAM",
|
|
"BBD",
|
|
"BDT",
|
|
"BGN",
|
|
"BIF",
|
|
"BMD",
|
|
"BND",
|
|
"BOB",
|
|
"BRL",
|
|
"BSD",
|
|
"BWP",
|
|
"BZD",
|
|
"CAD",
|
|
"CDF",
|
|
"CHF",
|
|
"CLP",
|
|
"CNY",
|
|
"COP",
|
|
"CRC",
|
|
"CVE",
|
|
"CZK",
|
|
"DJF",
|
|
"DKK",
|
|
"DOP",
|
|
"DZD",
|
|
"EGP",
|
|
"ETB",
|
|
"EUR",
|
|
"FJD",
|
|
"FKP",
|
|
"GBP",
|
|
"GEL",
|
|
"GIP",
|
|
"GMD",
|
|
"GNF",
|
|
"GTQ",
|
|
"GYD",
|
|
"HKD",
|
|
"HNL",
|
|
"HRK",
|
|
"HTG",
|
|
"HUF",
|
|
"IDR",
|
|
"ILS",
|
|
"INR",
|
|
"ISK",
|
|
"JMD",
|
|
"JPY",
|
|
"KES",
|
|
"KGS",
|
|
"KHR",
|
|
"KMF",
|
|
"KRW",
|
|
"KYD",
|
|
"KZT",
|
|
"LAK",
|
|
"LBP",
|
|
"LKR",
|
|
"LRD",
|
|
"LSL",
|
|
"MAD",
|
|
"MDL",
|
|
"MGA",
|
|
"MKD",
|
|
"MMK",
|
|
"MNT",
|
|
"MOP",
|
|
"MRO",
|
|
"MUR",
|
|
"MVR",
|
|
"MWK",
|
|
"MXN",
|
|
"MYR",
|
|
"MZN",
|
|
"NAD",
|
|
"NGN",
|
|
"NIO",
|
|
"NOK",
|
|
"NPR",
|
|
"NZD",
|
|
"PAB",
|
|
"PEN",
|
|
"PGK",
|
|
"PHP",
|
|
"PKR",
|
|
"PLN",
|
|
"PYG",
|
|
"QAR",
|
|
"RON",
|
|
"RSD",
|
|
"RUB",
|
|
"RWF",
|
|
"SAR",
|
|
"SBD",
|
|
"SCR",
|
|
"SEK",
|
|
"SGD",
|
|
"SHP",
|
|
"SLL",
|
|
"SOS",
|
|
"SRD",
|
|
"STD",
|
|
"SZL",
|
|
"THB",
|
|
"TJS",
|
|
"TOP",
|
|
"TRY",
|
|
"TTD",
|
|
"TWD",
|
|
"TZS",
|
|
"UAH",
|
|
"UGX",
|
|
"UYU",
|
|
"UZS",
|
|
"VND",
|
|
"VUV",
|
|
"WST",
|
|
"XAF",
|
|
"XCD",
|
|
"XOF",
|
|
"XPF",
|
|
"YER",
|
|
"ZAR",
|
|
"ZMW"
|
|
)
|
|
}
|
|
|
|
class Gateway(private val configuration: Configuration) : GooglePayApi.Gateway {
|
|
override fun getTokenizationSpecificationParameters(): Map<String, String> {
|
|
return mapOf(
|
|
"gateway" to "stripe",
|
|
"stripe:version" to configuration.version,
|
|
"stripe:publishableKey" to configuration.publishableKey
|
|
)
|
|
}
|
|
|
|
override val allowedCardNetworks: List<String> = listOf(
|
|
"AMEX",
|
|
"DISCOVER",
|
|
"JCB",
|
|
"MASTERCARD",
|
|
"VISA"
|
|
)
|
|
}
|
|
|
|
data class Configuration(
|
|
val publishableKey: String,
|
|
val baseUrl: String = "https://api.stripe.com/v1",
|
|
val version: String = "2018-10-31"
|
|
)
|
|
|
|
interface PaymentIntentFetcher {
|
|
fun fetchPaymentIntent(
|
|
price: FiatMoney,
|
|
level: Long
|
|
): Single<StripeIntentAccessor>
|
|
}
|
|
|
|
interface SetupIntentHelper {
|
|
fun fetchSetupIntent(): Single<StripeIntentAccessor>
|
|
}
|
|
|
|
@Parcelize
|
|
data class CardData(
|
|
val number: String,
|
|
val month: Int,
|
|
val year: Int,
|
|
val cvc: Int
|
|
) : Parcelable
|
|
|
|
interface PaymentSource {
|
|
val type: PaymentSourceType
|
|
fun parameterize(): JSONObject
|
|
fun getTokenId(): String
|
|
fun email(): String?
|
|
}
|
|
|
|
sealed interface Secure3DSAction {
|
|
data class ConfirmRequired(val uri: Uri, val returnUri: Uri, override val paymentMethodId: String?) : Secure3DSAction
|
|
data class NotNeeded(override val paymentMethodId: String?) : Secure3DSAction
|
|
|
|
val paymentMethodId: String?
|
|
|
|
companion object {
|
|
fun from(
|
|
uri: Uri,
|
|
returnUri: Uri,
|
|
paymentMethodId: String? = null
|
|
): Secure3DSAction {
|
|
return if (uri == Uri.EMPTY) {
|
|
NotNeeded(paymentMethodId)
|
|
} else {
|
|
ConfirmRequired(uri, returnUri, paymentMethodId)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} |