Android PayPal支付的接入和SDK支付过程解析
根据产品需求,要接入PayPal支付,在研究了几天支付后,总结下内容给新手使用,本篇文章如果与官方文档有差异的话,一切以官方为准。转载请注明出处。
注: 本篇文章只针对新版本的SDK进行解析,解析内容只针对部分功能,其它功能请自行参考PayPal开发者文档和SDK1
目录
- 一、Paypal接入
- 1、创建应用
- 2、应用内集成
- 3、代码实现
- 4、测试集成结果
- 二、支付流程解析
一、Paypal接入
本篇涉及的代码基于Github PayPal的示例进行修改
1、创建应用
- 用注册完成的账号登录PayPal开发者平台
- 先注册沙盒测试应用,左侧列表,点击 “DASHBOARD” -> “My Apps & Credentials” 后,在右侧窗口点击 “Sandbox” 或者 “Live” -> “Create App” 按钮进入创建沙盒App页面(正式环境把 “Sandbox” 切换成 “Live”)。 注: 沙盒界面,会默认创建了一个"DefaultApp"的默认应用,你也可以使用该应用进行测试。
- 在弹出的 “Create New App” 界面填写资料,完成后点击 “Create App”:
- App Name 应用名称,按需填写
- App Type 应用类型(Merchant:商家, Platform:平台)默认选择第一个
- Sandbox Business Account 沙盒企业账号。沙盒账号默认就行,测试用的企业账号,正式环境需要填写对应的企业账号,账号可以在"Account"创建,默认创建沙盒应用会生成一个个人和企业的账号,可以点击编辑查看密码。
- 创建应用完成,点击创建完成的应用 ,根据需求填写 (SANDBOX) APP SETTINGS,
Return URL 点击蓝色文字 “Show”,填入返回的URL,可以使用 “Android link生成的链接” 或者 “App包名 + 😕/paypalpay”注意:新版的SDK已经不需要用户创建返回连接了- App feature options,根据需要勾选,这里选择全部勾选,点击"Log in with PayPal" 的蓝色文本 “Advanced options”,在下拉选项中,按需勾选功能选项,一般勾选:
Personal profile -> “Full name” 和 “Emali”
Account information ->“PayPal accountId (player ID)”
Additional PayPal permissions->“Enable customers who have not yet confirmed their email with PayPal to log in to your app.”
并且填写指向App"用户协议"和"隐私政策"的网址;最后点击"Save"按钮。
2、应用内集成
- 清单文件声明网络权限
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
- 在项目根目录下的build.gradle集成maven地址,注意,官方文档password后面没有加引号,要注意
allprojects {
repositories {
mavenCentral()
// This private repository is required to resolve the Cardinal SDK transitive dependency.
maven {
url "https://cardinalcommerceprod.jfrog.io/artifactory/android"
credentials {
// Be sure to add these non-sensitive credentials in order to retrieve dependencies from
// the private repository.
username 'paypal_sgerritz'
password 'AKCp8jQ8tAahqpT5JjZ4FRP2mW7GMoFZ674kGqHmupTesKeAY2G8NcmPKLuTxTGkKjDLRzDUQ'
}
}
}
}
- 在app目录下的build.gradle添加java8支持和集成PayPal SDK
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation('com.paypal.checkout:android-sdk:0.8.7')
}
- 在App的继承Application的子类下初始化SDK
class YourApplication : Application() {
override fun onCreate() {
super.onCreate()
val config = CheckoutConfig(
application = this,
//注册的CilientId
clientId = YOUR_CLIENT_ID,
//测试环境选择沙盒模式,发版正式环境选择LIVE
environment = Environment.SANDBOX,
//此次和填写的RETURN URL一致,备注:新版本的SDK不需要
returnUrl = String.format("%s://paypalpay", BuildConfig.APPLICATION_ID),
//支付的货币类型
currencyCode = CurrencyCode.USD,
//支付动作:立即支付
userAction = UserAction.PAY_NOW,
//输出日志
settingsConfig = SettingsConfig(
loggingEnabled = true
)
)
PayPalCheckout.setConfig(config)
}
}
- 根据需求在需要用到支付的地方集成PlayPalButton,并根据业务调整逻辑。如果不想要paypal的自带的button也可以,区别在于调用有所差异
...
<com.paypal.checkout.paymentbutton.PayPalButton
android:id="@+id/btn_main_pay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="100dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
...
3、代码实现
class MainActivity : AppCompatActivity() {
private lateinit var root: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
root = ActivityMainBinding.inflate(layoutInflater)
setContentView(root.root)
/***/
root.btnMainPay.setup(
/*本地创建订单*/
createOrder = CreateOrder { createOrderActions -> val order = Order(
intent = OrderIntent.CAPTURE,
appContext = AppContext(userAction = UserAction.PAY_NOW),
purchaseUnitList =
listOf(
PurchaseUnit(
amount = Amount(currencyCode = CurrencyCode.USD, value = "10.00")
)
)
)
//客户端集成
createOrderActions.create(order)
//服务器集成
//createOrderActions.set(order.id)
},
onApprove =
OnApprove { approval ->
//客户端集成需要重写这个回调,里面封装了对订单的捕获,不重写,付款不会被扣除
//服务器端集成不能重写,在这里调用后台接口提供的订单捕获接口去捕获
approval.orderActions.capture { captureOrderResult ->
Log.i("CaptureOrder", "CaptureOrderResult: $captureOrderResult")
}
},
onCancel = OnCancel {
Log.d("OnCancel", "Buyer canceled the PayPal experience.")
},
onError = OnError { errorInfo ->
Log.d("OnError", "Error: $errorInfo")
}
)
}
}
客户端集成
/**
* 客户端集成,使用paypal自带的PaymentButton
*/
private fun setupPaymentButton() {
paymentButton.setup(
createOrder = CreateOrder { createOrderActions ->
Log.v(tag, "CreateOrder")
createOrderActions.create(
Order.Builder()
.appContext(
AppContext(
userAction = UserAction.PAY_NOW
)
)
.intent(OrderIntent.CAPTURE)
.purchaseUnitList(
listOf(
PurchaseUnit.Builder()
.amount(
Amount.Builder()
.value("0.01")
.currencyCode(CurrencyCode.USD)
.build()
)
.build()
)
)
.build()
.also { Log.d(tag, "Order: $it") }
)
},
onApprove = OnApprove { approval ->
Log.v(tag, "OnApprove")
Log.d(tag, "Approval details: $approval")
approval.orderActions.capture { captureOrderResult ->
Log.v(tag, "Capture Order")
Log.d(tag, "Capture order result: $captureOrderResult")
}
},
onCancel = OnCancel {
Log.v(tag, "OnCancel")
Log.d(tag, "Buyer cancelled the checkout experience.")
},
onError = OnError { errorInfo ->
Log.v(tag, "OnError")
Log.d(tag, "Error details: $errorInfo")
}
)
}
/**
* 客户端集成,不适用paypament自带的button
*/
private fun setupWithCustom() {
PayPalCheckout.registerCallbacks(
onApprove = OnApprove { approval ->
Log.i(tag, "OnApprove: $approval")
when (selectedOrderIntent) {
OrderIntent.AUTHORIZE -> approval.orderActions.authorize { result ->
val message = when (result) {
is AuthorizeOrderResult.Success -> {
Log.i(tag, "Success: $result")
"💰 Order Authorization Succeeded 💰"
}
is AuthorizeOrderResult.Error -> {
Log.i(tag, "Error: $result")
"🔥 Order Authorization Failed 🔥"
}
}
showSnackbar(message)
}
OrderIntent.CAPTURE -> approval.orderActions.capture { result ->
val message = when (result) {
is CaptureOrderResult.Success -> {
Log.i(tag, "Success: $result")
"💰 Order Capture Succeeded 💰"
}
is CaptureOrderResult.Error -> {
Log.i(tag, "Error: $result")
"🔥 Order Capture Failed 🔥"
}
}
showSnackbar(message)
}
}
},
onCancel = OnCancel {
Log.d(tag, "OnCancel")
showSnackbar("😭 Buyer Cancelled Checkout 😭")
},
onError = OnError { errorInfo ->
Log.d(tag, "ErrorInfo: $errorInfo")
showSnackbar("🚨 An Error Occurred 🚨")
}
)
PayPalCheckout.startCheckout(
createOrder = CreateOrder { actions ->
actions.create(
Order.Builder()
.appContext(
AppContext(
userAction = UserAction.PAY_NOW
)
)
.intent(OrderIntent.CAPTURE)
.purchaseUnitList(
listOf(
PurchaseUnit.Builder()
.amount(
Amount.Builder()
.value("0.01")
.currencyCode(CurrencyCode.USD)
.build()
)
.build()
)
)
.build()
.also { Log.d(tag, "Order: $it") }
)
}
)
}
服务器端集成
/**
* [orderId]从后台获取到的订单id
*/
private setupWith(orderId: String) {
//使用paypament自带的按钮
paymentButton.setup(
createOrder = CreateOrder { createOrderActions ->
createOrderActions.set(orderId)
},
onApprove = OnApprove { approval ->
// Optional -- retrieve order details first
yourAppsCheckoutRepository.getEC(approval.getData().getOrderId());
// Send the order ID to your own endpoint to capture or authorize the order
yourAppsCheckoutRepository.captureOrder(approval.getData().getOrderId());
}
},
...
}
//不使用paypament自带的按钮
PayPalCheckout.registerCallbacks(
onApprove = OnApprove { approval ->
//注意,这里调用的是后台提供的captureOrder接口
// Optional -- retrieve order details first
yourAppsCheckoutRepository.getEC(approval.getData().getOrderId());
// Send the order ID to your own endpoint to capture or authorize the order
yourAppsCheckoutRepository.captureOrder(approval.getData().getOrderId());
}
},
...
)
PayPalCheckout.startCheckout(
createOrder = CreateOrder { actions ->
actions.set(orderID)
}
)
}
4、测试是否集成结果
从"Account"中拿到创建成功的账号和密码,使用该测试账号测试付款是否成功。
二、支付流程解析
先从客户端集成说起,当开发者调用了createOrderActions.create(order)
这个方法时,实际最终调用的是SDK里面CreateOrderActions.createOrder(order: Order, onOrderCreated: OnOrderCreated?)
方法,该方法向调用paypal的REST API接口的创建订单接口去创建订单,然后当用户点击支付之后,本地通过接口请求,捕获/授权订单并且更新订单状态
服务器端集成则是将创建订单这一步骤的方法放到服务器端,然后再通过网络请求把OrderId拉到本地,再调用createOrderActions.set(orderId)
,然后当用户点击支付之后,通过接口回调,由app开发者调用后台的接口实现捕获/授权并且更新订单状态
核心的流程,paypal内部代码如下,具体过程已经标上注释了:
PayPalCheckout
PayPalCheckout,PayPal提供SDK工具类,通过paypamentButton来开启支付的,实际也是封装了这个类的调用
//注册回调
@JvmStatic
@JvmOverloads
fun registerCallbacks(
onApprove: OnApprove?,
onShippingChange: OnShippingChange? = null,
onCancel: OnCancel?,
onError: OnError?
) {
DebugConfigManager.getInstance().apply {
this.onApprove = onApprove
this.onShippingChange = onShippingChange
this.onCancel = onCancel
this.setOnError(onError)
}
}
//开始支付
@JvmStatic
@RequiresApi(Build.VERSION_CODES.M)
fun startCheckout(createOrder: CreateOrder) {
if (!isConfigSet) {
throw IllegalStateException("CheckoutConfig needs to be set before start() is called!")
}
//初始化一些参数,自行参看,里面最重要的是会把存储的LastToken重置为null
DebugConfigManager.getInstance().apply {
resetFieldsOnPaysheetLaunch()
}
handleLaunchOrder(createOrder, "startCheckout()")
}
private fun handleLaunchOrder(createOrder: CreateOrder, startFunction: String) {
val createOrderActions = SdkComponent.getInstance().createOrderActions
createOrderActions.internalOnOrderCreated = { orderCreateResult: OrderCreateResult? ->
if (orderCreateResult is OrderCreateResult.Success) {
// we must fetch this asap in case someone cancels checkout pre-auth.
SdkComponent.getInstance().repository.fetchCancelURL()
//跳转到PYPLInitiateCheckoutActivity,开始弹窗引导用户去
startInitiateCheckoutActivity(startFunction)
} else if (orderCreateResult is OrderCreateResult.Error) {
onOrderApiFailed(orderCreateResult.exception)
}
}
/*
此处获取到,通过回调,将外部传入的参数封装待用
就是外部我们用来create(order)或者set(orderID)那个回调对象
createOrder = CreateOrder { createOrderActions ->
createOrderActions.set(orderId)
}
*/
createOrder.create(createOrderActions)
}
CreateOrderActions
CreateOrderActions,可以理解为CreateOrderAction的再次封装,封装了createOrderActions.create(order)
和createOrderActions.set(orderId)
的操作
//CreateOrderActions类的创建订单方法
private fun createOrder(order: Order, onOrderCreated: OnOrderCreated?) {
/*开启一个协程,通过SDK封装的网络请求框架,
调用paypal服务器的创建订单接口,成功返回订单id,失败抛出错误
*/
CoroutineScope(coroutineContext).launch {
val orderId = try {
/*调用CreateAction类封装的网络请求,去创建订单,返回订单Id*/
createOrderAction.execute(order)
} catch (exception: Exception) {
internalOnOrderCreated(
OrderCreateResult.Error(
PYPLException("exception when creating order: ${exception.message}")
)
)
PLog.transition(
transitionName = PEnums.TransitionName.CREATE_ORDER_EXECUTED,
outcome = PEnums.Outcome.FAILED
)
null
}
//如果订单Id不为空,封装到OrderCreateResult.Success方法,通过接口回调
orderId?.let { nonNullOrderId ->
// Need to set BA eligibility here as well (Ask product)
onOrderCreated?.onCreated(nonNullOrderId)
internalOnOrderCreated(OrderCreateResult.Success(nonNullOrderId))
PLog.transition(
transitionName = PEnums.TransitionName.CREATE_ORDER_EXECUTED,
outcome = PEnums.Outcome.SUCCESS
)
}
}
}
/**
* 服务器集成时候,调用这个,对比上面的create,少了请求创建订单这些步骤
* 对应的步骤都在服务器进行了操作,所以这里就只拿到OrderId,然后判断是否
* 符合格式,不符合调用接口进行转换后,步骤和create方法没多大差别
* Sets the orderId for checkout.
* Supports Billing Agreement Id or EC Token
*
* @param orderId - id of the order for checkout
*/
fun set(orderId: String) {
CoroutineScope(coroutineContext).launch {
val updatedOrderId = attemptBATokenConversion(orderId)
DebugConfigManager.getInstance().checkoutToken = updatedOrderId
internalOnOrderCreated(OrderCreateResult.Success(updatedOrderId))
}
}
//将orderId进行转换,如果开头是BA的订单id将会调用封装好的网络请求进行转换
private suspend fun attemptBATokenConversion(updatedOrderId: String): String {
// if BA token here, convert to EC token first
val orderId = if (updatedOrderId.startsWith("BA", true)) {
baTokenToEcTokenAction.execute(updatedOrderId)
} else {
updatedOrderId
}
return orderId
}
CreateOderAction
CreateOderAction,创建订单操作,当使用客户端集成并且调用了createOrderActions.create(order)
时,触发的操作,封装了创建订单的网络请求,以及请求成功后的解析
注意:与CreateOderActions是两个单独的类
//CreateOderAction,网络请求
suspend fun execute(order: Order): String {
return withContext(ioDispatcher) {
//获取存储库里是否有token
val existingToken = repository.getLsatToken()
if (existingToken == null) {
val tokenFromAction: String
try {
//没有token,先请求token
tokenFromAction = createLsatTokenAction.execute()
//存储
repository.setLsatToken(tokenFromAction)
//再去创建订单
createOrderWithLsat(order, tokenFromAction)
} catch (ex: CreateLsatTokenException) {
logError("Attempt to create LSAT token failed.")
throw ex
}
} else {
//有token,直接创建订单
createOrderWithLsat(order, existingToken)
}
}
}
private fun createOrderWithLsat(order: Order, lsatToken: String): String {
//通过CreateOrderRequestFactory类封装的网络请求,调用REST API创建订单
val request = createOrderRequestFactory.create(order, lsatToken)
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful) {
val createOrderResponse = try {
gson.fromJson(
StringReader(response.body?.string()),
CreateOrderResponse::class.java
)
} catch (exception: JsonIOException) {
logSerializationException(exception)
throw exception
}
saveResponseValues(createOrderResponse)
return createOrderResponse.id
} else {
val createOrderErrorResponse = try {
gson.fromJson(
StringReader(response.body?.string()),
CreateOrderErrorResponse::class.java
)
} catch (exception: JsonIOException) {
logSerializationException(exception)
throw exception
}
var exceptionMessage = "exception when creating order: ${response.code}."
for (ordersErrorDetails in createOrderErrorResponse.details) {
exceptionMessage += "\nError description: ${ordersErrorDetails.description}." +
"\nField: ${ordersErrorDetails.field}"
}
val exception = PYPLException(exceptionMessage)
PLog.eR(TAG, "exception when creating order ${exception.message}", exception)
throw exception
}
}
/*将创建完成订单后返回的对象存储到DebugConfigManager里面和OrderContext里面,
注意:使用服务器集成的时候,就没有这几步,所以在OnApproval回调不要去调用
approval.orderActions.authorize/capture这两个,要不然会报错(
Tried to retrieve OrderContext before it was created.),因为这两个接口
里面其实封装了一层网络请求,里面会用到OrderContext.get()这个对象,如果不是create
而是set的,不会执行OrderContext.create()这个方法,这个方法只有两个地方执行,一个是这里
一个是在PYPLInitiateCheckoutActivity.restoreCreateOrderContext()方法
*/
private fun saveResponseValues(response: CreateOrderResponse) {
val orderId = response.id
DebugConfigManager.getInstance().checkoutToken = orderId
// get the order capture url
var orderCaptureUrl = response.links.find { it.rel == "capture" }?.href
DebugConfigManager.getInstance().orderCaptureUrl = orderCaptureUrl
var orderAuthorizeUrl = response.links.find { it.rel == "authorize" }?.href
DebugConfigManager.getInstance().orderAuthorizeUrl = orderAuthorizeUrl
val orderPatchUrl = response.links.find { it.rel == "update" }?.href
val checkoutEnvironment = DebugConfigManager.getInstance().checkoutEnvironment
// TODO: Remove this hard coded working Capture & Authorize URL. Figure out why MsMaster is giving us broken urls in response
if (checkoutEnvironment.environment == RunTimeEnvironment.STAGE.toString()) {
orderCaptureUrl = orderCaptureUrl?.let {
"${checkoutEnvironment.restUrl}/v2/checkout/orders/$orderId/capture"
}
orderAuthorizeUrl = orderAuthorizeUrl?.let {
"${checkoutEnvironment.restUrl}/v2/checkout/orders/$orderId/authorize"
}
}
// Create OrderContext for future capture or authorize call.
OrderContext.create(orderId, orderCaptureUrl, orderAuthorizeUrl, orderPatchUrl)
}
CreateOrderRequestFactory
CreateOrderRequestFactory,订单创建工厂类,本质就是调用REST API的订单创建接口,使用服务器集成的时候,这个步骤是服务器处理,然后将订单id传递给app
/**
* This factory creates [Request]s for the create order API call.
*/
class CreateOrderRequestFactory @Inject constructor(
private val requestBuilder: Request.Builder,
private val gson: Gson
) {
/**
* @return a [Request] for the create order API call.
*
* @param order - [Order] to create
* @param accessToken - access token for authentication
*/
internal fun create(order: Order, accessToken: String): Request {
return requestBuilder.apply {
setOrdersUrl()/*此处调用就是创建订单的接口,/v2/checkout/orders*/
addRestHeaders(accessToken)
addPostBody(gson.toJson(order))
}.build()
}
}
CreateLsatTokenAction类
CreateLsatTokenAction,封装了获取LSTAT token的操作,当创建订单的时候,没有缓存token就会触发该动作
//CreateLsatTokenAction类,请求auth/token接口,返回token
suspend fun execute(): String {
val response = getResponse()
val responseString = try {
response.body!!.use { responseBody ->
try {
responseBody.string()
} catch (ex: IOException) {
throw CreateLsatTokenException(clientId, ex).also { exception ->
logError(ERROR_RESPONSE_BODY_TO_STRING_FAILED, exception)
}
}
}
} catch (ex: NullPointerException) {
throw CreateLsatTokenException(clientId, ex).also { exception ->
logError(ERROR_RESPONSE_BODY_NULL, exception)
}
}
return try {
val responseJSON = JSONObject(responseString)
responseJSON.getString("access_token")
} catch (ex: JSONException) {
throw CreateLsatTokenException(clientId, ex).also { exception ->
logError(ERROR_ACCESS_TOKEN_MISSING, exception)
}
}
}
//CreateLsatTokenAction类
private suspend fun getResponse(retryAttempts: Int = 0): Response {
//调用LsatTokenRequestFactory封装的网络请求,传入clientId(此处这个clientId就是订单Id)
val lsatRequest = lsatTokenRequestFactory.create(clientId)
return try {
withContext(ioDispatcher) {
okHttpClient.newCall(lsatRequest).execute()
}
} catch (ex: IOException) {
//请求失败重试三次,三次不成功抛出失败回调
if (retryAttempts < 3) {
val delayAmount = (150L * (retryAttempts + 1))
delay(delayAmount)
getResponse(retryAttempts + 1)
} else {
throw CreateLsatTokenException(clientId, ex).also { exception ->
logError(ERROR_UNABLE_TO_CREATE_ACCESS_TOKEN, exception)
}
}
}
}
LsatTokenRequestFactory
LsatTokenRequestFactory,LSAT token请求网络工厂;此处不考虑其它,用途就是为了获取到订单创建需要传递的参数
//LsatTokenRequestFactory,获取token的工厂类,[DebugConfigManager]是一个单例,里面存储着网络请求等所需的一些参数
class LsatTokenRequestFactory @Inject constructor(
debugConfigManager: DebugConfigManager
) {
private val checkoutEnvironment: CheckoutEnvironment = debugConfigManager.checkoutEnvironment
private val requestUrl = "${checkoutEnvironment.restUrl}/v1/oauth2/token"
fun create(clientId: String): Request {
val body = RequestBody.create(null, "grant_type=client_credentials")
return Request.Builder()
.url(requestUrl)
.addBasicRestHeaders(clientId)
.post(body)
.build()
}
}
用户确认付款
在经过以上一系列处理,当用户点击“完成购物”的时候;这时候经过一系列复杂的流程,将结果返回到注册监听器中,不考虑其它失败的回调;当回调成功后,如果是客户端集成的,根据你传入的订单的OrderIntent是OrderIntent.AUTHORIZE
或OrderIntent.CAPTURE
进行接口回调,一定要注册对应approval.orderActions.authorize/capture{}回调;因为这两个回调本质上封装了两个不同网络请求,都是对订单进行授权或者捕获,最终paypal才会从用户的账户中扣钱,如果你没有实现,就会出现,点击完成购物,然后没有扣钱的情况。
onApprove = OnApprove { approval ->
Log.i(tag, "OnApprove: $approval")
when (selectedOrderIntent) {
//授权订单,实际里面会调用一个网络请求
OrderIntent.AUTHORIZE -> approval.orderActions.authorize { result ->
val message = when (result) {
is AuthorizeOrderResult.Success -> {
Log.i(tag, "Success: $result")
"💰 Order Authorization Succeeded 💰"
}
is AuthorizeOrderResult.Error -> {
Log.i(tag, "Error: $result")
"🔥 Order Authorization Failed 🔥"
}
}
showSnackbar(message)
}
OrderIntent.CAPTURE -> approval.orderActions.capture { result ->
val message = when (result) {
is CaptureOrderResult.Success -> {
Log.i(tag, "Success: $result")
"💰 Order Capture Succeeded 💰"
}
is CaptureOrderResult.Error -> {
Log.i(tag, "Error: $result")
"🔥 Order Capture Failed 🔥"
}
}
showSnackbar(message)
}
}
},
AuthorizeOrderAction
AuthorizeOrderAction,授权订单动作,用户点击完成购物后,如果开发者实现了 approval.orderActions.authorize {}
接口回调,就会触发这个动作,对订单状态进行更新,从Create->Complete,同时paypal将用户的付款冻结,延迟几天再支付给商户,具体参考API文档说明,没有特殊要求推荐使用OrderIntent.CAPTURE
class AuthorizeOrderAction @Inject constructor(
private val updateOrderStatusAction: UpdateOrderStatusAction,
@Named(DEFAULT_DISPATCHER_QUALIFIER) private val defaultDispatcher: CoroutineDispatcher
) {
suspend fun execute(): AuthorizeOrderResult {
return withContext(defaultDispatcher) {
try {
//本质就是通过这个类,然后调用UpdateOrderStatusAction,封装的网络请求,调用REST API的授权订单接口,更新状态
when (val response = updateOrderStatusAction.execute()) {
is UpdateOrderStatusResult.Success -> {
AuthorizeOrderResult.Success(response.orderResponse)
}
is UpdateOrderStatusResult.Error -> response.mapError()
}
} catch (e: Throwable) {
AuthorizeOrderResult.Error(
reason = "$ERROR_REASON_AUTHORIZE_FAILED ${e.message}"
)
}
}
}
private fun UpdateOrderStatusResult.Error.mapError(): AuthorizeOrderResult.Error {
return when (this) {
UpdateOrderStatusResult.Error.LsatTokenUpgradeError -> {
AuthorizeOrderResult.Error(reason = ERROR_REASON_LSAT_UPGRADE_FAILED)
}
is UpdateOrderStatusResult.Error.UpdateOrderStatusError -> {
AuthorizeOrderResult.Error(
reason = "$ERROR_REASON_AUTHORIZE_FAILED Response status code: $responseCode"
)
}
UpdateOrderStatusResult.Error.InvalidUpdateOrderRequest -> {
AuthorizeOrderResult.Error(reason = ERROR_REASON_NO_AUTHORIZE_URL)
}
}.also { error ->
PLog.error(
errorType = PEnums.ErrorType.WARNING,
code = PEnums.EventCode.E570,
message = error.message,
details = error.reason,
transitionName = PEnums.TransitionName.ORDER_CAPTURE_EXECUTED
)
}
}
}
/**
* AuthorizeOrderResult communicates whether Authorize Order succeeded or failed.
*/
sealed class AuthorizeOrderResult {
/**
* Success means that the order was authorized successfully (the request completed with a 2xx
* status code).
*
* @param orderResponse contains details about the order after it was authorized. In extremely
* rare circumstances this value may be null.
*/
data class Success(
val orderResponse: OrderResponse?
) : AuthorizeOrderResult()
/**
* Error means that the order was not authorized successfully (the request completed with a
* non-2xx status code).
*/
data class Error(
val message: String = ERROR_MESSAGE_AUTHORIZE_ORDER,
val reason: String
) : AuthorizeOrderResult() {
companion object {
const val ERROR_MESSAGE_AUTHORIZE_ORDER = "Authorize order failed."
const val ERROR_REASON_NO_AUTHORIZE_URL = "Authorize was invoked when the order did not have a" +
" valid authorize url. This typically happens when authorize is called for a capture" +
" order or if authorize was invoked prior to the order being approved."
const val ERROR_REASON_LSAT_UPGRADE_FAILED = "LSAT upgrade failed while authorizing order."
const val ERROR_REASON_AUTHORIZE_FAILED = "Authorize order response was not successful."
}
}
}
CaptureOrderAction
CaptureOrderAction,捕获订单动作,用户点击完成购物后,如果开发者实现了 approval.orderActions.capture {}
接口回调,就会触发这个动作,对订单状态进行更新,从Create->Complete,同时paypal将用户的付款划到商家的账户中
class CaptureOrderAction @Inject constructor(
private val updateOrderStatusAction: UpdateOrderStatusAction,
@Named(DEFAULT_DISPATCHER_QUALIFIER) private val defaultDispatcher: CoroutineDispatcher
) {
suspend fun execute(): CaptureOrderResult {
return withContext(defaultDispatcher) {
try {
when (val response = updateOrderStatusAction.execute()) {
is UpdateOrderStatusResult.Success -> {
CaptureOrderResult.Success(response.orderResponse)
}
is UpdateOrderStatusResult.Error -> response.mapError()
}
} catch (e: Throwable) {
CaptureOrderResult.Error(
reason = "$ERROR_REASON_CAPTURE_FAILED ${e.message}"
)
}
}
}
private fun UpdateOrderStatusResult.Error.mapError(): CaptureOrderResult.Error {
return when (this) {
UpdateOrderStatusResult.Error.LsatTokenUpgradeError -> {
CaptureOrderResult.Error(reason = ERROR_REASON_LSAT_UPGRADE_FAILED)
}
UpdateOrderStatusResult.Error.InvalidUpdateOrderRequest -> {
CaptureOrderResult.Error(reason = ERROR_REASON_NO_CAPTURE_URL)
}
is UpdateOrderStatusResult.Error.UpdateOrderStatusError -> {
CaptureOrderResult.Error(
reason = "$ERROR_REASON_CAPTURE_FAILED Response status code: $responseCode"
)
}
}.also { error ->
PLog.error(
errorType = PEnums.ErrorType.WARNING,
code = PEnums.EventCode.E570,
message = error.message,
details = error.reason,
transitionName = PEnums.TransitionName.ORDER_CAPTURE_EXECUTED
)
}
}
}
/**
* CaptureOrderResult communicates whether Capture Order succeeded or failed.
*/
sealed class CaptureOrderResult {
/**
* Success means that the order was captured successfully (the request completed with a 2xx
* status code).
*
* @param orderResponse contains details about the order after it was captured. In extremely
* rare circumstances this value may be null.
*/
data class Success(
val orderResponse: OrderResponse?
) : CaptureOrderResult()
/**
* Error means that the order was not captured successfully (the request completed with a
* non-2xx status code).
*/
data class Error(
val message: String = ERROR_MESSAGE_CAPTURE_ORDER,
val reason: String
) : CaptureOrderResult() {
companion object {
const val ERROR_MESSAGE_CAPTURE_ORDER = "Capture order failed."
const val ERROR_REASON_NO_CAPTURE_URL = "Capture was invoked when the order did not have a" +
" valid capture url. This typically happens when capture is called for an authorize" +
" order or if capture was invoked prior to the order being approved."
const val ERROR_REASON_LSAT_UPGRADE_FAILED = "LSAT upgrade failed while capturing order."
const val ERROR_REASON_CAPTURE_FAILED = "Capture order response was not successful."
}
}
}
UpdateOrderStatusAction
UpdateOrderStatusAction类,顾名思义,就是更新订单状态用的,无论是授权还是捕获,归根都是调用这个类,然后对订单的状态进行更新
class UpdateOrderStatusAction @Inject constructor(
private val updateOrderStatusRequestFactory: UpdateOrderStatusRequestFactory,
private val upgradeLsatTokenAction: UpgradeLsatTokenAction,
private val debugConfigManager: DebugConfigManager,
private val okHttpClient: OkHttpClient,
private val gson: Gson,
@Named(IO_DISPATCHER_QUALIFIER) private val ioDispatcher: CoroutineDispatcher,
@Named(DEFAULT_DISPATCHER_QUALIFIER) private val defaultDispatcher: CoroutineDispatcher
) {
private val TAG = UpdateOrderStatusAction::class.java.simpleName
suspend fun execute(): UpdateOrderStatusResult {
val orderContext = withContext(defaultDispatcher) {
/*
到这里,你就明白客户端集成,和服务器端集成的区别在哪了,客户端集成,订单在本地创建,该有的参数都有,
相比,服务器端集成只有一个订单id,所以如果实现了onApproval回调,不需要监听授权还是捕获成功,因为这
两个需要自己实现
*/
OrderContext.get().also { context ->
debugConfigManager.checkoutToken = context.orderId
OrderContext.clear()
}
}
return when (val upgradeLsatTokenResponse = upgradeLsatTokenAction.execute()) {
is UpgradeLsatTokenResponse.Success -> {
try {
val request = updateOrderStatusRequestFactory
.create(orderContext, upgradeLsatTokenResponse.upgradedAccessToken)
updateOrderStatus(request)
} catch (ex: NoValidUpdateOrderStatusUrlFound) {
UpdateOrderStatusResult.Error.InvalidUpdateOrderRequest
}
}
UpgradeLsatTokenResponse.Failed -> UpdateOrderStatusResult.Error.LsatTokenUpgradeError
}
}
private suspend fun updateOrderStatus(request: Request): UpdateOrderStatusResult {
return withContext(ioDispatcher) {
try {
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful) {
val orderResponse = response.body?.use { responseBody ->
val responseString = responseBody.string()
gson.fromJson(responseString, OrderResponse::class.java)
}
UpdateOrderStatusResult.Success(orderResponse)
} else {
UpdateOrderStatusResult.Error.UpdateOrderStatusError(response.code)
}
} catch (e: Exception) {
PLog.e(TAG, e.toString(), e)
UpdateOrderStatusResult.Error.UpdateOrderStatusError(RESPONSE_CODE_EXCEPTION)
}
}
}
}
sealed class UpdateOrderStatusResult {
data class Success(val orderResponse: OrderResponse?) : UpdateOrderStatusResult()
sealed class Error : UpdateOrderStatusResult() {
object LsatTokenUpgradeError : Error()
object InvalidUpdateOrderRequest : Error()
data class UpdateOrderStatusError(val responseCode: Int) : Error()
}
}
UpdateOrderStatusRequestFactory
UpdateOrderStatusRequestFactory,更新订单状态的网络请求工厂,根据创建订单配置的OrderIntent是哪个,调REST API
的哪个接口
class UpdateOrderStatusRequestFactory @Inject constructor() {
private val TAG = javaClass.simpleName
//网络请求,根据当前订单的这个字段:"intent": "CAPTURE",判断是要调用哪个接口
fun create(orderContext: OrderContext, merchantAccessToken: String): Request {
val url = when (orderContext.orderIntent) {
OrderIntent.CAPTURE -> orderContext.captureUrl!!
OrderIntent.AUTHORIZE -> orderContext.authorizeUrl!!
null -> throw NoValidUpdateOrderStatusUrlFound(orderContext)
}
PLog.d(TAG, "Creating update order status request with url: $url")
val body = RequestBody.create(null, "")
return Request.Builder()
.addMerchantRestHeaders(merchantAccessToken)
.url(url)
.post(body)
.build()
}
}
class NoValidUpdateOrderStatusUrlFound(
orderContext: OrderContext
) : RuntimeException(
"Unable to create a valid UpdateOrderStatusRequest as no valid URL was found: $orderContext"
)
补充
如果服务器集成,后台不想捕获订单,Android这边可以取巧来实现这个功能(IOS不行,IOS如果要捕获,要自行请求PayPal REST API),附上代码:
PayPalCheckout.registerCallbacks(
onApprove = OnApprove { approval ->
//和客户端一样正常注册回调,但是在注册回调的时候,要封装一层获取订单详情的回调,通过这层回调会拿到LAST toeken并且保存起来,要不然会报LSAT错误,然后再判断返回的订单状态是否是用户批准付款了
approval.orderActions.getOrderDetails {
when (it) {
is GetOrderResult.Success -> {
//如果订单的状态不是用户允许的状态,就不让做其它操作
if (it.orderResponse.status != OrderStatus.APPROVED){
return@getOrderDetails
}
when (selectedOrderIntent) {
OrderIntent.AUTHORIZE -> approval.orderActions.authorize { result ->
val message = when (result) {
is AuthorizeOrderResult.Success -> {
Log.e(tag, "Success: $result")
"💰 Order Authorization Succeeded 💰"
}
is AuthorizeOrderResult.Error -> {
Log.e(tag, "Error: $result")
"🔥 Order Authorization Failed 🔥"
}
}
}
OrderIntent.CAPTURE -> approval.orderActions.capture { result ->
val message = when (result) {
is CaptureOrderResult.Success -> {
Log.e(tag, "Success: $result")
"💰 Order Capture Succeeded 💰"
}
is CaptureOrderResult.Error -> {
Log.e(tag, "Error: $result")
"🔥 Order Capture Failed 🔥"
}
}
}
}
}
else -> {}
}
}
},
onCancel = OnCancel {
Log.e(tag, "OnCancel")
},
onError = OnError { errorInfo ->
Log.e(tag, "ErrorInfo: $errorInfo")
}
)
}
PayPalCheckout.startCheckout(
createOrder = CreateOrder { createOrderActions ->
//协程,模拟服务器生成订单
uiScope.launch {
//模拟服务器生成订单
val orderId = createOrder()?.id
orderId?.let {
createOrderActions.set(orderId)
//参考客户端集成,OrderAction.saveResponseValues()方法
val checkoutEnvironment =
DebugConfigManager.getInstance().checkoutEnvironment
val orderCaptureUrl =
"${checkoutEnvironment.restUrl}/v2/checkout/orders/$orderId/capture"
val orderAuthorizeUrl =
"${checkoutEnvironment.restUrl}/v2/checkout/orders/$orderId/authorize"
val orderPatchUrl =
"${checkoutEnvironment.restUrl}/v2/checkout/orders/$orderId"
DebugConfigManager.getInstance().orderCaptureUrl = orderCaptureUrl
DebugConfigManager.getInstance().orderAuthorizeUrl = orderAuthorizeUrl
/*
注意,OrderContext传入的url二选一,不能都填入,因为:
val orderIntent: OrderIntent? = if (captureUrl != null && authorizeUrl == null) {
OrderIntent.CAPTURE
} else if (authorizeUrl != null && captureUrl == null) {
OrderIntent.AUTHORIZE
} else {
PLog.dR(TAG, "OrderContext is in an invalid state: ${toString()}")
null
}
*/
OrderContext.create(
orderId,
orderCaptureUrl,
null,
orderPatchUrl
)
}
}
}
)
- FoldStar 2022/12/14 ↩︎