使用SDK实现ONE store In-App支付

概要

ONE store 支付程序库(Library)提供在Java 环境的最新功能。本指南对展现ONE store 支付程序库(Library)功能的方法进行说明。

设置Project

添加仓库(repository)及从属项目

在Project最上位gradle文件上注册 ONE store maven地址。

Android Studio (version: bumblebee)以上的情况,在 settings.gradle文件中添加。

repositories {
    ... 
    maven { url 'https://repo.onestore.net/repository/onestore-sdk-public' }
}

在App的build.gradle文件添加ONE store支付程序库从属项目。

dependencies {
    ...
    def iap_lastest_version = "21.xx.xx"
    implementation "com.onestorecorp.sdk:sdk-iap:$iap_lastest_version"
}

设置 <queries>

需要在 AndroidManifest.xml文件设置。详细内容请参考公告事项。

如果不设置<queries> tag, 就无法在SDK找到ONE store服务。

<manifest>
    <!-- 
        if your binary use ONE store's In-app SDK,
        Please make sure to declare the following query on Androidmanifest.xml. 
        Refer to the notice for more information.
        https://dev.onestore.net/devpoc/support/news/noticeView.omp?noticeId=32968
     -->
    <queries>
        <intent>
            <action android:name="com.onestore.ipc.iap.IapService.ACTION" />
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="onestore" />
        </intent>
    </queries>
    ...
    <application>
        ...
    </application>
</manifest>

发行全球store的测试选项设置

此是强制SDK和全球store联动的选项。本功能从SDK v21.01.00版本开始适用。

<manifest>
    ...
    <application>
        <activity>
            ...
        </activity>
        ...
        
        <!-- Options for in-app testing on your global store -->
        <meta-data android:name="onestore:dev_option" android:value="global" />
    </application>
</manifest>

发行组建版本中,本选项必须删除。

适用App内程序库(in-app Library)

设置Log level

研发阶段设置Log level,可以更仔细显示SDK的数据流向。以 android.util.Log内定义的值为基础运作。

/**
 * Set the log level.<br/>
 * {@link Log#VERBOSE}, {@link Log#DEBUG}, {@link Log#INFO},
 * {@link Log#WARN}, {@link Log#ERROR}
 * @param level int
 */
com.gaa.sdk.base.Logger.setLogLevel(2)

发行组建版本网络安全比较弱,本选项需要删除。

错误处理

ONE store 支付程序库以IapResult形式反馈错误。IapResult包含分类App内可能出现的支付相关错误的ResponseCode。例如,如果IapResult收到RESULT_NEED_LOGINRESULT_NEED_UPDATE等错误代码(code),应用软件需要做出相应的处理。

登录ONE store

GaaSignInClient 初始化

GaaSignInClient是为了登录ONE store的程序库。 通过getClient()生成instance。

val signInClient = GaaSignInClient.getClient(activity)

登录后台

通过slientSignIn()()调用后台登录。

用户已经登录ONE store账户时,之后开始尝试从后台token登录。作为成功或失败的结果值,以SignInResult对象获取回复。

signInClient.silentSignIn { signInResult ->
  
}

登录前台

不同于slientSignIn(),仅在UiThread 调用相应函数。

虽然原则上首先尝试后台登录,但是对失败的处理,在SDK将专门处理。

signInClient.launchSignInFlow(activity) { signInResult ->
  
}

PurchaseClient 初始化

PurchaseClient是ONE store支付程序库和App之间通信的基本接口。

为了避免生成单一活动相关的几个PurchasesUpdatedListener 回调的状况,建议打开 PurchaseClient连接。

想要生成PurchaseClient,需要使用newBuilder()。想要收到购买相关信息,需要调用setListener()传送对PurchasesUpdatedListener参考。这个Listener接收App的所有购买相关信息。setBase64PublicKey()在SDK负责购买数据伪造篡改相关的签字确认操作。虽然是选项值,但是建议使用。

可以在研发者中心确认许可证密钥。

许可证密钥,为了确保安全,更推荐使用服务器等接收传送使用,而不是将其保存为APP内的代码。

private val listener = PurchasesUpdatedListener { iapResult, purchases ->
       // To be implemented in a later section.
}

private var purchaseClient = PurchaseClient.newBuilder(activity)
   .setListener(listener)
   .setBase64PublicKey(/*your public key*/) // optional
   .build()

ONE store服务连接设置

