Android PayPal支付的接入和SDK支付过程解析

根据产品需求,要接入PayPal支付,在研究了几天支付后,总结下内容给新手使用,本篇文章如果与官方文档有差异的话,一切以官方为准。转载请注明出处。
注: 本篇文章只针对新版本的SDK进行解析,解析内容只针对部分功能,其它功能请自行参考PayPal开发者文档和SDK1

目录

  • 一、Paypal接入
  • 1、创建应用
  • 2、应用内集成
  • 3、代码实现
  • 4、测试集成结果
  • 二、支付流程解析

一、Paypal接入

本篇涉及的代码基于Github PayPal的示例进行修改

1、创建应用
  1. 用注册完成的账号登录PayPal开发者平台
  2. 先注册沙盒测试应用,左侧列表,点击 “DASHBOARD” -> “My Apps & Credentials” 后,在右侧窗口点击 “Sandbox” 或者 “Live” -> “Create App” 按钮进入创建沙盒App页面(正式环境把 “Sandbox” 切换成 “Live”)。 注: 沙盒界面,会默认创建了一个"DefaultApp"的默认应用,你也可以使用该应用进行测试。
  3. 在弹出的 “Create New App” 界面填写资料,完成后点击 “Create App”:
  • App Name 应用名称,按需填写
  • App Type 应用类型(Merchant:商家, Platform:平台)默认选择第一个
  • Sandbox Business Account 沙盒企业账号。沙盒账号默认就行,测试用的企业账号,正式环境需要填写对应的企业账号,账号可以在"Account"创建,默认创建沙盒应用会生成一个个人和企业的账号,可以点击编辑查看密码。
  1. 创建应用完成,点击创建完成的应用 ,根据需求填写 (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、应用内集成
  1. 清单文件声明网络权限
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <uses-permission android:name="android.permission.INTERNET" />
   ...
</manifest>
  1. 在项目根目录下的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'
            }
        }
    }
}
  1. 在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')
}
  1. 在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)
    }
}
  1. 根据需求在需要用到支付的地方集成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.AUTHORIZEOrderIntent.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
                        )
                    }
                }
            }
        )

  1. FoldStar 2022/12/14 ↩︎