Android完整接入PayPal支付及常见问题

前言

1.我是参考了一下@TimberBug哥子的文章https://www.jianshu.com/p/346e7f308f23,感谢,了解了一个大概,但是支付的代码没跑起来,最终还是参考Paypal的Demo代码才跑起来,估计是我用的最新版本的问题,然后吐槽哈Paypal官方的Android的示例代码,你们示例代码写得那么简单,看得大家一脸懵逼,真的良心不会痛吗?
2.服务端集成接口文档地址:https://developer.paypal.com/docs/api/orders/v2/
3.Android端集成接口文档地址:https://developer.paypal.com/docs/business/native-checkout/android/或新版的地址https://developer.paypal.com/sdk/in-app/android/

4.墙裂建议开个webview来集成js支付方式,放弃这个坑逼的AndroidSdk,听劝,真的省时省心,集成相关可看文档最后面

Paypal集成准备工作

  1. 登录或注册账号,注册新账号需要选择国家,最好绑定一张银行卡,储蓄卡或信用卡均可
  2. 登录进去在Dashboard界面左边的菜单“My apps & credentials”里面的“REST API apps”项创建一个App或修改一个已有的App,目前系统会默认创建一个Default Application,可直接点击进去修改使用
    image.png
  3. 点进去之后主要有3大块:

1).SANDBOX API CREDENTIALS:配置信息相关;主要包含Sandbox account(沙箱环境商家收款邮箱账号),Client ID(后台Api接口认证加签和App的Sdk初始化需要用到),Secret(后台Api接口认证加签需要用到)

image.png

2).SANDBOX APP SETTINGS:App的Sdk相关配置设置;主要关注Return URL(自定义协议Uri,这个不是支付宝、微信支付完成的回调Url,这个是调起支付需要登录Paypal买家账号时,Paypal会跳转到浏览器进行网页登录,当登录完成需要返回App的时候就需要自定义协议Uri返回我们App里面,格式类类似于网址:com.xxxx.protocol://xxxx.xxx,目前Paypal固定后缀格式:xxx.xxx.xx://paypalpay,一般xxx.xxx.xx设置为当前App的包名);其他需要关注的是Accept payments,必须勾上,且Advanced options展开Native Checkout SDK必须是绿色的勾勾;然后Log in with PayPal,必须勾上,且Advanced options展开Full name和Email必须勾上,Privacy policy URL和User agreement URL随便填写给地址就行,比如百度https://www.baidu.com/;总体参数按我如下截图勾选的应该就没错
image.png

image.png

3).SANDBOX WEBHOOKS:这个就是支付完成Paypal后台通知我们自己后台的配置地方,这个才是类似支付宝、微信支付完成的回调Url,一般配置后台的接口,按需使用
image.png

  1. 其他需要关注的就是沙箱环境里面的卖家测试账号和买家账号,账号类型Type字段Business标识的就是卖家,如果需要就配置在代码收款商Payee位置,Type字段Personal标识的就是买家,需要在App的Sdk拉起网页付款登录时登录这个账号;点开Manage accounts对应的三个点,选择View/edit account,在弹框中可点击Change password修改系统随机生成的密码成你自己想要的,注意:修改保存完成后并不会显示,但是已经修改成功啦,可用这个密码去做付款账号的登录了(但是换成生产账号测试时遇到一个奇怪现象:我在App的Sdk拉起网页付款登录时登录Personal标识的买家账号时,结果网页一直提示我是登录的卖家账号,不能完成登录付款,换成Business标识的账号登录就成功了,测试环境就必须登录Personal标识的才能付款,因此暂时只能定义成Paypal的Bug了)


    image.png

    image.png

Paypal Android SDK集成工作

  • 1.开启网络权限
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
      ...
      <uses-permission android:name="android.permission.INTERNET" />
      ...
  </manifest>
  • 2.项目根build.gradle文件中配置paypal的仓库
allprojects {
    repositories {
        ...
        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'  //官方文档这里字符串没得单引号,导致报错,差评
            }
        }
    }
}
  • 3.添加Java 8的兼容处理
android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}
  • 4.添加Paypal的lib库最新版依赖(注:因Paypal Sdk是用kotlin写的,请提前添加androidx.core:core-ktx和org.jetbrains.kotlin:kotlin-stdlib-jdk7支持)
