결제 모듈 흐름
1. store 연결여부
2. 제품이 존재하는지 체크
3. 제품을 구매
4. 구매 후 구매 확정
큰 결제 흐름은 위와 같으며 이에 맞춰 interface를 커스텀 하면 됩니다.
예제
public interface BillingModuleCallback {
//store 연결여부
public void onStoreConnection(BillingModule billingModule ,int result);
}
activity에 사용할 interface 부분을 이런식으로 만들어서 쓴다.
0.기본셋팅
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.han.goodmorningpops">
<uses-permission android:name="android.permission.INTERNET" />
....
</manifest>
dependencies {
//billingClient v3
implementation 'com.android.billingclient:billing:3.0.0'
}
optional
public class BillingModule {
private String PRODUCT_TYPE = BillingClient.SkuType.INAPP;
//결제 모듈을 사용할 액티비티
private Activity activity;
//스토어에 등록된 제품명
private List<String> sku_contents_list;
//결제모듈
private BillingClient billingClient;
..........
}
......
/**
* late init
*/
private BillingModule(){ }
private static class BillingPlaceHolder{
private static final BillingModule INSTANCE = new BillingModule();
}
public static BillingModule getInstance(){
return BillingPlaceHolder.INSTANCE;
}
/**
* step1
* create instance
*/
public BillingModule whereToUse(@NonNull Activity activity){
this.activity = activity;
return this;
}
/**
* step2
* set product list
*/
public BillingModule setContentsList(@NonNull List<String> sku_contents_list){
this.sku_contents_list = sku_contents_list;
return this;
}
public BillingModule setPurchaseType(@BillingClient.SkuType String purchaseType){
this.PRODUCT_TYPE = purchaseType;
return this;
}
.......
- 편하게 사용하기위해 별도 클래스를 빼서 사용함.
activit에서 사용
//initialize
List<String> sku_contents_list = new ArrayList<>();
sku_contents_list.add("com.my.showmethemonby.product1");
billingModule = BillingModule.getInstance()
.whereToUse(this)
.setContentsList(sku_contents_list)
.setBillingModuleCallback(this);
//start
billingModule.start();
1. store 연결여부
private BillingClient billingClient;
billingClient = BillingClient.newBuilder(activity)
.setListener(purchaseUpdateListener)
.enablePendingPurchases()
.build();
- purchaseUpdateListener는 '3. 제품을 구매' 에서 설명.
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
//ready to connection
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
getProduct();
}
// billing is not ready
else {
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
//
}
});
- onBillingSetupFinished로 스토어와 통신이 이뤄지는지 체크한다.
optional
public BillingModule start(){
//check validation
if (activity == null) throw new IllegalArgumentException("activity must be 'Not Null'");
if (sku_contents_list == null) throw new IllegalArgumentException("sku product list must be 'not Null'");
if (sku_contents_list.size()==0) throw new IllegalArgumentException("sku product list size greater than '0'");
//initialize billingClient
if (billingClient == null){
billingClient = BillingClient.newBuilder(activity)
.setListener(purchaseUpdateListener)
.enablePendingPurchases()
.build();
}
//request to store connection open
getStoreBillingConnection();
return this;
}
- optional을 선택한 분은 이렇게 시작을해 start() 사용시 필요한 인자들을 validation 할 수 있다.
- throw로 본인이 원하는 validation custom
2. 제품이 존재하는지 체크
private List<String> sku_contents_list;//product name in your store
private void getProduct(){
if (sku_contents_list==null || sku_contents_list.size() == 0) return;
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(sku_contents_list)
.setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),getProductCallback);
}
- sku_contents_list를 가지고 스토어에 이런 제품이 있는지 질의한다.
- params에 setType이 있는데 INAPP은 일회성 결제 , SUBS는 정기 구독 결제를 의미한다.
//get product callback
private SkuDetailsResponseListener getProductCallback = new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(@NonNull BillingResult billingResult,
@Nullable List<SkuDetails> list) {
//연결성공
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK){
//상품 가저오기 실패
if (list == null){
return;
}
//상품을 가저왔으나 제품이 없음
if (list.size()==0){
return;
}
//success
for (SkuDetails details:list){
showBilling(details);
}
}else {
//fail to connect
}
}
};
- SkuDetails list는 sku_contents_list에서 질의한 제품만큼 리턴된다.
SkuDetails은 VO객체라서 for문을 써서 처리해도 되고 ,
가지고 있다가 원하는 타이밍에 활용해도된다.
3. 제품을 구매
//결제화면 보여주기
private void showBilling(SkuDetails skuDetails) {
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
billingClient.launchBillingFlow(activity,flowParams);
//go to PurchasesUpdatedListener
}
//결제 결과 처리
private PurchasesUpdatedListener purchaseUpdateListener = new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {
//when you complete purchase , confirm product purchase
for (Purchase purchase : list) {
handlePurchase(purchase);
//결제결과를 리턴
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
// 사용자의 의지대로 취소했을 경우
} else {
// Handle any other error codes.
// 알수 없는 에러
}
}
};
- 위에서 제품이 있는지 확인 했으니 가저온 SkuDetails 를 가지고 '저 결제 할게요' 하고 통신 하는 부분.
- 결제 결과는 purchaseUpdateListener에 들어온다.
- Purchase객체는 4번 내용에 이어지니 잘 가지고 있어야한다.
4. 구매 후 구매 확정
private void handlePurchase(Purchase purchase){
switch (PRODUCT_TYPE){
case BillingClient.SkuType.INAPP:
handlePurchaseINAPP(purchase);
break;
case BillingClient.SkuType.SUBS:
handlePurchaseSUBS(purchase);
break;
default:
throw new IllegalArgumentException("Undefined purchase type!");
}
}
- 위에서 설명한것처럼 두가지 결제 타입이 있고 결제 타입에 따라 처리하는 method가 다르다.
둘중 하나 만 사용한다면 이부분은 없어도 되는 부분
일회성 결제 BillingClient.SkuType.INAPP
//소비성 결제 상품 결제확정
private void handlePurchaseINAPP(Purchase purchase) {
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.consumeAsync(consumeParams, consumeResponseListener);
}
//소비성 결제 상품 결제확정 callback
private ConsumeResponseListener consumeResponseListener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// Handle the success of the consume operation.
} else {
//fail connection
}
}
};
- 3에서 언급한듯이 Purchase객체를 잘가지고 있다가 소비성 결제 상품을 결제 확정했다는 것을 구글에 알림
정기구독결제 BillingClient.SkuType.SUBS
private void handlePurchaseSUBS(Purchase purchase) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams,
acknowledgePurchaseResponseListener);
}
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING){
//거래 중지 등등 ... 결제관련 문제가 발생했을때
} else {
//그외 알 수 없는 에러들...
}
}
//인앱결제 확정 callback
private AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//success
} else {
//fail
}
}
};
- 구독상품 같은 경우는 결제 유예라는 시스템을 두어 , 사용자가 결제문제를 해결할때까지 기다려준다.
참고용
import android.app.Activity;
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.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import java.util.List;
public class BillingModule {
private String PRODUCT_TYPE = BillingClient.SkuType.INAPP;
/**
*
*/
private Activity activity;
private List<String> sku_contents_list;//product name in your store
private BillingClient billingClient;
private PurchasesUpdatedListener purchaseUpdateListener;
private BillingModuleCallback billingModuleCallback;
/**
* late init
*/
private BillingModule(){ }
private static class BillingPlaceHolder{
private static final BillingModule INSTANCE = new BillingModule();
}
public static BillingModule getInstance(){
return BillingPlaceHolder.INSTANCE;
}
/**
* step1
* create instance
*/
public BillingModule whereToUse(@NonNull Activity activity){
this.activity = activity;
return this;
}
/**
* step2
* set product list
*/
public BillingModule setContentsList(@NonNull List<String> sku_contents_list){
this.sku_contents_list = sku_contents_list;
return this;
}
public BillingModule setPurchaseType(@BillingClient.SkuType String purchaseType){
this.PRODUCT_TYPE = purchaseType;
return this;
}
/**
* optional
* add listener
*/
public BillingModule setBillingModuleCallback(BillingModuleCallback billingModuleCallback){
this.billingModuleCallback = billingModuleCallback;
return this;
}
// public BillingModule setPurchaseUpdateListener(PurchasesUpdatedListener purchaseUpdateListener){
// this.purchaseUpdateListener = purchaseUpdateListener;
// return this;
// }
// public BillingModule setPurchaseUpdateListener(){
// this.purchaseUpdateListener = purchaseUpdateListener2;
// return this;
// }
/**
* ### FLOW1: open connection with store ###
* open
*/
public BillingModule start(){
//check validation
if (activity == null) throw new IllegalArgumentException("activity must be 'Not Null'");
if (sku_contents_list == null) throw new IllegalArgumentException("sku product list must be 'not Null'");
if (sku_contents_list.size()==0) throw new IllegalArgumentException("sku product list size greater than '0'");
//initialize billingClient
if (billingClient == null){
billingClient = BillingClient.newBuilder(activity)
.setListener(purchaseUpdateListener2)
.enablePendingPurchases()
.build();
}
//request to store connection open
getStoreBillingConnection();
return this;
}
//request to store connection open
private void getStoreBillingConnection(){
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
//ready to connection
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (billingModuleCallback !=null){
billingModuleCallback.onStoreConnection(BillingModule.this,BillingModuleCallback.OpenStore.CONNECTION_OK);
}
getProduct();
}
// billing is not ready
else {
if (billingModuleCallback !=null){
billingModuleCallback.onStoreConnection(BillingModule.this,BillingModuleCallback.OpenStore.CONNECTION_FAIL);
}
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
if (billingModuleCallback !=null) {
billingModuleCallback.onStoreConnection(BillingModule.this,BillingModuleCallback.OpenStore.CONNECTION_CANCEL);
}
}
});
}
/**
* ### FLOW 2: Check if there is a product in store ###
*
* BillingClient.SkuType.INAPP : 일회성 상품
* BillingClient.SkuType.SUBS : 구독상품
*
* check product
*/
private void getProduct(){
if (sku_contents_list==null || sku_contents_list.size() == 0) return;
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(sku_contents_list).setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),getProductCallback);
}
//get product callback
private SkuDetailsResponseListener getProductCallback = new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> list) {
//연결성공
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK){
//상품 가저오기 실패
if (list == null){
if (billingModuleCallback !=null) {
billingModuleCallback.onCheckProduct(BillingModule.this,BillingModuleCallback.CheckProduct.PRODUCT_NULL);
}
return;
}
//can get
if (list.size()==0){
if (billingModuleCallback !=null) {
billingModuleCallback.onCheckProduct(BillingModule.this,BillingModuleCallback.CheckProduct.PRODUCT_EMPTY);
}
return;
}
//success
if (billingModuleCallback !=null) {
billingModuleCallback.onCheckProduct(BillingModule.this,BillingModuleCallback.CheckProduct.PRODUCT_GET);
}
for (SkuDetails details:list){
showBilling(details);
}
}else {
//fail to connect
if (billingModuleCallback !=null) {
billingModuleCallback.onCheckProduct(BillingModule.this,BillingModuleCallback.CheckProduct.CONNECTION_FAIL);
}
}
}
};
/**
* ### FLOW3: show Billing module and Check purchase your Product ###
* show purchase
*/
//결제화면 보여주기
private void showBilling(SkuDetails skuDetails) {
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
billingClient.launchBillingFlow(activity,flowParams);
//go to PurchasesUpdatedListener
}
//결제 결과 처리
private PurchasesUpdatedListener purchaseUpdateListener2 = new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {
//when you complete purchase , confirm product purchase
for (Purchase purchase : list) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}
}
};
/**
* ### FLOW4: Confirm purchased your product ###
*
*
* check purchase type
* confirm purchased
*/
private void handlePurchase(Purchase purchase){
switch (PRODUCT_TYPE){
case BillingClient.SkuType.INAPP:
handlePurchaseINAPP(purchase);
break;
case BillingClient.SkuType.SUBS:
handlePurchaseSUBS(purchase);
break;
default:
throw new IllegalArgumentException("Undefined purchase type!");
}
}
/**
* 소비성 결제 BillingClient.SkuType.INAPP
*/
//소비성 결제 상품 결제확정
private void handlePurchaseINAPP(Purchase purchase) {
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.consumeAsync(consumeParams, consumeResponseListener);
}
//소비성 결제 상품 결제확정 callback
private ConsumeResponseListener consumeResponseListener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// Handle the success of the consume operation.
} else {
//fail connection
}
}
};
/**
* 인앱결제 BillingClient.SkuType.SUBS
* - 결제 확정
* int UNSPECIFIED_STATE = 0;
* int PURCHASED = 1;
* int PENDING = 2;
*/
private void handlePurchaseSUBS(Purchase purchase) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
}
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING){
//거래 중지 등등 ... 결제관련 문제가 발생했을때
} else {
//그외 알 수 없는 에러들...
}
}
//인앱결제 확정 callback
private AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//success
} else {
//fail
}
}
};
/**
* etc tool
*/
private void showToast(String msg){
Toast.makeText(activity,msg,Toast.LENGTH_SHORT).show();
}
}
'AOS' 카테고리의 다른 글
(안드로이드)채팅 구현하기 (0) | 2021.01.14 |
---|---|
(안드로이드) jobService (0) | 2020.10.15 |
(android)pinch zoom imageview (0) | 2020.09.03 |
(안드로이드)명시적,암시적 인텐트 (0) | 2020.05.03 |
(안드로이드)manifest에서 activity list 가저오기 (0) | 2020.05.03 |