Google Play InApp v5 구글플레이 인앱 결제 버전5
22년 11월 1일부터, 구글플레이 모든 앱의 인앱결제 라이브러리 버전3을 사용이 완전 종료된다. 따라서, v4나 v5로 마이그레이션을 필수적으로 해야 하는데,
망할 구글 샘플 소스 https://developer.android.com/google/play/billing/integrate?hl=ko#java 는 ImmutableList 라는 클래스 덕택에 컴파일 되지 않는다.
삽질끝에 ArrayList로 변경하여 구현하는데 성공
1. build.graddle(:app)에 라이브러리 추가
def billingVersion = "5.0.0"
implementation "com.android.billingclient:billing:$billingVersion"
2. graddle 변경후에는 반드시 "Sync Project with Graddle Files"를 해준다.
3. 빌링 모듈 클래스를 만들자. PurchaseUpdateListener 와 ConsumeResponseListener 를 구현해야 한다.
public class BillingModule implements PurchasesUpdatedListener, ConsumeResponseListener {
4. 이 모듈에서 발생하는 이벤트를 받을 콜백 인터페이스를 선언해주자
public interface BillingModuleCallback {
void onBillingModulesIsReady(List<ProductDetails> detailsList);
void onSuccess(Purchase purchase);
void onFailure(int errorCode);
void onInit(int result);
}
5. 생성자 선언. 이 모듈의 엄마(?)가 될 액티비티를 전달해주어야 한다.
BillingClient billingClient = null;
Activity callActivity = null;
BillingModuleCallback callBack = null;
public BillingModule(Activity activity, BillingModuleCallback callback)
{
billingClient = BillingClient.newBuilder(activity)
.setListener(this)
.enablePendingPurchases()
.build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
callBack.onInit(0);
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
});
callActivity = activity;
callBack = callback;
}
public void dispose()
{
billingClient.endConnection();
}
빌링 셋업이 성공적으로 완료되면 엄마의 onInit();를 호출하게 만들어줬다. 부모는 onInit() 를 받으면, SKU 리스트를 달라고 요청한다.
...
BillingModule bm = null;
...
@Override
public void onCreate (Bundle savedInstanceState)
{
...
bm = new BillingModule(this, this);
...
}
final String SKU_STAR10 = "star10";
final String SKU_STAR20 = "star20";
final String SKU_STAR100 = "star100";
@Override
public void onInit(int result) {
// TODO Auto-generated method stub
if(result == BillingClient.BillingResponseCode.OK){
ArrayList<String> listSku = new ArrayList <String> ();
listSku.add(SKU_STAR10);
listSku.add(SKU_STAR20);
listSku.add(SKU_STAR100);
bm.asyncGetReadyToBuy(listSku);
} else {
Log.d("iap5", "onInit error : " + result);
}
}
MainActivity의 onInit 구현
6. getReadyToBuy 의 구현
public void asyncGetReadyToBuy(ArrayList<String> listSku)
{
ArrayList <QueryProductDetailsParams.Product> listProduct = new ArrayList<>();
for(String sku : listSku) {
Log.d("iap5", "asyncGetReadyToBuy sku : " + sku);
listProduct.add(QueryProductDetailsParams.Product.newBuilder().setProductId(sku).setProductType(BillingClient.ProductType.INAPP).build());
}
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(listProduct)
.build();
billingClient.queryProductDetailsAsync(
params,
new ProductDetailsResponseListener() {
public void onProductDetailsResponse(BillingResult billingResult, List<ProductDetails> productDetailsList) {
// Process the result
Log.d("iap5", "onProductDetailsResponse : " + productDetailsList.size());
callBack.onBillingModulesIsReady(productDetailsList);
}
}
);
}
지정된 SKU들의 Detail을 달라고 요청하고, 요청이 도착하면 엄마를 콜백한다.
ArrayMap<String, ProductDetails> skus = new ArrayMap<String, ProductDetails> ();
@Override
public void onBillingModulesIsReady(List<ProductDetails> listSku) {
// TODO Auto-generated method stub
if(listSku == null) {
Log.d("iap5", "onBillingModulesIsReady sku is null");
} else {
for(ProductDetails sku : listSku){
Log.d("iap5", "onBillingModulesIsReady sku : " + sku.toString());
skus.put(sku.getProductId(), sku);
}
}
}
MainActivity의 onBillingModilesIsReady 구현 : 준비가된 SKU들을 어레이맵에 담아둔다.
7. 여기까지 결제준비는 완료되었다! 이제 사용자가 구매를 눌렀을때 처리를 시작해보자. 사용자가 어떤 아이템 구매버튼을 누르면, 이 아이템의 ProductDetail (예전 버전에서는 SKU Detail)을 알아내서 구글플레이 서버로 전송해야 한다.
public boolean buyStar(String productId)
{
if(skus.isEmpty()){
showToast("Not available (NO SKUS)");
return false;
}
ProductDetails sku = skus.get(productId);
if(sku == null){
showToast(String.format("Not available (NO SKUS : %s)", productId));
return false;
}
bm.asyncLaunchFlow(sku);
return true;
}
8. 빌링 플로우 시작
public int asyncLaunchFlow(ProductDetails productDetails){
ArrayList<BillingFlowParams.ProductDetailsParams> productDetailsParamsList = new ArrayList<>();
productDetailsParamsList.add(BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()
);
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.build();
Log.d("iap5", "asyncLaunchFlow : " + productDetails.getProductId());
return billingClient.launchBillingFlow(callActivity, billingFlowParams).getResponseCode();
}
9. 구매를 성공(혹은 실패)하면 onPurchaseUpdated 가 호출된다.
@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
// TODO Auto-generated method stub
Log.d("iap5", "onPurchasesUpdated : " + billingResult.getResponseCode());
if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}
}
구매성공하면, 구매가 아니라 구매들(!)이 리스트로 넘어온다. 이제 이것들을 잘 처리하면 된다.
10. 구매 물건에 대한 처리 = Consume
private Purchase curPurchase = null;
void handlePurchase(Purchase purchase) {
curPurchase = purchase;
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
Log.d("iap5", "handlePurchase : " + purchase.getProducts().get(0));
billingClient.consumeAsync(consumeParams, this);
}
11. Consume 에 대한 처리 : 최종적으로 부모의 onSuccess()가 호출된다.
@Override
public void onConsumeResponse(BillingResult billingResult, String token) {
// TODO Auto-generated method stub
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
// Handle the success of the consume operation.
Log.d("iap5", "onConsumeResponse : " + billingResult.getResponseCode());
callBack.onSuccess(curPurchase);
}
}
12. 결제 프로세스가 중단되는경우 (결제는 완료했는데, Consume 이 되지 않은 경우) 에는 다시 확인하고 처리해야한다.
public void resume(String ptype){
Log.d("iap5", "resume : " + ptype);
if (billingClient.isReady()) {
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(ptype).build(), new PurchasesResponseListener() {
public void onQueryPurchasesResponse(BillingResult billingResult, List<Purchase> purchases) {
Log.d("iap5", "onResume.onQueryPurchasesResponse : " + billingResult.getResponseCode());
if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
}
}
}
);
}
@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
bm.resume(BillingClient.ProductType.INAPP);
}
MainActivity의 onResume 구현