implementation('com.paypal.checkout:android-sdk:0.5.2') {
        exclude group: 'com.google.code.gson', module: 'gson'  //这里排除gson是因为和我主项目的gson冲突了,而且paypal依赖的还是比我新的版本,导致我本地报错了;其实还有个重要冲突是okhttp,我们项目用的是3.14.0,paypal用的是4.8.0,因为4.x版本比3.x版本变化比较大,而且还不能排除paypal的4.x版本,会导致paypal初始化就报错,所以我们只有把主项目okhttp升级到最新了
    }

Paypal Android SDK开发工作

  • 1.在Application的onCreate方法做如下Paypal初始化操作
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//其中clientId需要替换成上面步骤申请到的客户端id
    PayPalCheckout.setConfig(CheckoutConfig(application, clientId, if (BuildConfig.DEBUG) Environment.SANDBOX else Environment.LIVE, String.format("%s://paypalpay", BuildConfig.APPLICATION_ID), CurrencyCode.USD, UserAction.PAY_NOW, PaymentButtonIntent.CAPTURE, SettingsConfig(BuildConfig.DEBUG, false)))
}else{
    System.out.println("initPayPal失败,系统版本过低")
}
  • 2.Paypal的3种代码集成方式

1). Client-side integration: 客户端集成,主要特点是需要集成一个PayPal自带的支付按钮PayPalButton,这个按钮仅可做少量Paypal固定的UI定制,侵入性比较强,然后创建订单和执行订单捕获逻辑均在App端,适用于不要后台参与的情况

2). Server-side integration: 服务端集成,主要特点是服务端创建订单和执行订单捕获,App端只需接收服务端返回的paypal订单id,调用createOrderActions.set(orderId)设置即可开启订单支付逻辑,适合于主流支付逻辑,后台订单可信可控,我们采用的这种方式

3). Programmatically start the SDK: 客户端集成,主要特点和Client-side integration比较类似,只是不需要集成PayPal自带的支付按钮PayPalButton,然后创建订单和订单捕获逻辑均在App端,适用于不要后台参与的情况

  • 3.我们集成Server-side integration的代码如下(参数paypalOrderId为后台去调paypal的创单接口获取到的订单id)

我们的逻辑就是如下代码示例,当执行PayPalCheckout.start()方法开启支付时,同步开启一个定时器,循环去轮询paypal订单的状态,接口为REST v2订单详情,检测status是否是COMPLETED支付成功了

    private var checkOrderMax: Int = 300//循环检测的最大次数
    private var checkOrderCount: Int = -1//循环检测的初始次数
    private var isCheckOrderFlag: Boolean = false//是否检测订单状态中

  @RequiresApi(Build.VERSION_CODES.M)
  private fun startPaypal(context: Activity, paypalOrderId: String){
        PayPalCheckout.start(CreateOrder { createOrderActions -> createOrderActions.set(paypalOrderId) }, null, null,
            OnCancel {//实测取消订单后没走这个方法,走到了报错,报的returnUrl是空,但我们再paypal后台和sdk初始化均设置了的,而且在手机浏览器登录paypal买家账号后也是可以拉回我们app的,但是它还是会报这个错,无语子,估计还是paypal得bug
                resetCheckPaypalOrder()
                System.out.println("用户取消Paypal支付")
            },
            OnError { errorInfo ->
                resetCheckPaypalOrder()
                System.out.println("==========paypal onError=======ErrorInfo=======>: $errorInfo")
                System.out.println(if(errorInfo?.reason.isNullOrBlank()) (if(errorInfo?.error?.message.isNullOrBlank())
                    "Paypal支付未知错误" else errorInfo.error.message) else errorInfo!!.reason)
            })
        checkOrderCount = 0
        isCheckOrderFlag = true
        startCheckPaypalOrder(context, paypalOrderId)
    }

    private fun startCheckPaypalOrder(context: Activity, paypalOrderId: String){
        System.out.println("======Stephen=======checkPaypalOrder====>Count:$checkOrderCount")
        if(!isCheckOrderFlag)return
        if(checkOrderCount >= checkOrderMax){
            resetCheckPaypalOrder()
            System.out.println("Paypal订单支付等待超时")
            return
        }//end of if
        ApiRequestMethod.checkPaypalOrder(paypalOrderId, object : RequestAllCallback<String> {//这个是我们接口调用封装方法,换成你们自己的哈(REST v2订单详情接口:https://developer.paypal.com/api/orders/v2/#orders_get)
            override fun onSuccess(data: String?) {//接口返回正确json形如:{"paypal_status":"CREATED"}
                var isContinueLoop = true
                val jsonObject = ToolUtils.instance.fromJsonToObj(data)
                if(null != jsonObject && jsonObject.has("paypal_status")){
                    when(jsonObject.getString("paypal_status")){
                        "APPROVED" -> {//这个状态表示用户已经支付完成,后台开始捕获订单并开始执行确认了,实测这个状态有时差不多得持续1分多钟,快的话也是耗费20多秒,因此添加一个loading逻辑如下,此具体loading显示逻辑更换成你自己实际的loading框,注意loading框得依附在你自己App的当前支付界面上,因为你看到paypal的支付框实质是一个activity,依附在paypal上面会被误关闭
                            if(!ToolUtils.instance.isLoadingShow())ToolUtils.instance.showLoading("确认支付结果中...", appendActivity = context)
                        }
                        "COMPLETED" -> {//这个状态表示后台捕获确认订单完成,也就是这笔订单真正的完成了
                            isContinueLoop = false
                            resetCheckPaypalOrder()
                            System.out.println("Paypal订单支付成功")
                        }
                    }
                }//end of if
                if(isContinueLoop){
                    ToolUtils.instance.delayExecute(1000L){//这个是我们延时调用封装方法,换成你们自己的哈
                        checkOrderCount++
                        startCheckPaypalOrder(context, paypalOrderId)
                    }
                }//end of if
            }

            override fun onFailure(aliErrorResponse: AliErrorResponse, httpCode: Int): Boolean {
                ToolUtils.instance.delayExecute(1000L){
                    checkOrderCount++
                    startCheckPaypalOrder(context, paypalOrderId)
                }
                return false
            }
        })
    }

    private fun resetCheckPaypalOrder(){
        checkOrderCount = 0
        isCheckOrderFlag = false
        ToolUtils.instance.closeLoading()//关闭loading框
    }

