使用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.co.kr/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.co.kr/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)
常數

VERBOSE

2

DEBUG

3

INFO (default)

4

WARN

5

ERROR

6

發行版本網絡安全比較弱,本選項需要刪除。

錯誤處理

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如下

ProductEnum

管理型商品

ProductType.INAPP

定期支付商品 (訂閱商品)

ProductType.SUBS

包月型商品

ProductType.AUTO (该商品今后本商品将不予支持)

想一次性查詢上述所有類型資訊,請設置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)

比例分配模式說明

IMMEDIATE_WITH_TIME_PRORATION

定期支付的變更將立即進行,剩餘時間基於差價條整後退款或支付。(這是基本操作)

IMMEDIATE_AND_CHARGE_PRORATED_PRICE

定期支付的變更將立即進行,請求支付周期和之前一致。請求用戶支付剩余時間的價格。(本選項僅在更新中可使用)

IMMEDIATE_WITHOUT_PRORATION

定期支付的變更將立即進行,下一個支付日支付新的價格。請求支付周期和之前一致。

DEFERRED

現有套餐到期後立即變更,同時發给用戶新的資費。

升级或降级

定期支付可以使用與請求購買同一个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