生成PurchaseClient后,需要连接ONE store服务。.

需要调用startConnection()进行连接。连接程序非同步,完成客户端连接后,通过PurchaseClientStateListener收到回调。

下面例子是测试连接开始、使用准备与否的方法。

purchaseClient.startConnection(object : PurchaseClientStateListener {
    override fun onSetupFinished(iapResult: IapResult) {
        if (iapResult.isSuccess) {
            // The PurchaseClient is ready. You can query purchases here.
        }
    }

    override fun onServiceDisconnected() {
        // Try to restart the connection on the next request to
        // PurchaseClient by calling the startConnection() method.
    }
})

查询商品详细信息

调用queryProductDetailsAsync(),查询商品详情。是给用户标记产品前之重要阶段。

调用queryProductDetailsAsync()时,同时与setProductType()一起传送在ONE store研发者中心生成的指定内部app商品ID文字列目录的ProductDetailParams的instance。

ProductType如下

想一次性查询上述所有类型信息,请设置ProductType.ALL

ProductType.ALL仅可在查询商品详细信息中使用,在请求购买,查询购买明细中无法使用。

要处理非同步操作结果,需要实现ProductDetailsListener 接口。

val params = ProductDetailsParams.newBuilder()
        .setProductIdList(productIdList)
        .setProductType(ProductType.INAPP)
        .build()
purchaseClient.queryProductDetailsAsync(params) { iapResult, productDetails -> 
    // Process the result.
}

请求购买

为了在应用软件请求购买,要在基本线程调用launchPurchaseFlow() 函数。

这一函数以调用queryProductDetailsAsync() 函数获取的ProductDetail对象的值为基础,生成PurchaseFlowParams对象。要生成PurchaseFlowParams对象,需要使用PurchaseFlowParams.Builder class。

setDeveloperPayload()作为研发者随意输入的值,最多200byte。此值可以用于确认支付后信息的整合性和附加数据。 setProductName()在想要支付时变更显示商品名的情况下使用。setQuantity()仅用于管理型app内商品,在购买数个同一商品时使用。

ONEstore正在进行为用户提供优惠券、现金卡等诸多优惠宣传活动。

研发公司可以在请求购买时使用gameUserId, promotionApplicable参数,限制或允许app用户参与宣传活动。 研发公司选择传送app的固有用户识别号码及是否参与宣传活动,ONE store以此值为基础,适用用户的宣传优惠。

gameUserId, promotionApplicable参数作为选项值,只能在事先与ONE store事业部负责人协商宣传活动时使用,其他一般情况下不发送值。 另外,即便是已经达成事先协议并发送值,为了保护个人信息,gameUserId也需要以hash的固有值形式传送。

val activity: Activity = ...

val purchaseFlowParams = PurchaseFlowParams.newBuilder()
      .setProductId(productId)
      .setProductType(productType)
      .setDeveloperPayload(devPayload)    // optional
      .setQuantity(1)                     // optional
      .setProductName("")                 // optional
      .setGameUserId("")                  // optional
      .setPromotionApplicable(false)      // optional
      .build()

purchaseClient.launchPurchaseFlow(activity, purchaseFlowParams)

如果成功调用launchPurchaseFlow(),显示如下界面。【图片1】显示定期支付购买界面。

如果购买成功,向PurchasesUpdatedListener 接口的 onPurchasesUpdated() 函数传送购买操作结果。该Listener 在PurchaseClient 初始化时,使用setListener() 被指定。