附上一个流程图
流程图

附上几个操作中的交互图


浏览器登录账号

拉起支付

原生Sdk支付

网页界面支付

点击空白处或返回键关闭订单

浏览器通过returnUrl拉回App
  • 4.说一下我们集成Server-side integration的代码遇到的问题

1).当开始支付时如果没有登录过Paypal买家账号或账号登录过期了,Sdk会拉起外部浏览器进行网页登录,登录成功后会通过returnUrl拉回业务App进行原生App的支付,然而有时又会直接就待在外部浏览器网页进行支付,支付完成后才会拉回业务App;更有情况登录操作是直接拉起的Sdk内部Webview进行操作,具体原因均未可知,知道的铁子可评论区回复哈(PS:外部浏览器网页通过returnUrl拉起App这个操作有时会直接无效,连“是否跳转XX App”这个弹窗都没有,排查N久无效,猜测应该是浏览器屏蔽了网页的频繁弹窗导致的,具体原因也未可知)

2).另一个最大的异常问题就是支付中频繁切换后台程序了:调start拉起paypal的loading界面是有一两秒延迟的,此时如果立马执行挂后台操作,有一定几率导致paypal的loading及付款界面(com.paypal.pyplcheckout.home.view.activities.PYPLHomeActivity)显示不出来,直接导致支付流程中断无反应了;还有当sdk拉起浏览器登录paypal账号或支付时,此时从最近任务列表切换回app,app会回调到OnError方法里面,此时我们的业务支付流程已经结束了,但如果此时仍然回浏览器完成paypal的支付,这时支付完成首先可能会造成一个异常单问题(paypal扣了用户的钱,但是我们后台订单有可能已经关闭导致用户没得到相应的服务)

3).官方文档写的:【Note: If you're integrating for the first time, we recommend using the REST v2 server-side integration. You must complete the first five steps in a Client-side integration.】,意思是服务端集成前面步骤和客户端集成一样,需要添加PayPal自带的支付按钮PayPalButton,然后调用payPalButton.setup()方法设置订单id,同时也可以覆盖OnApprove()回调方法;然而我实测发现,可以不用集成支付按钮PayPalButton,直接调用PayPalCheckout.start()方法设置订单id即可开启支付流程,只是最好不要去覆盖OnApprove()回调方法,因为这里执行it.orderActions.capture { }捕获状态时,会报上下文异常:

