-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Card: Vault without Purchase 3DS (#243)
* Move OptionList into shared components. * Create BooleanOptionList. * Add 3DS when required step to vault card view. * Add SCA_ALWAYS option to project. * Add SCA auth to CardClient.vault(). * Add parsing method for card client vaulting. * Add notes for changes related to semantic versioning. * Fix unit test. * Rename CardAuthChallengeLauncher to CardAuthLauncher. * Migrate remaining browser switch related code from CardClient into CardAuthChallenge. * Migrate browser switch related tests out of CardClient. * Remove parcelable conformance from CardVaultResult. * Update CardAuthLauncher unit test. * Restore unit tests for browser switch result parsing in card auth launcher. * Add success test for CardAuthLauncher. * Update CardAuthLauncher unit test to support vault deliver result cases. * Implement CardClient unit tests for notify of order cancelation. * Add tests for verifying approve order notifications. * Add additional card client tests for vault notifications using listeners. * Add vault notification tests for paypal web checkout client. * Reduce function count in CardClient to comply with linter. * Remove unused imports. * Add supporting methods for fetching a setup token. * Add GetSetupTokenUseCase to project. * Implement auth challenge present with into about updated setup token. * Remove unused imports in InfoColumn. * Remove additional unused imports. * Add unit tests for present auth challenge. * Add Unit test stub. * Add unit test for approval url. * Update CHANGELOG and add documentation. * Fix lint error. * Use sca enum. * Remove unecessary comments. * Update order id capturing for status objects.
- Loading branch information
1 parent
a4bd718
commit 3af42b4
Showing
40 changed files
with
1,233 additions
and
342 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
CardPayments/src/main/java/com/paypal/android/cardpayments/CardAuthChallenge.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.paypal.android.cardpayments | ||
|
||
import android.net.Uri | ||
import android.os.Parcelable | ||
import kotlinx.parcelize.Parcelize | ||
|
||
/** | ||
* Pass this object to [CardClient.presentAuthChallenge] to present an authentication challenge | ||
* that was received in response to a [CardClient.approveOrder] or [CardClient.vault] call. | ||
*/ | ||
sealed class CardAuthChallenge { | ||
// Ref: https://stackoverflow.com/a/44420084 | ||
internal abstract val url: Uri | ||
internal abstract val returnUrlScheme: String? | ||
|
||
@Parcelize | ||
internal class ApproveOrder( | ||
override val url: Uri, | ||
val request: CardRequest, | ||
override val returnUrlScheme: String? = Uri.parse(request.returnUrl).scheme | ||
) : CardAuthChallenge(), Parcelable | ||
|
||
@Parcelize | ||
internal class Vault( | ||
override val url: Uri, | ||
val request: CardVaultRequest, | ||
override val returnUrlScheme: String? = Uri.parse(request.returnUrl).scheme | ||
) : CardAuthChallenge(), Parcelable | ||
} |
153 changes: 153 additions & 0 deletions
153
CardPayments/src/main/java/com/paypal/android/cardpayments/CardAuthLauncher.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
package com.paypal.android.cardpayments | ||
|
||
import androidx.fragment.app.FragmentActivity | ||
import com.braintreepayments.api.BrowserSwitchClient | ||
import com.braintreepayments.api.BrowserSwitchException | ||
import com.braintreepayments.api.BrowserSwitchOptions | ||
import com.braintreepayments.api.BrowserSwitchResult | ||
import com.braintreepayments.api.BrowserSwitchStatus | ||
import com.paypal.android.corepayments.PayPalSDKError | ||
import org.json.JSONObject | ||
|
||
internal class CardAuthLauncher( | ||
private val browserSwitchClient: BrowserSwitchClient = BrowserSwitchClient(), | ||
) { | ||
|
||
companion object { | ||
private const val METADATA_KEY_REQUEST_TYPE = "request_type" | ||
private const val REQUEST_TYPE_APPROVE_ORDER = "approve_order" | ||
private const val REQUEST_TYPE_VAULT = "vault" | ||
|
||
private const val METADATA_KEY_ORDER_ID = "order_id" | ||
private const val METADATA_KEY_SETUP_TOKEN_ID = "setup_token_id" | ||
} | ||
|
||
fun presentAuthChallenge( | ||
activity: FragmentActivity, | ||
authChallenge: CardAuthChallenge | ||
): PayPalSDKError? { | ||
val metadata = when (authChallenge) { | ||
is CardAuthChallenge.ApproveOrder -> { | ||
val request = authChallenge.request | ||
JSONObject() | ||
.put(METADATA_KEY_REQUEST_TYPE, REQUEST_TYPE_APPROVE_ORDER) | ||
.put(METADATA_KEY_ORDER_ID, request.orderId) | ||
} | ||
|
||
is CardAuthChallenge.Vault -> { | ||
val request = authChallenge.request | ||
JSONObject() | ||
.put(METADATA_KEY_REQUEST_TYPE, REQUEST_TYPE_VAULT) | ||
.put(METADATA_KEY_SETUP_TOKEN_ID, request.setupTokenId) | ||
} | ||
} | ||
|
||
// launch the 3DS flow | ||
val browserSwitchOptions = BrowserSwitchOptions() | ||
.url(authChallenge.url) | ||
.returnUrlScheme(authChallenge.returnUrlScheme) | ||
.metadata(metadata) | ||
return launchBrowserSwitch(activity, browserSwitchOptions) | ||
} | ||
|
||
private fun launchBrowserSwitch( | ||
activity: FragmentActivity, | ||
options: BrowserSwitchOptions | ||
): PayPalSDKError? { | ||
var error: PayPalSDKError? = null | ||
try { | ||
browserSwitchClient.start(activity, options) | ||
} catch (e: BrowserSwitchException) { | ||
error = CardError.browserSwitchError(e) | ||
} | ||
return error | ||
} | ||
|
||
fun deliverBrowserSwitchResult(activity: FragmentActivity) = | ||
browserSwitchClient.deliverResult(activity)?.let { browserSwitchResult -> | ||
val requestType = | ||
browserSwitchResult.requestMetadata?.optString(METADATA_KEY_REQUEST_TYPE) | ||
if (requestType == REQUEST_TYPE_VAULT) { | ||
parseVaultResult(browserSwitchResult) | ||
} else { | ||
// Assume REQUEST_TYPE_APPROVE_ORDER | ||
parseApproveOrderResult(browserSwitchResult) | ||
} | ||
} | ||
|
||
private fun parseVaultResult(browserSwitchResult: BrowserSwitchResult): CardStatus? { | ||
val setupTokenId = | ||
browserSwitchResult.requestMetadata?.optString(METADATA_KEY_SETUP_TOKEN_ID) | ||
return when (browserSwitchResult.status) { | ||
BrowserSwitchStatus.SUCCESS -> parseVaultSuccessResult(browserSwitchResult) | ||
BrowserSwitchStatus.CANCELED -> CardStatus.VaultCanceled(setupTokenId) | ||
else -> null | ||
} | ||
} | ||
|
||
private fun parseApproveOrderResult(browserSwitchResult: BrowserSwitchResult): CardStatus? { | ||
val orderId = browserSwitchResult.requestMetadata?.optString(METADATA_KEY_ORDER_ID) | ||
return if (orderId == null) { | ||
CardStatus.ApproveOrderError(CardError.unknownError, orderId) | ||
} else { | ||
when (browserSwitchResult.status) { | ||
BrowserSwitchStatus.SUCCESS -> | ||
parseApproveOrderSuccessResult(browserSwitchResult, orderId) | ||
|
||
BrowserSwitchStatus.CANCELED -> CardStatus.ApproveOrderCanceled(orderId) | ||
else -> null | ||
} | ||
} | ||
} | ||
|
||
private fun parseVaultSuccessResult(browserSwitchResult: BrowserSwitchResult): CardStatus { | ||
val deepLinkUrl = browserSwitchResult.deepLinkUrl | ||
val requestMetadata = browserSwitchResult.requestMetadata | ||
|
||
return if (deepLinkUrl == null || requestMetadata == null) { | ||
CardStatus.VaultError(CardError.unknownError) | ||
} else { | ||
// TODO: see if there's a way that we can require the merchant to make their | ||
// return and cancel urls conform to a strict schema | ||
|
||
// NOTE: this assumes that when the merchant created a setup token, they used a | ||
// return_url with word "success" in it (or a cancel_url with the word "cancel" in it) | ||
val setupTokenId = | ||
browserSwitchResult.requestMetadata?.optString(METADATA_KEY_SETUP_TOKEN_ID) | ||
val deepLinkUrlString = deepLinkUrl.toString() | ||
val didSucceed = deepLinkUrlString.contains("success") | ||
if (didSucceed) { | ||
val result = CardVaultResult(setupTokenId!!, "SCA_COMPLETE") | ||
CardStatus.VaultSuccess(result) | ||
} else { | ||
val didCancel = deepLinkUrlString.contains("cancel") | ||
if (didCancel) { | ||
CardStatus.VaultCanceled(setupTokenId) | ||
} else { | ||
CardStatus.VaultError(CardError.unknownError) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun parseApproveOrderSuccessResult( | ||
browserSwitchResult: BrowserSwitchResult, | ||
orderId: String | ||
): CardStatus { | ||
val deepLinkUrl = browserSwitchResult.deepLinkUrl | ||
|
||
return if (deepLinkUrl == null || deepLinkUrl.getQueryParameter("error") != null) { | ||
CardStatus.ApproveOrderError(CardError.threeDSVerificationError, orderId) | ||
} else { | ||
val state = deepLinkUrl.getQueryParameter("state") | ||
val code = deepLinkUrl.getQueryParameter("code") | ||
if (state == null || code == null) { | ||
CardStatus.ApproveOrderError(CardError.malformedDeepLinkError, orderId) | ||
} else { | ||
val liabilityShift = deepLinkUrl.getQueryParameter("liability_shift") | ||
val result = CardResult(orderId, deepLinkUrl, liabilityShift) | ||
CardStatus.ApproveOrderSuccess(result) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.