override fun onPurchasesUpdated(iapResult: IapResult, purchases: List<PurchaseData>?) {
    if (iapResult.isSuccess && purchases != null) {
        for (purchase in purchases) {
            handlePurchase(purchase)
        }
    } else if (iapResult.responseCode == ResponseCode.NEED_UPDATE) {
        // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
    } else if (iapResult.responseCode == ReponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}

如果购买成功,购买数据还会生成用户及商品ID的固有识别符号购买token。购买token虽然可在应用软件内储存,但是最好将token传送到验证购买、避免诈骗的后端服务器。 管理型商品和定期支付商品的购买token在每次支付的时候都会发行购买token。

(包月型商品自动支付更新的时候,购买token将保持不变。) 另外,用户通过电子邮件收到包含收据号码的交易收据。管理型商品每次购买都会收到电子邮件,包月型商品和定期支付商品在第一次购买和之后有更新时会收到电子邮件。

定期支付

定期支付在取消之前一直自动更新。定期支付可能出现如下状态。

  • 激活:用户使用内容过程中没有问题的良好状态,可以访问定期支付。

  • 预约暂停:当用户使用定期支付中想要暂停时,选择此选项。

    • 周定期支付:以1-3周为单位进行暂停。

    • 月定期支付:以1-3个月为单位进行暂停。

    • 年定期支付:不予支持暂停。

  • 预约解除:虽然用户在使用定期支付,但是想取消时可以选择。下一个支付日将不会支付。

  • 延期、保留:如果用户出现支付问题,则无法在下一个支付日支付。无法取消预约,可以立刻“解除订阅”。

允许用户升级、降级或变更定期支付

如果用户想要升级或降级定期支付时,在购买时可设置比例分配模式,或者设定变更事项影响定期支付用户的方式。

下表列出了可使用的比例分配模式 (PurchaseFlowParams.ProrationMode)

升级或降级

定期支付可以使用与请求购买同一个API,向用户提供升级或降级。但是,为了适用定期支付的升级与降级,必须具备现有定期支付购买token和比例分配模式值。

如下例子,需要提供现有定期支付、今后(升级或降级)定期支付及比例分配模式的相关信息。

val subscriptionUpdateParams = SubscriptionUpdateParams.newBuilder()
      .setProrationMode(desiredProrationMode)
      .setOldPurchaseToken(oldPurchaseToken)
      .build()

val purchaseFlowParams = PurchaseFlowParams.newBuilder()
      .setProductId(newProductId)
      .setProductType(productType)
      .setProductName(productName)        // optional
      .setDeveloperPayload(devPayload)    // optional
      .setSubscriptionUpdateParams(subscriptionUdpateParams)
      .build()

purchaseClient.launchPurchaseFlow(activity, purchaseFlowParams)

因为升级或降级也会执行请求购买逻辑,所以在PurchasesUpdatedListener接收回复。另外也会在查询购买明细中得到响应。 用比例分配模式购买也与一般购买相同,需要使用PurchaseClient.acknowledgeAsync()确认购买。

购买处理

购买完成,需要在应用软件进行购买确认处理。大部分情况下,应用软件通过PurchasesUpdatedListener接收购买通知。或者如查询购买明细说明那样,也有时应用软件调用PurchaseClient.queryPurchasesAsync()函数处理。

可以使用如下方法中的一个确认购买。

  • 消耗性产品使用PurchaseClient.consumeAsync()

  • 非消耗性产品使用PurchaseClient.acknowledgeAsync()

消费管理型商品(consume)

管理型商品在消费之前不能再次购买。

为了消费商品,需要调用consumeAsync()。另外,要想接收消费操作结果,需要展现ConsumeListener 接口。

不消费管理型商品时,可以作为永久性形态的商品类型使用,购买后立即消费时,也可以作为消费性形态的商品使用。 另外,如果特定期间以后消费,可以作为固定期限形态的商品使用。

如果3天内不确认购买(acknowledge)或不消费(consume),判断为没有向用户提供商品,自动退款给用户。

fun handlePurchase(purchase: PurchaseData) {
    // Purchase retrieved from PurchaseClient#queryPurchasesAsync
    // or your PurchasesUpdatedListener.
    val purchase: PurchaseData = ...
      
    // Verify the purchase.
    // Ensure entitlement was not already granted for this purchaseToken.
    // Grant entitlement to the user.

    val consumeParams = ConsumeParams.newBuilder()
                            .setPurchaseData(purchase)
                            .build()
                            
    purchaseClient.consumeAsync(consumeParams) { iapResult, purchaseData -> 
        // Process the result.
    }
}

消费请求有时会失败,因此必须检查安全后端服务器,确认各购买token是否使用。只有这样应用软件才不会对同一个购买进行多次授权。或者,在授权之前,可以等到成功消费回复为止 。

确认购买(acknowledge)

可以使用PurchaseClient.acknowledgeAsync() 函数确认处理非消费型商品的购买。管理型商品、包月型商品、订阅型商品均可使用。

使用PurchaseData.isAcknowledged() 函数判断是否已确认购买。另外,如果想要接收对购买确认的操作结果,需要展现AcknowledgeListener接口。

// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
fun handlePurchase(PurchaseData purchase) {
    if (purchase.purchaseState == PurchaseState.PURCHASED) {
        if (!purchase.isAcknowledged) {
            val acknowledgeParams = AcknowledgeParams.newBuilder()
                                        .setPurchaseData(purchase)
                                        .build()
                                        
            purchaseClient.acknowledgeAsync(acknowledgeParams) { iapResult, purchaseData ->
                 // PurchaseClient by calling the queryPurchasesAsync() method.
            }
        }
    }
}

AcknowledgeListener.onAcknowledgeResponse() 函数传送的PurchaseData是请求时的数据,因此acknowledgeState值不变。需要通过查询购买明细更换成变更的数据。

查询购买明细

仅使用PurchasesUpdatedListener处理购买,不足以保证应用软件已经处理所有购买。应用软件可能错过购买跟踪或无法识别购买等几种情况如下。

在如下几种情况,可能发生应用软件没接收购买回复或无法识别购买。

  • 网络问题:用户成功购买,ONE store上也接收确认,

    但是设备在通过PurchasesUpdatedListener接收购买通知前,发生网络连接断开。

  • 多台设备:用户在一台设备上购买商品后,切换到其他设备。

为了应对这种情况,需要在应用软件的onCreate()onResume()中调用PurchaseClient.queryPurchasesAsync()方法来确认购买是否已经成功处理。

回调处理与PurchasesUpdatedListener相同。

val queryPurchasesListener = QueryPurchasesListener { iapResult, purchases -> 
    if (iapResult.isSuccess && purchases != null) {
        for (purchase in purchases) {
            handlePurchase(purchase)
        }
    } else if (iapResult.responseCode == ResponseCode.NEED_UPDATE) {
        // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
    } else if (iapResult.responseCode == ReponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}
purchaseClient.queryPurchasesAsync(ProductType.INAPP, queryPurchasesListener)

更改包月型商品状态 (Deprecated)

包月型商品是最初购买后下个月同一天更新的商品。包月型商品的状态可以通过PurchaseData.getRecurringState()来确认。

使用PurchaseClient.manageRecurringProductAsync()更改包月型商品的状态。在RecurringProductParams 对象输入购买数据和要变更的PurchaseClient.RecurringAction值。

从SDK V21 (API V7)开始,无法开发新的包月型商品。请使用支付周期为一个月的订阅型商品。

// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
fun manageRecurring(purchase: PurchaseData) {
    val recurringParams = RecurringProductParams.newBuilder()
            .setPurchaseData(purchase)
            .setRecurringAction(RecurringAction.CANCEL | RecurringAction.REACTIVATE)
            .build()

    purchaseClient.manageRecurringProductAsync(recurringParams) { iapResult, purchaseData, action ->
        // PurchaseClient by calling the queryPurchasesAsync() method.
    }
}

RecurringProductListener.onRecurringResponse() 函数传送的PurchaseData是请求时的数据,所以recurringState值不变。需要通过查询购买明细更换更改的数据。

打开定期支付管理界面

可以显示管理定期支付中商品的界面。

作为参数输入包含PurchaseDataSubscriptionsParams,确认购买数据,可以运行相应定期支付商品的管理界面。 但是SubscriptionParams加入null时,运行用户的定期支付列表界面。

如下是显示定期支付管理界面的方法的示例。

fun launchManageSubscription(@Nullable purchaseData: PurchaseData) {
    val subscriptionParams = when (purchaseData != null) {
        true -> SubscriptionParams.newBuilder()
                    .setPurchaseData(purchaseData)
                    .build()
        else -> null
    }
    purchaseClient.launchManageSubscription(mActivity, subscriptionParams)
}

获取市场分类代码(code)

从SDK v19以上版本开始,为了使用Server API需要市场分类代码。 可以通过getStoreInfoAsync()获取市场分类代码。

purchaseClient.getStoreInfoAsync { iapResult, storeCode ->
    // Save storecode and use it in Server to Server API.
}

安装ONE store服务

ONE store服务的版本低或者没有时,无法使用app内支付。 通过PurchaseClient.startConnection()连接时,可以在IapResult.getResponseCode()确认。如果发生RESULT_NEED_UPDATE,需要调用launchUpdateOrInstallFlow()方法。

val activity: Activity = ...

purchaseClient.launchUpdateOrInstallFlow(activity) { iapResult ->
    if (iapResult.isSuccess) {
        // If the installation is completed successfully,
        // you should try to reconnect with the ONE store service. 
        // PurchaseClient by calling the startConnection() method.
    }
}

Last updated