com.paypal.checkout.order.OrderContextNotAvailableException: Tried to retrieve OrderContext before it was created.
        at com.paypal.checkout.order.OrderContext$Companion.get(OrderContext.kt:27)
        at com.paypal.checkout.order.UpdateOrderStatusAction$execute$orderContext$1.invokeSuspend(UpdateOrderStatusAction.kt:28)
        at com.paypal.checkout.order.UpdateOrderStatusAction$execute$orderContext$1.invoke(Unknown Source:10)
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:161)
        at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
        at com.paypal.checkout.order.UpdateOrderStatusAction.execute(UpdateOrderStatusAction.kt:27)
        at com.paypal.checkout.order.CaptureOrderAction$execute$2.invokeSuspend(CaptureOrderAction.kt:21)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

4).还有一个坑爹的问题,唤起paypal支付拉到浏览器后在最近任务里面切换回app,sdk回调到OnError方法,此时再次切换到浏览器paypal登录/支付页面,执行登录购买操作,完成拉回业务app时,app会被paypal的sdk搞崩溃,应该是支付服务在AppResume时Sdk回调OnError方法后支付服务被关闭导致的服务状态异常,最关键是还没法拦截处理,报错有时还不一样,我抓到下面两种异常,再次无语子

2021-12-27 17:51:45.505 E/DEBUG: Back traces starts.
2021-12-27 17:51:45.509 E/DEBUG:     at com.paypal.openid.AuthorizationService.a(Unknown Source:9)
2021-12-27 17:51:45.507 E/DEBUG: java.lang.IllegalStateException: Service has been disposed and rendered inoperable
2021-12-27 17:51:45.510 E/DEBUG:     at com.paypal.openid.AuthorizationService.performTokenRequest(Unknown Source:0)
2021-12-27 17:51:45.509 E/DEBUG:     at com.paypal.openid.AuthorizationService.a(Unknown Source:9)
2021-12-27 17:51:45.511 E/DEBUG:     at com.paypal.openid.AuthorizationService.performTokenRequest(Unknown Source:2)
2021-12-27 17:51:45.510 E/DEBUG:     at com.paypal.openid.AuthorizationService.performTokenRequest(Unknown Source:0)
2021-12-27 17:51:45.513 E/DEBUG:     at com.paypal.authcore.authentication.Authenticator.a(Unknown Source:191)
2021-12-27 17:51:45.511 E/DEBUG:     at com.paypal.openid.AuthorizationService.performTokenRequest(Unknown Source:2)
2021-12-27 17:51:45.514 E/DEBUG:     at com.paypal.authcore.authentication.Authenticator.authenticateForAccessTokenWithDelegate(Unknown Source:86)
2021-12-27 17:51:45.513 E/DEBUG:     at com.paypal.authcore.authentication.Authenticator.a(Unknown Source:191)
2021-12-27 17:51:45.515 E/DEBUG:     at com.paypal.pyplcheckout.flavorauth.ThirdPartyAuth.getUserAccessToken(ThirdPartyAuth.java:149)
2021-12-27 17:51:45.514 E/DEBUG:     at com.paypal.authcore.authentication.Authenticator.authenticateForAccessTokenWithDelegate(Unknown Source:86)
2021-12-27 17:51:45.516 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel.logInUser(MainPaysheetViewModel.java:1629)
2021-12-27 17:51:45.515 E/DEBUG:     at com.paypal.pyplcheckout.flavorauth.ThirdPartyAuth.getUserAccessToken(ThirdPartyAuth.java:149)
2021-12-27 17:51:45.517 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel.$r8$lambda$u3bJQV5CM255mmoJkHAH5Y7Tgzs(Unknown Source:0)
2021-12-27 17:51:45.516 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel.logInUser(MainPaysheetViewModel.java:1629)
2021-12-27 17:51:45.518 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel$$ExternalSyntheticLambda9.onUpdateClientConfig(Unknown Source:2)
2021-12-27 17:51:45.517 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel.$r8$lambda$u3bJQV5CM255mmoJkHAH5Y7Tgzs(Unknown Source:0)
2021-12-27 17:51:45.519 V/nxoBaseCallback: https://www.paypal.com/xoplatform/logger/api/logger returned with response
2021-12-27 17:51:45.518 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel$$ExternalSyntheticLambda9.onUpdateClientConfig(Unknown Source:2)
2021-12-27 17:51:45.520 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel.lambda$updateClientConfigBefore$22(MainPaysheetViewModel.java:818)
2021-12-27 17:51:45.519 V/nxoBaseCallback: https://www.paypal.com/xoplatform/logger/api/logger returned with response
2021-12-27 17:51:45.521 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel$$ExternalSyntheticLambda0.onEvent(Unknown Source:2)
2021-12-27 17:51:45.520 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel.lambda$updateClientConfigBefore$22(MainPaysheetViewModel.java:818)
2021-12-27 17:51:45.522 E/DEBUG:     at com.paypal.pyplcheckout.events.Events.fire(Events.java:116)
2021-12-27 17:51:45.521 E/DEBUG:     at com.paypal.pyplcheckout.home.viewmodel.MainPaysheetViewModel$$ExternalSyntheticLambda0.onEvent(Unknown Source:2)
2021-12-27 17:51:45.523 E/DEBUG:     at com.paypal.pyplcheckout.services.callbacks.ClientConfigUpdateCallback.onApiSuccess(ClientConfigUpdateCallback.kt:54)
2021-12-27 17:51:45.522 E/DEBUG:     at com.paypal.pyplcheckout.events.Events.fire(Events.java:116)
2021-12-27 17:51:45.525 E/DEBUG:     at com.paypal.pyplcheckout.services.callbacks.BaseCallback.handleApiSuccess(BaseCallback.kt:114)
2021-12-27 17:51:45.523 E/DEBUG:     at com.paypal.pyplcheckout.services.callbacks.ClientConfigUpdateCallback.onApiSuccess(ClientConfigUpdateCallback.kt:54)
2021-12-27 17:51:45.526 E/DEBUG:     at com.paypal.pyplcheckout.services.callbacks.BaseCallback.onResponse(BaseCallback.kt:68)
2021-12-27 17:51:45.525 E/DEBUG:     at com.paypal.pyplcheckout.services.callbacks.BaseCallback.handleApiSuccess(BaseCallback.kt:114)
2021-12-27 17:51:45.527 V/nxoBaseCallback: https://www.paypal.com/xoplatform/logger/api/logger returned with response
2021-12-27 17:51:45.526 E/DEBUG:     at com.paypal.pyplcheckout.services.callbacks.BaseCallback.onResponse(BaseCallback.kt:68)
2021-12-27 17:51:45.527 E/DEBUG:     at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
2021-12-27 17:51:45.527 V/nxoBaseCallback: https://www.paypal.com/xoplatform/logger/api/logger returned with response
2021-12-27 17:51:45.529 E/DEBUG:     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
2021-12-27 17:51:45.527 E/DEBUG:     at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
2021-12-27 17:51:45.530 E/DEBUG:     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
2021-12-27 17:51:45.529 E/DEBUG:     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
2021-12-27 17:51:45.531 E/DEBUG:     at java.lang.Thread.run(Thread.java:784)
java.lang.Exception: Error Parsing Experiment
        at com.paypal.pyplcheckout.ab.elmo.Elmo.fetchRemoteTreatment(Elmo.kt:108)
