Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Alipay PaymentMethod public #2692

Merged
merged 6 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# CHANGELOG

## 15.0.3 - unreleased
* [#2692](https://github.com/stripe/stripe-android/pull/2692) Make Alipay PaymentMethod public

## 15.0.2 - 2020-08-03
* [#2666](https://github.com/stripe/stripe-android/pull/2666) Bump 3DS2 SDK to `4.0.4`
* [#2671](https://github.com/stripe/stripe-android/pull/2671) Add `cardParams` property to `CardInputWidget` and `CardMultilineWidget`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import com.stripe.android.model.PaymentIntent
* }
* </pre>
*/
internal interface AlipayAuthenticator {
interface AlipayAuthenticator {
@WorkerThread
fun onAuthenticationRequest(data: String): Map<String, String>
}
2 changes: 1 addition & 1 deletion stripe/src/main/java/com/stripe/android/Stripe.kt
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class Stripe internal constructor(
* @param callback a [ApiResultCallback] to receive the result or error
*/
@JvmOverloads
internal fun confirmAlipayPayment(
fun confirmAlipayPayment(
confirmPaymentIntentParams: ConfirmPaymentIntentParams,
authenticator: AlipayAuthenticator,
stripeAccountId: String? = this.stripeAccountId,
Expand Down
34 changes: 30 additions & 4 deletions stripe/src/main/java/com/stripe/android/StripePaymentController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import com.stripe.android.model.Stripe3ds2AuthResult
import com.stripe.android.model.Stripe3ds2Fingerprint
import com.stripe.android.model.StripeIntent
import com.stripe.android.model.StripeIntent.NextActionData.RedirectToUrl
import com.stripe.android.model.StripeIntent.NextActionData.RedirectToUrl.MobileData.Alipay as AlipayData
import com.stripe.android.stripe3ds2.init.ui.StripeUiCustomization
import com.stripe.android.stripe3ds2.service.StripeThreeDs2Service
import com.stripe.android.stripe3ds2.service.StripeThreeDs2ServiceImpl
Expand Down Expand Up @@ -353,13 +352,13 @@ internal class StripePaymentController internal constructor(
) : ApiOperation<AlipayAuthResult>(callback = callback) {
override suspend fun getResult(): AlipayAuthResult {
val nextActionData = intent.nextActionData
if (nextActionData is RedirectToUrl && nextActionData.mobileData is AlipayData) {
if (nextActionData is StripeIntent.NextActionData.AlipayRedirect) {
val output =
authenticator.onAuthenticationRequest(nextActionData.mobileData.data)
authenticator.onAuthenticationRequest(nextActionData.data)
return AlipayAuthResult(
when (output[RESULT_FIELD]) {
RESULT_CODE_SUCCESS -> {
nextActionData.mobileData.authCompleteUrl?.let {
nextActionData.authCompleteUrl?.let {
runCatching {
apiRepository.retrieveObject(it, requestOptions)
}
Expand Down Expand Up @@ -546,6 +545,33 @@ internal class StripePaymentController internal constructor(
enableLogging = enableLogging
)
}
/**
* If using the standard confirmation path, handle Alipay the same as
* a standard webview redirect.
* Alipay Native SDK use case is handled by [Stripe.confirmAlipayPayment]
* outside of the standard confirmation path.
*/
is StripeIntent.NextActionData.AlipayRedirect -> {
analyticsRequestExecutor.executeAsync(
analyticsRequestFactory.create(
analyticsDataFactory.createAuthParams(
AnalyticsEvent.AuthRedirect,
stripeIntent.id.orEmpty()
),
requestOptions
)
)

beginWebAuth(
host,
getRequestCode(stripeIntent),
stripeIntent.clientSecret.orEmpty(),
nextActionData.webViewUrl.toString(),
requestOptions.stripeAccount,
nextActionData.returnUrl,
enableLogging = enableLogging
)
}
is StripeIntent.NextActionData.DisplayOxxoDetails -> {
// TODO(smaskell): add analytics event
if (nextActionData.hostedVoucherUrl != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ data class ConfirmPaymentIntentParams internal constructor(
* process
*/
@JvmStatic
internal fun createAlipay(
fun createAlipay(
clientSecret: String
): ConfirmPaymentIntentParams {
return ConfirmPaymentIntentParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ data class PaymentMethodCreateParams internal constructor(

@JvmSynthetic
@JvmOverloads
internal fun createAlipay(
fun createAlipay(
metadata: Map<String, String>? = null
): PaymentMethodCreateParams {
return PaymentMethodCreateParams(
Expand Down
53 changes: 27 additions & 26 deletions stripe/src/main/java/com/stripe/android/model/StripeIntent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ interface StripeIntent : StripeModel {
enum class NextActionType(val code: String) {
RedirectToUrl("redirect_to_url"),
UseStripeSdk("use_stripe_sdk"),
DisplayOxxoDetails("display_oxxo_details");
DisplayOxxoDetails("display_oxxo_details"),
AlipayRedirect("alipay_handle_redirect");

override fun toString(): String {
return code
Expand Down Expand Up @@ -162,33 +163,33 @@ interface StripeIntent : StripeModel {
* If the customer does not exit their browser while authenticating, they will be redirected
* to this specified URL after completion.
*/
val returnUrl: String?,
val mobileData: MobileData?
val returnUrl: String?
) : NextActionData()

@Parcelize
data class AlipayRedirect constructor(
val data: String,
val authCompleteUrl: String?,
val webViewUrl: Uri,
val returnUrl: String? = null
) : NextActionData() {

sealed class MobileData : StripeModel {
@Parcelize
data class Alipay constructor(
val data: String,
val authCompleteUrl: String?
) : MobileData() {
constructor(data: String) : this(data, extractReturnUrl(data))
}

private companion object {
/**
* The alipay data string is formatted as query parameters.
* When authenticate is complete, we make a request to the
* return_url param, as a hint to the backend to ping Alipay for
* the updated state
*/
private fun extractReturnUrl(data: String): String? = runCatching {
Uri.parse("alipay://url?$data")
.getQueryParameter("return_url")?.takeIf {
StripeUrlUtils.isStripeUrl(it)
}
}.getOrNull()
}
internal constructor(data: String, webViewUrl: String, returnUrl: String? = null) :
this(data, extractReturnUrl(data), Uri.parse(webViewUrl), returnUrl)

private companion object {
/**
* The alipay data string is formatted as query parameters.
* When authenticate is complete, we make a request to the
* return_url param, as a hint to the backend to ping Alipay for
* the updated state
*/
private fun extractReturnUrl(data: String): String? = runCatching {
Uri.parse("alipay://url?$data")
.getQueryParameter("return_url")?.takeIf {
StripeUrlUtils.isStripeUrl(it)
}
}.getOrNull()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal class NextActionDataParser : ModelJsonParser<StripeIntent.NextActionDat
StripeIntent.NextActionType.DisplayOxxoDetails -> DisplayOxxoDetailsJsonParser()
StripeIntent.NextActionType.RedirectToUrl -> RedirectToUrlParser()
StripeIntent.NextActionType.UseStripeSdk -> SdkDataJsonParser()
StripeIntent.NextActionType.AlipayRedirect -> AlipayRedirectParser()
else -> return null
}
return parser.parse(json.optJSONObject(nextActionType.code) ?: JSONObject())
Expand Down Expand Up @@ -40,49 +41,31 @@ internal class NextActionDataParser : ModelJsonParser<StripeIntent.NextActionDat
json.has(FIELD_URL) ->
StripeIntent.NextActionData.RedirectToUrl(
Uri.parse(json.getString(FIELD_URL)),
json.optString(FIELD_RETURN_URL),
MobileDataParser().parse(json.optJSONObject(FIELD_MOBILE) ?: JSONObject())
json.optString(FIELD_RETURN_URL)
)
else -> null
}
}

internal class MobileDataParser : ModelJsonParser<StripeIntent.NextActionData.RedirectToUrl.MobileData> {
override fun parse(json: JSONObject): StripeIntent.NextActionData.RedirectToUrl.MobileData? {
val type = Type.fromCode(optString(json, FIELD_TYPE))
val obj = type?.let { json.optJSONObject(it.code) }
return when (type) {
Type.Alipay -> obj?.let {
StripeIntent.NextActionData.RedirectToUrl.MobileData.Alipay(
it.optString(FIELD_DATA)
)
}
else -> null
}
}

private companion object {
internal const val FIELD_TYPE = "type"
internal const val FIELD_DATA = "data"

internal enum class Type(
internal val code: String
) {
Alipay("alipay");
private companion object {
internal const val FIELD_URL = "url"
internal const val FIELD_RETURN_URL = "return_url"
}
}

internal companion object {
internal fun fromCode(code: String?): Type? {
return values().firstOrNull { it.code == code }
}
}
}
}
internal class AlipayRedirectParser : ModelJsonParser<StripeIntent.NextActionData.AlipayRedirect> {
override fun parse(json: JSONObject): StripeIntent.NextActionData.AlipayRedirect? {
return StripeIntent.NextActionData.AlipayRedirect(
json.getString(FIELD_NATIVE_DATA),
json.getString(FIELD_URL),
optString(json, FIELD_RETURN_URL)
)
}

private companion object {
internal const val FIELD_URL = "url"
internal const val FIELD_NATIVE_DATA = "native_data"
internal const val FIELD_RETURN_URL = "return_url"
internal const val FIELD_MOBILE = "mobile"
internal const val FIELD_URL = "url"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class AlipayAuthenticationTaskTest {
assertThat(result.outcome)
.isEqualTo(StripeIntentResult.Outcome.SUCCEEDED)
verify(stripeRepository).retrieveObject(
"https://hooks.stripe.com/adapter/alipay/redirect/complete/src_1Gt188KlwPmebFhp4SWhZwn1/src_client_secret_RMaQKPfAmHOdUwcNhXEjolR4",
"https://hooks.stripe.com/adapter/alipay/redirect/complete/src_1HDEFWKlwPmebFhp6tcpln8T/src_client_secret_S6H9mVMKK6qxk9YxsUvbH55K",
requestOptions
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,10 @@ class PaymentMethodEndToEndTest {

@Test
fun createPaymentMethod_withAlipay_shouldCreateObject() {
val repository = StripeApiRepository(
context,
ApiKeyFixtures.ALIPAY_PUBLISHABLE_KEY,
apiVersion = "2020-03-02;alipay_beta=v1"
)

val paymentMethod = repository.createPaymentMethod(
PaymentMethodCreateParams.createAlipay(),
ApiRequest.Options(ApiKeyFixtures.ALIPAY_PUBLISHABLE_KEY)
)
val params = PaymentMethodCreateParams.createAlipay()
val paymentMethod =
Stripe(context, ApiKeyFixtures.ALIPAY_PUBLISHABLE_KEY)
.createPaymentMethodSynchronous(params)
assertThat(paymentMethod?.type)
.isEqualTo(PaymentMethod.Type.Alipay)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package com.stripe.android.model

import com.google.common.truth.Truth.assertThat
import com.stripe.android.model.StripeIntent.NextActionData.RedirectToUrl.MobileData
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class MobileDataTest {
class AlipayRedirectTest {
@Test
fun `Alipay data should parse return_url correctly`() {
val data = MobileData.Alipay("_input_charset=utf-8&app_pay=Y" +
val data = StripeIntent.NextActionData.AlipayRedirect("_input_charset=utf-8&app_pay=Y" +
"&currency=USD&forex_biz=FP" +
"&notify_url=https%3A%2F%2Fhooks.stripe.com%2Falipay%2Falipay%2Fhook%2F6255d30b067c8f7a162c79c654483646%2Fsrc_1Gt188KlwPmebFhp4SWhZwn1" +
"&out_trade_no=src_1Gt188KlwPmebFhp4SWhZwn1" +
Expand All @@ -27,21 +26,27 @@ class MobileDataTest {
"&subject=Yuki-Test" +
"&supplier=Yuki-Test" +
"&timeout_rule=20m" +
"&total_fee=1.00")
"&total_fee=1.00",
WEB_URL
)
assertThat(data.authCompleteUrl).isEqualTo(
"https://hooks.stripe.com/adapter/alipay/redirect/complete/src_1Gt188KlwPmebFhp4SWhZwn1/src_client_secret_RMaQKPfAmHOdUwcNhXEjolR4"
)
}

@Test
fun `Alipay data should handle missing data`() {
val data = MobileData.Alipay("")
val data = StripeIntent.NextActionData.AlipayRedirect("", WEB_URL)
assertThat(data.authCompleteUrl).isNull()
}

@Test
fun `Alipay data should ignore non-stripe urls`() {
val data = MobileData.Alipay("return_url=https://google.com")
val data = StripeIntent.NextActionData.AlipayRedirect("return_url=https://google.com", WEB_URL)
assertThat(data.authCompleteUrl).isNull()
}

private companion object {
internal const val WEB_URL = "https://unused-param.com"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -686,35 +686,30 @@ internal object PaymentIntentFixtures {
val ALIPAY_REQUIRES_ACTION_JSON = JSONObject(
"""
{
"id": "pi_1GiUlYHSL10J9wqv4ZXqstCu",
"id": "pi_1HDEFVKlwPmebFhpCobFP55H",
"object": "payment_intent",
"amount": 1099,
"amount": 100,
"canceled_at": null,
"cancellation_reason": null,
"capture_method": "automatic",
"client_secret": "pi_1GiUlYHSL10J9wqv4ZXqstCu_secret_m8KioBxOULjcOevkIuihrYxXI",
"client_secret": "pi_1HDEFVKlwPmebFhpCobFP55H_secret_XW8sADccCxtusewAwn5z9kAiw",
"confirmation_method": "automatic",
"created": 1589415456,
"created": 1596740133,
"currency": "usd",
"description": "Example PaymentIntent",
"last_payment_error": null,
"livemode": false,
"livemode": true,
"next_action": {
"redirect_to_url": {
"mobile": {
"alipay": {
"data": "_input_charset=utf-8&app_pay=Y&currency=USD&forex_biz=FP&notify_url=https%3A%2F%2Fhooks.stripe.com%2Falipay%2Falipay%2Fhook%2F6255d30b067c8f7a162c79c654483646%2Fsrc_1Gt188KlwPmebFhp4SWhZwn1&out_trade_no=src_1Gt188KlwPmebFhp4SWhZwn1&partner=2088621828244481&payment_type=1&product_code=NEW_WAP_OVERSEAS_SELLER&return_url=https%3A%2F%2Fhooks.stripe.com%2Fadapter%2Falipay%2Fredirect%2Fcomplete%2Fsrc_1Gt188KlwPmebFhp4SWhZwn1%2Fsrc_client_secret_RMaQKPfAmHOdUwcNhXEjolR4&secondary_merchant_id=acct_1EqOyCKlwPmebFhp&secondary_merchant_industry=5734&secondary_merchant_name=Yuki-Test&sendFormat=normal&service=create_forex_trade_wap&sign=44e797e6d5ba1c784d3cceb176c359db&sign_type=MD5&subject=Yuki-Test&supplier=Yuki-Test&timeout_rule=20m&total_fee=1.00"
},
"native_url": null,
"type": "alipay"
},
"alipay_handle_redirect": {
"native_data": "_input_charset=utf-8&app_pay=Y&currency=USD&forex_biz=FP&notify_url=https%3A%2F%2Fhooks.stripe.com%2Falipay%2Falipay%2Fhook%2F6255d30b067c8f7a162c79c654483646%2Fsrc_1HDEFWKlwPmebFhp6tcpln8T&out_trade_no=src_1HDEFWKlwPmebFhp6tcpln8T&partner=2088621828244481&payment_type=1&product_code=NEW_WAP_OVERSEAS_SELLER&return_url=https%3A%2F%2Fhooks.stripe.com%2Fadapter%2Falipay%2Fredirect%2Fcomplete%2Fsrc_1HDEFWKlwPmebFhp6tcpln8T%2Fsrc_client_secret_S6H9mVMKK6qxk9YxsUvbH55K&secondary_merchant_id=acct_1EqOyCKlwPmebFhp&secondary_merchant_industry=5734&secondary_merchant_name=Yuki-Test&sendFormat=normal&service=create_forex_trade_wap&sign=b691876a7f0bd889530f54a271d314d5&sign_type=MD5&subject=Yuki-Test&supplier=Yuki-Test&timeout_rule=20m&total_fee=1.00",
"native_url": null,
"return_url": "example://return_url",
"url": "https://hooks.stripe.com/redirect/authenticate/src_1GiUlyHSL10J9wqvLZKrtWo3?client_secret=src_client_secret_JjkxntbeO885UyGjnwqjVDwI"
"url": "https://hooks.stripe.com/redirect/authenticate/src_1HDEFWKlwPmebFhp6tcpln8T?client_secret=src_client_secret_S6H9mVMKK6qxk9YxsUvbH55K"
},
"type": "redirect_to_url"
"type": "alipay_handle_redirect"
},
"payment_method": {
"id": "pm_1GiUlyHSL10J9wqv0SUGxiGi",
"id": "pm_1HDEFVKlwPmebFhpKYYkSm8H",
"object": "payment_method",
"alipay": {},
"billing_details": {
Expand All @@ -730,14 +725,12 @@ internal object PaymentIntentFixtures {
"name": null,
"phone": null
},
"created": 1589415482,
"created": 1596740133,
"customer": null,
"livemode": false,
"metadata": {},
"livemode": true,
"type": "alipay"
},
"payment_method_types": [
"card",
"alipay"
],
"receipt_email": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ class SetupIntentTest {
assertEquals(
StripeIntent.NextActionData.RedirectToUrl(
Uri.parse("https://hooks.stripe.com/redirect/authenticate/src_1EqTStGMT9dGPIDGJGPkqE6B" + "?client_secret=src_client_secret_FL9m741mmxtHykDlRTC5aQ02"),
returnUrl = "stripe://setup_intent_return",
mobileData = null
returnUrl = "stripe://setup_intent_return"
),
setupIntent.nextActionData
)
Expand Down
Loading