원본출처: 티스토리 바로가기
아직 잘 모르던 시절에 작성했던 인앱 결제에 대한 첫번째글을 상기해 보면서, 다시금 인앱 결제에 대한 이야기를 적어 본다.
https://billcorea.tistory.com/27
그 시절에는 잘 몰랐는데, 인앱 결제의 1회성 결제는 위 글에서 확인해 볼 수 있다. 이번에는 정기결제에 대한 구현 이야기를 적어 볼까 한다.
gradle 설정이 쪼금 변했다. 버전에 변해서.
dependencies { ... implementation 'com.android.billingclient:billing:4.1.0' implementation 'com.google.code.gson:gson:2.8.8' ... }
소스 구현은 다음과 같이 class을 하나 만들어 두었다.
import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.billingclient.api.AcknowledgePurchaseParams; import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingFlowParams; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.PurchaseHistoryResponseListener; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.PurchasesUpdatedListener; import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsParams; import com.android.billingclient.api.SkuDetailsResponseListener; import com.billcoreatech.daycnt415.R; import com.billcoreatech.daycnt415.util.KakaoToast; import com.billcoreatech.daycnt415.util.StringUtil; import org.json.JSONException; import org.json.JSONObject; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; public class BillingManager implements PurchasesUpdatedListener, ConsumeResponseListener { String TAG = "BillingManager" ; BillingClient mBillingClient ; Activity mActivity ; public List<SkuDetails> mSkuDetails ; public enum connectStatusTypes { waiting, connected, fail, disconnected } public connectStatusTypes connectStatus = connectStatusTypes.waiting ; private ConsumeResponseListener mConsumResListnere ; String punchName = "210414_monthly_bill_999"; String payType = BillingClient.SkuType.SUBS ; SharedPreferences option ; SharedPreferences.Editor editor ; public BillingManager (Activity _activity) { mActivity = _activity ; option = mActivity.getSharedPreferences("option", mActivity.MODE_PRIVATE); editor = option.edit(); mBillingClient = BillingClient.newBuilder(mActivity) .setListener(this) .enablePendingPurchases() .build() ; mBillingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(@NonNull BillingResult billingResult) { Log.e(TAG, "respCode=" + billingResult.getResponseCode() ) ; if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { connectStatus = connectStatusTypes.connected ; Log.e(TAG, "connected...") ; purchaseAsync(); getSkuDetailList(); } else { connectStatus = connectStatusTypes.fail ; Log.i(TAG, "connected... fail ") ; } } @Override public void onBillingServiceDisconnected() { connectStatus = connectStatusTypes.disconnected ; Log.i(TAG, "disconnected ") ; } }); } /** * 정기 결재 소모 여부를 수신 : 21.04.20 1회성 구매의 경우는 결재하면 끝임. * @param billingResult * @param purchaseToken */ @Override public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String purchaseToken) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { Log.i(TAG, "사용끝 + " + purchaseToken) ; return ; } else { Log.i(TAG, "소모에 실패 " + billingResult.getResponseCode() + " 대상 상품 " + purchaseToken) ; return ; } } public int purchase(SkuDetails skuDetails) { BillingFlowParams flowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) .build(); return mBillingClient.launchBillingFlow(mActivity, flowParams).getResponseCode(); } public void purchaseAsync() { Log.e(TAG, "--------------------------------------------------------------"); mBillingClient.queryPurchasesAsync(payType, new PurchasesResponseListener() { @Override public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) { Log.e(TAG, "onQueryPurchasesResponse=" + billingResult.getResponseCode()) ; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); if (list.size() < 1) { editor = option.edit(); editor.putBoolean("isBill", false); editor.commit(); } else { for (Purchase purchase : list) { Log.e(TAG, "getPurchaseToken=" + purchase.getPurchaseToken()); for (String str : purchase.getSkus()) { Log.e(TAG, "getSkus=" + str); } Date now = new Date(); now.setTime(purchase.getPurchaseTime()); Log.e(TAG, "getPurchaseTime=" + sdf.format(now)); Log.e(TAG, "getQuantity=" + purchase.getQuantity()); Log.e(TAG, "getSignature=" + purchase.getSignature()); Log.e(TAG, "isAutoRenewing=" + purchase.isAutoRenewing()); editor = option.edit(); editor.putBoolean("isBill", purchase.isAutoRenewing()); editor.commit(); } } } }); mBillingClient.queryPurchaseHistoryAsync(payType, new PurchaseHistoryResponseListener() { @Override public void onPurchaseHistoryResponse(@NonNull BillingResult billingResult, @Nullable List<PurchaseHistoryRecord> list) { if (billingResult.getResponseCode() == 0) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); for(PurchaseHistoryRecord purchase : list) { Log.e(TAG, "getPurchaseToken=" + purchase.getPurchaseToken()); for (String str : purchase.getSkus()) { Log.e(TAG, "getSkus=" + str); } Date now = new Date(); now.setTime(purchase.getPurchaseTime()); Log.e(TAG, "getPurchaseTime=" + sdf.format(now)); Log.e(TAG, "getQuantity=" + purchase.getQuantity()); Log.e(TAG, "getSignature=" + purchase.getSignature()); if (payType.equals(BillingClient.SkuType.INAPP)) { ConsumeParams params = ConsumeParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(); mBillingClient.consumeAsync(params, BillingManager.this); } } } } }); } public void getSkuDetailList() { List<String> skuIdList = new ArrayList<>() ; skuIdList.add(punchName); SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); params.setSkusList(skuIdList).setType(payType); mBillingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() { @Override public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> skuDetailsList) { if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { Log.i(TAG, "detail respCode=" + billingResult.getResponseCode()) ; return ; } if (skuDetailsList == null) { KakaoToast.makeToast(mActivity, mActivity.getString(R.string.msgNotInfo), Toast.LENGTH_LONG).show(); return ; } Log.i(TAG, "listCount=" + skuDetailsList.size()); for(SkuDetails skuDetails : skuDetailsList) { Log.i(TAG, "\n" + skuDetails.getSku() + "\n" + skuDetails.getTitle() + "\n" + skuDetails.getPrice() + "\n" + skuDetails.getDescription() + "\n" + skuDetails.getFreeTrialPeriod() + "\n" + skuDetails.getIconUrl() + "\n" + skuDetails.getIntroductoryPrice() + "\n" + skuDetails.getIntroductoryPriceAmountMicros() + "\n" + skuDetails.getOriginalPrice() + "\n" + skuDetails.getPriceCurrencyCode()) ; } mSkuDetails = skuDetailsList ; } }); } /** * @param billingResult * @param purchases */ @Override public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) { Log.i(TAG, "구매 성공>>>" + billingResult.getDebugMessage()); JSONObject object = null ; String pID = "" ; String pDate = "" ; for(Purchase purchase : purchases) { //handlePurchase(purchase); Log.i(TAG, "성공값=" + purchase.getPurchaseToken()) ; try { Log.e(TAG, "getOriginalJson=" + purchase.getOriginalJson()); object = new JSONObject(purchase.getOriginalJson()); String sku = ""; for (String str : purchase.getSkus()) { sku = str ; Log.e(TAG, "SKU=" + sku); } pID = object.getString("purchaseToken"); pDate = StringUtil.getDate(object.getLong("purchaseTime")); if (sku.equals(punchName)) { editor.putLong("billTimeStamp", object.getLong("purchaseTime")); editor.putBoolean("isBill", object.getBoolean("autoRenewing")); editor.putString("token", purchase.getPurchaseToken()); } editor.commit(); } catch (JSONException e) { e.printStackTrace(); } } } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) { Log.i(TAG, "결제 취소"); editor = option.edit(); editor.putBoolean("isBill", false); editor.commit(); } else { Log.i(TAG, "오류 코드=" + billingResult.getResponseCode()) ; editor = option.edit(); editor.putBoolean("isBill", false); editor.commit(); } } void handlePurchase(Purchase purchase) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { if (!purchase.isAcknowledged()) { AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(); mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() { @Override public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) { Log.e(TAG, "getResponseCode=" + billingResult.getResponseCode()); } }); } } } }
정기결제를 실행하는 화면은 다음과 왼쪽 그림과 같이 처리가 된다.
이렇게 정기결제된 경우에는 다른 처리를 할 필요는 없다.
다만, 정기결제가 유지 되고 있는 지를 확인하는 처리가 필요했다.
그래서 아래 코드와 같이 queryPurchaseAsync 을 호출해서
구매된 내역을 확인하여 그 값중에
isAutoRenewing() 의 값이 true 가 오는 지를 보고 값이 true 인 경우는 정기결제가 유지 되고 있음을 확인할 수 있었다.
list 값이 오지 않거나, false 가 오면 정기구매가 되지 않고 있다고 보고 필요한 처리를 하면 된다.
mBillingClient.queryPurchasesAsync(payType, new PurchasesResponseListener() { @Override public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) { Log.e(TAG, "onQueryPurchasesResponse=" + billingResult.getResponseCode()) ; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); if (list.size() < 1) { editor = option.edit(); editor.putBoolean("isBill", false); editor.commit(); } else { for (Purchase purchase : list) { Log.e(TAG, "getPurchaseToken=" + purchase.getPurchaseToken()); for (String str : purchase.getSkus()) { Log.e(TAG, "getSkus=" + str); } Date now = new Date(); now.setTime(purchase.getPurchaseTime()); Log.e(TAG, "getPurchaseTime=" + sdf.format(now)); Log.e(TAG, "getQuantity=" + purchase.getQuantity()); Log.e(TAG, "getSignature=" + purchase.getSignature()); Log.e(TAG, "isAutoRenewing=" + purchase.isAutoRenewing()); editor = option.edit(); editor.putBoolean("isBill", purchase.isAutoRenewing()); editor.commit(); } } } });
테스트 구매의 경우는 5분마다 한번씩 갱신 되므로 5분 뒤에 다시 확인하는 처리에 대한 검증을 해 볼 수 있다.
기타 추가적인 작업을 해야할 것들이 남아 있는 것 같다. 그것들은 아래 링크의 내용을 참고해 보도록 해야겠다.
https://developer.android.com/google/play/billing/subscriptions?hl=ko
이것으로 인앱결제 정기결제에 대한 이해를 해 보았다.
댓글
댓글 쓰기