2022-01-13 14:38:01.533 28410-513/com.xxxx.xxx.xxx.xxx D/BaseSecureKeyWrapper: generateSignature : Signature Object Signature object: SHA256withECDSA<not initialized>
2022-01-13 14:38:01.569 28410-28410/com.xxxx.xxx.xxx.xxx D/CubicBezierInterpolator: CubicBezierInterpolator  mControlPoint1x = 0.6, mControlPoint1y = 0.9, mControlPoint2x = 0.8, mControlPoint2y = 1.0
        at com.paypal.pyplcheckout.ab.elmo.Elmo$getTreatmentRemote$1.invokeSuspend(Elmo.kt:79)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
  • 5.以上操作Server-side integration集成就完成了,我再补上个参考TimberBug老哥和Paypal的Demo代码前提下我们这边完成的Programmatically start the SDK客户端集成方式的测试代码以做备份:
  fun startPaypal(context: Activity){
      if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
          PayPalCheckout.start(
            createOrder = CreateOrder { actions ->
                val createdItems = listOf(CreatedItem("TestGoods", "1", "1", "0.1", ItemCategory.DIGITAL_GOODS))//参数依次:商品名称,数量,单价,税费,商品类型
                val shippingPreference = ShippingPreference.NO_SHIPPING //航运信息,非必须
                val currencyCode = CurrencyCode.USD //收款货币Code
                //以下为统一计算总数,避免出现不一致情况
                val itemTotal = createdItems.map { it.amount.toDouble() * it.quantity.toInt() }
                    .sum().toBigDecimal().scaledForMoney
                val taxTotal = createdItems.map { it.taxAmount.toDouble() * it.quantity.toInt() }
                    .sum().toBigDecimal().scaledForMoney
                val shippingTotal = BigDecimal(0.00).scaledForMoney
                val handlingTotal = BigDecimal(0.00).scaledForMoney
                val shippingDiscountTotal = BigDecimal(0.00).scaledForMoney
                val itemDiscountTotal = BigDecimal(0.00).scaledForMoney
                val totalValue = itemTotal.add(taxTotal).add(shippingTotal).add(handlingTotal).subtract(shippingDiscountTotal).subtract(itemDiscountTotal)
                actions.create(
                    Order.Builder()
                    .intent(OrderIntent.CAPTURE)
                    .purchaseUnitList(listOf(PurchaseUnit.Builder()
                            .referenceId(UUID.randomUUID().toString())//唯一id
                            .amount(
                                Amount.Builder()
                                    .currencyCode(currencyCode)
                                    .value(totalValue.asMoneyString)
                                    .breakdown(
                                        BreakDown.Builder()
                                            .itemTotal(itemTotal.unitAmountFor(currencyCode))
                                            .shipping(shippingTotal.unitAmountFor(currencyCode))
                                            .handling(handlingTotal.unitAmountFor(currencyCode))
                                            .taxTotal(taxTotal.unitAmountFor(currencyCode))
                                            .shippingDiscount(shippingDiscountTotal.unitAmountFor(currencyCode))
                                            .discount(itemDiscountTotal.unitAmountFor(currencyCode))
                                            .build())
                                    .build())
                            .items(
                                createdItems.map { createdItem ->
                                    Items.Builder().name(createdItem.name)
                                        .quantity(createdItem.quantity)
                                        .category(createdItem.itemCategory)
                                        .unitAmount(UnitAmount.Builder().value(createdItem.amount).currencyCode(currencyCode).build())
                                        .tax(UnitAmount.Builder().value(createdItem.taxAmount).currencyCode(currencyCode).build()).build()
                                })
                            .shipping(Shipping.Builder().address(Address.Builder()
                                        .addressLine1("123 Townsend St")
                                        .addressLine2("Floor 6")
                                        .adminArea2("San Francisco")
                                        .adminArea1("CA")
                                        .postalCode("94107")
                                        .countryCode("US")
                                        .build()).options(null).build())//航运地址,非必须,Omitting shipping will default to the customer's default shipping address.
                            .customId("CUSTOM-123")//The API caller-provided external ID. Used to reconcile API caller-initiated transactions with PayPal transactions. Appears in transaction and settlement reports.
                            .description("Purchase from Orders Quick Start")
                            .softDescriptor("800-123-1234")//The soft descriptor is the dynamic text used to construct the statement descriptor that appears on a payer's card statement.不太清楚有啥用
                            .build() )
                    ).appContext(
                            AppContext.Builder().brandName("公司品牌,非必须")
                                .userAction(UserAction.PAY_NOW)
                                .shippingPreference(shippingPreference)
                                .build()
                        ).build()){ id ->
                            System.out.println("生成的订单ID: $id")
                        }
            },
            onApprove = OnApprove { approval ->
                approval.orderActions.capture { result ->
                    val message = when (result) {
                        is CaptureOrderResult.Success -> {
                            "Order Capture Succeeded"
                        }
                        is CaptureOrderResult.Error -> {
                            "Order Capture Failed"
                        }
                    }
                    System.out.println(message)
                }
            },
            onCancel = OnCancel {
                System.out.println("Buyer Cancelled Checkout")
            },
            onError = OnError { errorInfo ->
                System.out.println("An Error Occurred")
            }
        )
      }else{
          System.out.println("payPal支付失败,系统版本过低")
      }
    }

    private fun BigDecimal.unitAmountFor(currencyCode: CurrencyCode): UnitAmount {
        return UnitAmount.Builder().value(asMoneyString).currencyCode(currencyCode).build()
    }

    private val BigDecimal.asMoneyString: String
        get() = DecimalFormat("#0.00").format(this)

    private val BigDecimal.scaledForMoney: BigDecimal
        get() = setScale(2, RoundingMode.HALF_UP)
  • 6.后来研究了哈Js支付,发现Js支付比Android支付银杏化多了,不会各种跳转,而且很快就能ok,墙裂建议使用js支付,集成支付文档地址:https://developer.paypal.com/docs/checkout/standard/integrate/,但是我用Vue的Npm模式依赖没有尝试成功,正常Js模式到是很快ok,代码就是demo代码,如下:
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Ensures optimal rendering on mobile devices -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <!-- Optimal Internet Explorer compatibility -->
  </head>

  <body>
    <!-- Include the PayPal JavaScript SDK; replace "test" with your own sandbox Business account app client ID -->
    <script src="https://www.paypal.com/sdk/js?client-id=test&currency=USD"></script>

    <!-- Set up a container element for the button -->
    <div id="paypal-button-container"></div>

    <script>
      paypal.Buttons({

        // Sets up the transaction when a payment button is clicked
        createOrder: function(data, actions) {
          return actions.order.create({
            purchase_units: [{
              amount: {
                value: '1.44' // Can reference variables or functions. Example: `value: document.getElementById('...').value`
              }
            }]
          });
        },

        // Finalize the transaction after payer approval
        onApprove: function(data, actions) {
          return actions.order.capture().then(function(orderData) {
            // Successful capture! For dev/demo purposes:解析这个orderData里面就有状态status,为COMPLETED即为成功
                console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
                var transaction = orderData.purchase_units[0].payments.captures[0];
                alert('Transaction '+ transaction.status + ': ' + transaction.id + '\n\nSee console for all available details');

            // When ready to go live, remove the alert and show a success message within this page. For example:
            // var element = document.getElementById('paypal-button-container');
            // element.innerHTML = '';
            // element.innerHTML = '<h3>Thank you for your payment!</h3>';
            // Or go to another URL:  actions.redirect('thank_you.html');
          });
        }
      }).render('#paypal-button-container');

    </script>
  </body>
