[Android] 인앱 결제 시스템 만들기 (PBL v6, 비소비성 결제)
https://developer.android.com/google/play/billing/integrate?hl=ko
앱에 Google Play 결제 라이브러리 통합 | Google Play 결제 시스템 | Android Developers
알림 1. 2023년 8월 2일부터 모든 신규 앱은 결제 라이브러리 버전 5 이상을 사용해야 합니다. 2023년 11월 1일부터는 기존 앱의 모든 업데이트에도 결제 라이브러리 버전 5 이상이 요구됩니다. 자세
developer.android.com
https://developer.android.com/google/play/billing/migrate?hl=ko
AIDL에서 Google Play 결제 라이브러리로 이전 가이드 | Google Play 결제 시스템 | Android Developers
알림 1. 2023년 8월 2일부터 모든 신규 앱은 결제 라이브러리 버전 5 이상을 사용해야 합니다. 2023년 11월 1일부터는 기존 앱의 모든 업데이트에도 결제 라이브러리 버전 5 이상이 요구됩니다. 자세
developer.android.com
https://blog.naver.com/saka/223063090061
V5 안드로이드 인앱 결제 최신 구현 방법 간단 정리, 초간단 예제, Android In-app Billing, 결제 라이브
안드이드 인 앱 결제 v5 에 관한 설명입니다. 기본적인 흐름은 제가 이전에 작성했던 v3 과 크게 다르지는 ...
blog.naver.com
위 포스트들을 참조했습니다.
오래된 앱에 TargetSDK를 33으로 올리라는 메세지가 구글에서 왔다.
OS 13을 지원하여 작동되도록 만들어놓고보니 인앱 결제 시스템이 있었고 확인해보니 되게 오래된 IInAppBillingService.aidl 라이브러리를 사용하고 있었다. (PBL v3 보다 오래됨)
물론 당연히 오래된 인앱결제 시스템은 구글이 더 이상 서비스를 하지 않아 작동하지 않는다
23년 10월 기준 인앱 결제 시스템은 PBL v5 또는 v6를 권장하고 있다.
따라서 결제 시스템을 PBL v6으로 다시 만들기로 했다.
v5와 v6의 차이는 구독 시스템인 정기 결제에 대해서만 변경점이 있다고 하니 한 번만 구매해도 되는 또는 일반 구매의 경우는 차이가 없다고 한다.
필자가 수정한 앱은 한 번만 구매해도 되는 비소비성 상품이다.
먼저 build.gradle 파일에 인앱 결제 시스템의 종속 항목을 추가한다.
dependencies {
implementation 'com.android.billingclient:billing:6.0.1'
}
그 다음 BillingClient 초기화, Google Play에 연결해야하는데 이 부분은 구매를 진행하는 스크립트에서 작성한다.
필자의 앱은 MainActivity에서 구매 버튼을 누르면 구매를 진행하는 스크립트로 이동되는데 구매 버튼에 Play Console에서 지정한 인앱 상품의 제품 ID가 연결되어 있었다.
app.startPay(MainActivity.this, code);
app이라는 변수는 구매를 진행하는 스크립트다, 액티비티가 아니고 구매를 진행한 후 Main으로 돌아와 구매한 제품의 언락을 진행해야하므로 Activity와 제품 ID를 인자로 넘겼다.
startPay 함수를 실행하는 스크립트의 onCreate에서 BillingClient 초기화를 진행한다.
// 전역 변수로 선언
private BillingClient mBillingClient;
// onCreate 또는 init 함수에서 초기화
mBillingClient = BillingClient.newBuilder(this)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases().build();
purchasesUpdatedListener 리스너는 나중에 결제 확인으로 사용할 것이다.
startPay 함수를 보자.
public void startPay(Activity act, String product){
mActivity = act;
actBuyCode = product;
productList
= ImmutableList.of(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(actBuyCode)
.setProductType(BillingClient.ProductType.INAPP)
.build()
);
queryProductDetailsParams =
QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
if(mBillingClient.getConnectionState() == BillingClient.ConnectionState.DISCONNECTED ||
mBillingClient.getConnectionState() == BillingClient.ConnectionState.CLOSED) {
mBillingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
mBillingClient.queryProductDetailsAsync(queryProductDetailsParams, new ProductDetailsResponseListener() {
@Override
public void onProductDetailsResponse(@NonNull @NotNull BillingResult billingResult, @NonNull @NotNull List<ProductDetails> list) {
// check billing result, process returned productdetailslist
for (ProductDetails productDetails : list) {
if (actBuyCode.equals(productDetails.getProductId())) {
productCode = productDetails;
startBillingFlow(productCode);
}
}
}
});
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
});
}
else{
startBillingFlow(productCode);
}
}
productList 와 queryProductDetailsParams 은 전역변수로 지정했다.
ImmutableList<QueryProductDetailsParams.Product> productList;
QueryProductDetailsParams queryProductDetailsParams;
mActivity = act; 로 구매를 진행하는 스크립트에서 전역 액티비티 변수에 넣고
actBuyCode = product; 로 구매할 제품 ID를 설정한다.
productList 와 queryProductDetailsParams 은 포스트 상단에 링크한 개발자 문서에서 자세히 설명하고 있다.
이후 mBillingClient.startConnection 로 Google Play에 연결
onBillingSetupFinished에서 if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) 가 발동되면 연결된 것으로 판단한다.
mBillingClient.queryProductDetailsAsync 로 비동기적으로 결제를 요청하는데 구매하려는 제품 ID로 요청한다.
if (actBuyCode.equals(productDetails.getProductId())) {
productCode = productDetails;
startBillingFlow(productCode);
}
Google Play에서 받아온 인앱 결제 리스트에서 구매하려는 제품 ID가 있는지 확인하고 startBillingFlow로 결제를 요청
void startBillingFlow(ProductDetails productDetails){
ImmutableList<BillingFlowParams.ProductDetailsParams> productDetailsParamsList = ImmutableList.of(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails).build()
);
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList).build();
BillingResult billingResult = mBillingClient.launchBillingFlow(mActivity, billingFlowParams);
}
startBillingFlow 마지막에 있는 launchBillingFlow 로 결제창이 나타난다.
여기서 구매를 눌러 구매에 성공하면 purchasesUpdatedListener 리스너가 자동으로 실행된다.
final PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(BillingResult billingResult, List<com.android.billingclient.api.Purchase> purchases) {
// To be implemented in a later section.
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchases != null) {
for (com.android.billingclient.api.Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
actBuyCode = null;
mBillingClient.endConnection();
mBillingClient = BillingClient.newBuilder(getApplicationContext())
.setListener(this)
.enablePendingPurchases().build();
} else {
// Handle any other error codes.
actBuyCode = null;
mBillingClient.endConnection();
mBillingClient = BillingClient.newBuilder(getApplicationContext())
.setListener(this)
.enablePendingPurchases().build();
}
}
};
구매 성공(BillingResponseCode.OK)을 제외한 나머지 BillingResponseCode 에서
actBuyCode = null;
mBillingClient.endConnection();
mBillingClient = BillingClient.newBuilder(getApplicationContext())
.setListener(this)
.enablePendingPurchases().build();
를 한 이유는 mBillingClient 의 연결을 끊고 다시 연결하도록 해야 구매 단계에서 취소 후 다시 구매하려고 할 때 결재창이 나와서 이렇게 처리했다.
BillingResponseCode.OK를 받으면 handlePurchase(purchase) 함수가 실행된다.
소비성 제품을 구매할 경우와 비 소비성 제품을 구매할 경우 둘 다 작성했고 필자는 비 소비성 제품을 사용하므로 해당 구문만 사용했다.
void handlePurchase(com.android.billingclient.api.Purchase purchase) {
// Purchase retrieved from BillingClient#queryPurchasesAsync or your PurchasesUpdatedListener.
// Purchase purchase = ...;
// Verify the purchase.
// Ensure entitlement was not already granted for this purchaseToken.
// Grant entitlement to the user.
// 소비성 제품 구매
/* ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// Handle the success of the consume operation.
// For example, increase the number of coins inside the user's basket.
}
}
};
mBillingClient.consumeAsync(consumeParams, listener);*/
// 비 소비성 제품 구매
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// Handle the success of the consume operation.
// For example, increase the number of coins inside the user's basket.
// 구매 성공시 처리할 함수 넣기
((MainActivity)mActivity).delayProcessPurchase(actBuyCode);
}
}
};
if (purchase.getPurchaseState() == com.android.billingclient.api.Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
}
}
}
이 함수에서 mActivity를 사용하여 메인 액티비티의 delayProcessPurchase 함수를 실행시켰다.
메인 액티비티에서 구매 성공한 제품을 언락하는 기능이 있었기 때문이다.
만약 제품 언락 기능이 다른 곳에 있다면 해당 함수를 부르자.
이제 구매 내역 복원 기능을 위한 구매 내역을 가져오는 기능을 만들자.
결제 시스템을 작성한 스크립트에 작성하자
public void refresh(Activity act){
ArrayList<String> purchasedBuyId = new ArrayList<>();
purchasedBuyAll = false;
if(mBillingClient.getConnectionState() == BillingClient.ConnectionState.DISCONNECTED ||
mBillingClient.getConnectionState() == BillingClient.ConnectionState.CLOSED) {
mBillingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
mBillingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build(),
new PurchasesResponseListener() {
public void onQueryPurchasesResponse(BillingResult billingResult, List<com.android.billingclient.api.Purchase> purchases) {
// check billingResult
// process returned purchase list, e.g. display the plans user owns
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
for(int i = 0; i < purchases.size(); i++)
{
//((MainActivity)mActivity).delayProcessPurchase(purchases.get(i).toString());
purchasedBuyId.add(purchases.get(i).getProducts().get(0));
}
for(int j = 0; j < purchasedBuyId.size(); j++)
{
if(purchasedBuyId.get(j).equals("모든 제품 복원 ID"))
{
purchasedBuyAll = true;
break;
}
}
if(purchasedBuyAll)
{
// 만약 모든 제품을 언락하는 제품 ID를 보유했다면
((MainActivity)act).delayProcessPurchase("모든 제품 복원 ID", true);
}
else
{
// 아니면 각 제품 ID들을 받아서 넘김
for(int j = 0; j < purchasedBuyId.size(); j++)
{
((MainActivity)act).delayProcessPurchase(purchasedBuyId.get(j), true);
}
}
}
}
}
);
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
});
}
}
필자는 MainActivity 화면이 시작될 때와 구매 내역 복원 버튼을 눌렀을 때 구매 내역을 가져와 제품을 언락하고 싶었다.
따라서 Google Play에 연결하지 않은 상태에서 진행하므로 mBillingClient를 Google Play에 연결시키고 사용자가 현재 가지고 있는(환불한 내역은 안 가져옴)구매 내역을 가져오도록 했다.
메인 액티비티의 delayProcessPurchase 함수를 오버라이딩 하여 true 값이 넘어오면 구매 내역 복원하는 것이다라는 플래그를 새웠다.
메인 화면이 시작될 때에 구매 내역 복원은 onResume 함수를 이용했더니 가능했다.
@Override
protected void onResume() {
super.onResume();
Log.w("MainActivity", "onResume: ");
app.refresh(MainActivity.this);
}
app은 구매를 진행하는 스크립트다.
MainActivity에 진입할 때 발동되나 mBillingClient를 Google Play에 연결되어 있으면 구매내역 가져오기가 호출되지 않는다