2019년 이번해는 정기구독 서비스들이 대세를 이뤘다. 이에 맞춰 정기구독기능을 추가해달라는 요청이 들어와 붙혀보고 정리하는 시간을 가질까한다.
앱스토어 준비
-이미 플레이스토어에 앱이 올라와있다고 가정하고 진행하겠다.
- 앱정보 -> 인앱상품 -> 구독 으로 가면 해당 페이지가 뜬다.
- 구독 만들기를 눌러 새로운 구독 아이템을 만들어보자.
다른부분은 일반적으로 작성하면된다. 그중 중요하게 봐야는 부분은 '제품 ID'인데 추후에 앱에서 연동할때 쓰이는 key 값이기 때문에 잘 기억해 둬야한다.(중요)
-<그림2>을 보면 가격 추가부분이 있다.
이부분을 누르면<그림3>이 나오고 가격을 책정하면 자동으로 10프로 부가세 붙어 가격이 책정된다.
- 나머지 부분은 너무 쉽기때문에 생략하고 작성완료하면 <그림1>과 같이 아이템이 생성된다.
앱과연동
gradle,manifest에 추가
dependencies {
//billing
implementation 'com.android.billingclient:billing:2.0.3'
implementation 'com.google.code.gson:gson:2.8.6'
}
-gradle dependencies에 추가해 라이브러리를 불러온다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
....
<uses-permission android:name="com.android.vending.BILLING" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
.....
</application>
</manifest>
- use permission에 com.android.vending.BILLNG을추가해준다.
핸들러
public Handler billingHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
//스토어와 연결
case Common.HANDLER_BILLING_INIT://1
initBilling();
break;
// 연결된 상태에서 구독 상품 가저오기
case Common.HANDLER_BILLING_GET_CONTENTS://2
getContentsList();
break;
//가저온 구독 상품을 화면에 보여주기
case Common.HANDLER_BILLING_SHOW_PURCHASE:
showBilling(userSkuDetails);
break;
//스토어 앱캐시로 체크(첫번째 체크 방법)
case Common.HANDLER_BILLING_CHECK_PERCHASE:
checkPurchaseAppCache();
break;
//스토어와 통신한뒤 체크(두번째 체크 방법)
case Common.HANDLER_BILLING_HISTORY_PURCHASE:
checkPurchaseHttpConection();
break;
//통신 실패시 처리
case Common.HANDLER_BILLING_CONNECTION_FAIL:
unstableServer();
break;
}
}
};
- 핸들러로 정확하게 동작을 제어해준다.
결제콜백 만들어주기
//purchase callback
private PurchasesUpdatedListener purchasesUpdatedListener = (BillingResult billingResult, @Nullable List<Purchase> purchases)->{
//성공
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null){
for (Purchase purchaseItem: purchases){
handlePurchase(purchaseItem);//구매 정보
confirmPerchase(purchaseItem);//구매 확정
}
}
//취소
else if (BillingClient.BillingResponseCode.USER_CANCELED == billingResult.getResponseCode()){
}
//에러
else {
}
};
-결제 한뒤 결과를 알려주는 listener이다.
-성공 : 성공한뒤에는 구매정보는 서버에 저장시켜놓고, 구매확정을 해야 최종 결제 승인이된다(안할경우 자동 취소됨)
-취소
-에러
구매 정보
private void handlePurchase(Purchase purchase) {
String purchase_json= purchase.getOriginalJson();
// 원하는 데이터를 가공해 서버에 저장시킨다
}
- json으로 구매 데이터가 들어오는데 이부분을 서버측에 저장시킨다.
구매 확정
//구매확정
public void confirmPerchase(Purchase purchase) {
//PURCHASED
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams, (BillingResult billingResult) ->{
// 구매확정콜백
});
}
}
//PENDING
else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
//구매 유예
}
else {
//구매확정 취소됨(기타 다양한 사유...)
}
}
스토어와 연결
private BillingClient billingClient;
public void initBilling(){
billingClient = BillingClient.newBuilder(mContext)
.enablePendingPurchases()
.setListener(purchasesUpdatedListener)
.build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
//연결 성공
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
billingHandler.sendEmptyMessage(Common.HANDLER_BILLING_GET_CONTENTS);//상품리스트 가저오기
}
//연결 실패
else{
billingHandler.sendEmptyMessage(Common.HANDLER_BILLING_CONNECTION_FAIL);
}
}
//연결 끊김
@Override
public void onBillingServiceDisconnected() {
}
});
}
- 위에서 만들엇던 listner를 setListner부분에 붙혀준다.
-연결성공 : 연결이 성공되면 그커넥션을 가지고 상품 리스트를 가저와야한다.
-연결실패 : 하면서 연결이 실패된적은 없엇던것같다.
-연결끊김
상품가저오기
public void getContentsList(){
List<String> sku_contents_list = new ArrayList<String>();
sku_contents_list.add(itemName);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(sku_contents_list).setType(BillingClient.SkuType.SUBS);//결제타입 지정
SkuDetailsResponseListener listener = new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
//연결을 못함.
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK){
return;
}
//상품정보를 가저오지 못함 -
if (skuDetailsList == null){
return;
}
//상품사이즈 체크
L.i("## response data size : " + skuDetailsList.size());
//상품가저오기 : 정기결제상품 하나라서 한개만 처리함.
try {
for (SkuDetails skuDetails : skuDetailsList) {
String title = skuDetails.getTitle();
String sku = skuDetails.getSku();
String price = skuDetails.getPrice();
userSkuDetails = skuDetails;
if (itemName.equals(sku)){
showBilling(skuDetails);
}
}
}
catch (Exception e){
L.e("## 리스트 가저오기 오류" + e.toString());
}
}
};
billingClient.querySkuDetailsAsync(params.build() , listener);
}
- 결제타입 지정 : BillingClient.SkuType.SUBS, BillingClient.SkuType.INAPP 두 가지중 우리는 정기구독을 만들것이기 때문에 BillingClient.SkuType.SUBS를 사용한다.
-itemName은 <그림2>에서 언급한 key값을 넣어주면된다.
- 상품가저온 뒤엔 skuDetails를 가지고 상품을 보여줘야한다. ->showBilling(skuDetails)
결제화면 보여주기
//결제화면 보여주기
private void showBilling(SkuDetails skuDetails) {
// Retrieve a value for "skuDetails" by calling querySkuDetailsAsync().
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
billingClient.launchBillingFlow(mActivity,flowParams);
}
- google 라이브러리내 내장된 결제 화면이 나온다.
- purchaseUpdateListner에 결제 결과가 나온다.
- 작동시키면 아마 화면은 뜨지만 결제 화면은 안뜰것이다. 뒤쪽에 설명하겠다.
결제상태 체크
public void checkPurchase() {
billingClient = BillingClient.newBuilder(mContext)
.enablePendingPurchases()
.setListener(purchasesUpdatedListener)
.build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
//connection success
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//check query
Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
int list = purchasesResult.getPurchasesList().size();
//앱스토어에서 정기결제 구독한 사람
if (list>=0){
for (Purchase purchase : purchasesResult.getPurchasesList()){
Gson gson = new Gson();
BillingStateVO bsVO = gson.fromJson(purchase.getOriginalJson(), BillingStateVO.class);
boolean isPerchase = false;
isPerchase = pakageName.equals(bsVO.getPackageName())&&itemName.equals(bsVO.getProductId());
//해당앱 정기 구독한 사람
if (isPerchase){
//정기구독 유지중인 사람
if (bsVO.isAutoRenewing()){
}
//정기구독 취소한사람
else {
}
}
//해당앱 정기구독 구입한적이 없는 사람
else {
}
}
}
//앱스토어에서 정기결제 한번도 한적 없는 사람
else {
}
}
//connection fail
else{
}
}
@Override
public void onBillingServiceDisconnected() {
//
}
});
}
- 필요할때 상태를 체크해 서버에 업데이트를 한다.
결제상태 체크 2번째
콜백
public interface CheckPurchaseCallback{
void onSubscribe(BillingStateVO bsVO);
void onSubscribePending();
void onSubscribeEND(BillingStateVO bsVO);
void onNotPurchased(String msg);
void onUnstableServer();
}
구현
public static void checkPurchaseAppCache(
Context context
,String itemID
,CheckPurchaseCallback callback)
{
BillingClient cBillingClient = BillingClient.newBuilder(context)
.enablePendingPurchases()
.setListener((BillingResult billingResult, @Nullable List<Purchase> purchases)->{
})
.build();
cBillingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
//connection success
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//check query
Purchase.PurchasesResult purchasesResult = cBillingClient.queryPurchases(BillingClient.SkuType.SUBS);
List<Purchase> list = purchasesResult.getPurchasesList();
//앱스토어에서 정기결제 구독한 사람
if (list.size() > 0 ){
//제품이 하나라서 한개만 처리
Purchase purchase = list.get(0);
Gson gson = new Gson();
BillingStateVO bsVO = gson.fromJson(purchase.getOriginalJson(), BillingStateVO.class);
boolean isPerchase = false;
isPerchase = itemID.equals(bsVO.getPackageName())&&itemID.equals(bsVO.getProductId());
// 정기 구독한 사람
if (isPerchase){
//시간비교
long diff = DateUtils.currentTime() - bsVO.getPurchaseTime();
int diffDays = (int)(diff/(24*60*60*1000));
switch (bsVO.getPurchaseState()){
//구독중 ...
case LifeInAppConfig.PURCHASE_STATE_PURCHASED:
callback.onSubscribe(bsVO);
break;
// 구독이 끝나도 30일간 사용 가능
case LifeInAppConfig.PURCHASE_STATE_CANCELED:
//30일 지나감
if (diffDays>30){
callback.onSubscribeEND(bsVO);
} else {
callback.onSubscribe(bsVO);
}
break;
// 결제 수단문제로 구매 보류가 이뤄졌을때 ...
case LifeInAppConfig.PURCHASE_STATE_PENDING:
callback.onSubscribePending();
break;
}
}
// 정기구독 구입한적이 없는 사람
else {
callback.onNotPurchased("auto Renewing NOT YET");
}
}
//앱스토어에서 정기결제 한번도 한적 없는 사람
else {
callback.onNotPurchased(" auto renew nothing ");
}
}
//connection fail
else{
callback.onUnstableServer();
}
}
@Override
public void onBillingServiceDisconnected() {
}
});
}
purchase state : 중요한 녀석이다. 결제상태를 확인할 수 있다.
0 : 결제상태
1 : 취소상태
2 : 결제 보류 상태
사용
private void checkInAppbilling(){
LifeInAppBilling.checkPurchaseAppCache(
this
,AppConfig.BILLING_ITEM
,new CheckPurchaseCallback() {
@Override
public void onSubscribe(BillingStateVO bsVO) {
bsVO.printVO();
if (!bsVO.isAutoRenewing()){
//정책에 따라 자동갱신안한경우 자동갱신하도록 유도
}
}
@Override
public void onSubscribePending() {
//보류
}
@Override
public void onSubscribeEND(BillingStateVO bsVO) {
bsVO.printVO();
}
@Override
public void onNotPurchased(String msg) {
}
@Override
public void onUnstableServer() {
}
});
}
- interface callback을 수정해 원하는 콜백으로 구현하면 됩니다.
BillingStateVO
// 상태조회시 넘어오는 값들.
public class BillingStateVO {
private String orderId;
private String packageName;
private String productId;
private long purchaseTime;//long
private String purchaseState;//int
private String purchaseToken;
private boolean autoRenewing;//boolean
private boolean acknowledged;//boolean
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String pakageName) {
this.packageName = pakageName;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public long getPurchaseTime() {
return purchaseTime;
}
public void setPurchaseTime(long purchaseTime) {
this.purchaseTime = purchaseTime;
}
public String getPurchaseState() {
return purchaseState;
}
public void setPurchaseState(String purchaseState) {
this.purchaseState = purchaseState;
}
public String getPurchaseToken() {
return purchaseToken;
}
public void setPurchaseToken(String purchaseToken) {
this.purchaseToken = purchaseToken;
}
public boolean isAutoRenewing() {
return autoRenewing;
}
public void setAutoRenewing(boolean autoRenewing) {
this.autoRenewing = autoRenewing;
}
public boolean isAcknowledged() {
return acknowledged;
}
public void setAcknowledged(boolean acknowledged) {
this.acknowledged = acknowledged;
}
public void printVO(){
L.i("\n## orderId : "+orderId +
"\n pakageName : "+packageName +
"\n productId : "+productId +
"\n purchaseTime : "+purchaseTime +
"\n purchaseState : "+purchaseState +
"\n purchaseToken : "+purchaseToken +
"\n autoRenewing : "+autoRenewing +
"\n acknowledged : "+acknowledged +"\n"
);
}
}
알파테스트
- 앱버전 -> 관리
-새 목록 만들기
- 테스트 참여 URL 복사한뒤 구글아이디가 로그인된 브라우저(휴대폰)에서 해당 url로 이동
- 테스트 참여 시작 클릭!(
-몇분뒤 테스터로 등록되고 플레이 스토어 들어가보면 [ 앱이름 (베타,알파) ]로 이름이 바뀌어있다, 테스트 앱 등록 완료
- 결제 테스트 진행!
테스트
여기다 이메일 추가하면 15분 정도뒤 테스터로 등록되 실제 결제 하듯이 결제할수있다.
일회성 제품 및 정기 결제의 구매 흐름은 비슷하지만, 정기 결제에는 정기 결제 갱신의 성공 또는 거부와 같은 추가 시나리오가 있습니다. 두 가지 상황 모두에서 애플리케이션을 테스트하는 데 도움이 되도록 '테스트 도구, 항상 승인' 및 '테스트 도구, 항상 거부' 결제 수단을 사용할 수 있습니다. 성공적인 정기 결제 시나리오 이상의 시나리오를 테스트하려면 이러한 결제 수단을 사용하세요.
테스트 정기 결제 갱신
테스트 정기 결제는 테스트를 돕기 위해 일반적인 경우보다 더 빨리 갱신됩니다. 다음 표에는 기간이 다양한 정기 결제의 테스트 갱신 시간이 나와 있습니다.
참고: 이러한 시간은 대략적인 것으로, 이벤트의 정확한 시간에는 약간의 변동이 있을 수 있습니다. 이러한 변동을 보완하려면 각 정기 결제 만료일 이후 API를 호출하여 현재 상태를 확인하세요.
프로덕션 정기 결제 기간테스트 구독 갱신
1주 | 5분 |
1개월 | 5분 |
3개월 | 10분 |
6개월 | 15분 |
1년 | 30분 |
참고: 테스트 정기 결제는 최대 6회 갱신됩니다.
'AOS' 카테고리의 다른 글
[안드로이드]오늘 하루 안보기 (0) | 2019.12.21 |
---|---|
[안드로이드] 위아래가 사라지는 레이아웃 (0) | 2019.12.21 |
[안드로이드] permission 관련 util (0) | 2019.12.20 |
[안드로이드]커스텀 위젯만들기 (0) | 2019.12.20 |
[안드로이드] 실행중 시스템 언어 변경하기 (0) | 2019.12.20 |