</html>

支付回调结果附上一个:

{
  "id": "6TK99509CY0808713",
  "intent": "CAPTURE",
  "status": "COMPLETED",
  "purchase_units": [
    {
      "reference_id": "default",
      "amount": {
        "currency_code": "USD",
        "value": "1.44"
      },
      "payee": {
        "email_address": "sb-c0o5o9202448@personal.example.com",
        "merchant_id": "GMGDWWB7E42QQ"
      },
      "shipping": {
        "name": {
          "full_name": "Doe John"
        },
        "address": {
          "address_line_1": "NO 1 Nan Jin Road",
          "admin_area_2": "Shanghai",
          "admin_area_1": "Shanghai",
          "postal_code": "200000",
          "country_code": "C2"
        }
      },
      "payments": {
        "captures": [
          {
            "id": "4WN3533027557664E",
            "status": "COMPLETED",
            "amount": {
              "currency_code": "USD",
              "value": "1.44"
            },
            "final_capture": true,
            "seller_protection": {
              "status": "ELIGIBLE",
              "dispute_categories": [
                "ITEM_NOT_RECEIVED",
                "UNAUTHORIZED_TRANSACTION"
              ]
            },
            "create_time": "2022-01-17T08:50:46Z",
            "update_time": "2022-01-17T08:50:46Z"
          }
        ]
      }
    }
  ],
  "payer": {
    "name": {
      "given_name": "John",
      "surname": "Doe"
    },
    "email_address": "sb-3isgd9205973@business.example.com",
    "payer_id": "F5EZF49V6R3V6",
    "address": {
      "country_code": "C2"
    }
  },
  "create_time": "2022-01-17T08:49:52Z",
  "update_time": "2022-01-17T08:50:46Z",
  "links": [
    {
      "href": "https://api.sandbox.paypal.com/v2/checkout/orders/6TK99509CY0808713",
      "rel": "self",
      "method": "GET"
    }
  ]
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容