시작하며...
이번에 프로젝트를 하면서 인앱결제 부분을 맡게 되었는데 안드로이드 인앱결제 구현을 한 번도 해본적이 없어서 애좀 먹었다.
나 역시도 인앱결제 별거아니겠지~ 생각했는데 의외로 새로운 개념들이 많았다!
* 수정 (2018.10.13)
관련 소스파일은 링크(http://jizard.tistory.com/153) 를 참조하세요!
* 추가 (2019.03.10)
빠른 진행을 원하신다면 간단 버전(https://jizard.tistory.com/164)을 참조 해 주세요~
시작 전에 미리 준비할 것
안드로이드 개발자 계정
알기싫어도 꼭 알아 둬야 할 개념들
- 인앱 상품의 종류
인앱상품의 종류에는 관리되는 제품과 구독 2가지가 있다.
관리되는 제품이란 우리가 일반적으로 생각하는 소비가능한 게임 아이템/화폐 또는 일회성(광고제거) 아이템등을 말한다.
하지만 관리되는 제품은 한 번 구매했을 경우 다시 구매하지 못한다.
만약 다시 구매하도록 하고싶을 경우 구매 즉시 소비(consume)처리를 해주어야한다.
즉 게임 아이템은 구매 즉시 소비처리를 해야하고, 광고제거같은 기능은 소비 처리를 하면 안된다.
(이번 예제에서는 앱 내에서 사용되는 화폐, 즉 관리되는 제품을 다룰 것이다.)
구독 상품은 예를들면 멜론 스트리밍권, 유튜브 레드 이용권처럼 주기적으로 결제되는 상품이다.
- Sku
예제를 따라가다보면, 그리고 Android Developers 설명을 읽다보면 Sku라는 용어를 어렵지 않게 볼 수 있다.
Sku는 인앱결제 제품의 고유 ID로, 아래 사진의 "h1000", "h10000" 같은 ID에 해당한다.
더 쉽게는 그냥 개별 결제 아이템 종류를 Sku라고 생각하면 된다.
예제에서는 이 sku 객체 안에 아이템의 가격, ID, 라벨등이 포함되어있다.
- 테스팅
안드로이드 스튜디오에서 빌드한 apk 상태에서는 결제 테스트가 되지 않는다. (Sku 목록조차 안뜬다.)
꼭 알파든 베타든 구글 개발자 콘솔에 올린 버전으로 테스트해야한다.
1. 구글 개발자 콘솔에서 준비하기
개발자 콘솔에서 자신의 앱을 하나 생성 후, 앱 정보 > 인앱상품 탭에 들어가서 관리되는 제품 만들기를 클릭해서 결제 할 아이템을 미리 만들어 둔다.
여기서 말하는 제품 ID = Sku이다.
그리고 콘솔의 개발 도구 > 서비스 및 API > 라이선스 및 인앱 결제 에서 이 애플리케이션 용 라이선스키를 복사해서 어딘가에 메모 해둔다.
1 2 3 4 5 6 7 8 9 | jANBgkPzchXqpooauBScMIIBAMIIBCgKCAQEAjbxbw0P0BTY M9qiEY+2EAeubIIXoMJosOn0sVVw0P0BTYly2aV9QlfwrDn5 JDksdj2kjsfalkdfjsdkfNCxkcSdXksd29aF8VT0KM1Ou3UK Hds3z+2yQ9H/jiq1EtDsKDFFfDFHdksSSdks4501PpDnjcO0 czSyBpV80ujOi/zSdXksd29aF8VTxLtgSKqgOI1f/7q3UyaH A0z4MBjJ8Ayuvmm7CMKqWPpDn5YDVkvHW866MYsTZTnkgHZ3 yPjMN9gRFTRSvUD/GBqVDdWwrWUqxF1d+dtiIuL7CoCYuVGi g96317wW9qzpPEIdsISdjschC8S14U82qcwVbeqvrzsxMeI sdjsdAQAB | cs |
2. 안드로이드 스튜디오에서 세팅
이제 프로젝트에 InAppBilling 라이브러리(https://github.com/anjlab/android-inapp-billing-v3)를 불러와야한다.
project 레벨의 build.gradle에 아래 구문을 추가한다.
1 2 3 4 5 6 | repositories { mavenCentral() } | cs |
그리고 app 레벨의 build.gradle 에 아래의 한 줄을 추가한다.
1 | dependencies { compile 'com.anjlab.android.iab.v3:library:1.0.44' } | cs |
<uses-permission android:name="com.android.vending.BILLING" />
3. 구현
public class MainActivity extends Activity implements BillingProcessor.IBillingHandler
onPurchaseHistoryRestored()
깃헙 설명에보면 Called when purchase history was restored and the list of all owned PRODUCT ID's was loaded from Google Play
라고 되어있다.
일종의 '구매복원'이되었을때인데, 이때는 앱 내에서 구매를 했는지 안했는지 체크가 되는 상태이기때문에 광고제거라면 광고를 숨겨주는 등의 일을 하면된다.
1) 메인 액티비티의 상단에 결제 관련 변수를 추가 시켜준다.
1 2 3 4 | private PurchaseHeartsAdapter skusAdapter; private BillingProcessor bp; public static ArrayList<SkuDetails> products; private MaterialDialog purchaseDialog; | cs |
*PurchaseHeartsAdapter는 내가 구매할 아이템들을 purchaseDialog에 리스트 형식으로 쭉 보여주기 위해서 만든 커스텀 어댑터다.
리스트 클릭시 purchase하도록 하는 친구다.
2) 메인 액티비티의 onCreate 부분에 BillingProcessor를 초기화 시켜준다.
<GP_LICENSE_KEY>라고 되어있는 부분에 아까 1번 단계에서 메모장에 복사 해 두었던 구글 플레이 라이선스 키 값을 넣어주면된다.
1 2 3 4 5 6 7 | @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); bp = new BillingProcessor(this, <GP_LICENSE_KEY>, this); } | cs |
3) 준비가 되었으니 이제 onBillingInitialized() 메소드를 채워보자.
결제 준비가 되면 BillingProcessor에서 구매가능한 아이템 리스트를 가지고온다.
가지고올때 순서는 구글 개발자 콘솔에 있는 순서랑 같은듯한데, 나는 가격낮은순으로 불러오고 싶어서 sort작업을 했다.
아이템 하나 = SkuDetails 객체이다.
자주 쓰는 SkuDetails 필드
- SkuDetails.title: 하면 해당 아이템의 '이름 (구글플레이 앱 제목)' 형식으로 올라온다.
그래서 나같은 경우 아이템 이름만 보여주고 싶어서 SkuDetails.title.replaceAll("\\(.*\\)", "") 를 붙여주어 구글 플레이 앱 제목 부분을 없앴다.
- SkuDetails.priceText: 아이템 가격에 현지 화폐 단위를 붙인 String을 리턴한다. 예를들면 '1.99$'이런 식이다.
- SkuDetails.priceLong: 가격을 long 으로 리턴한다. 1.99 이런식이다.
- SkuDetails.productId: 제품ID(sku)를 가지고 온다. 이를 통해서 어떤 아이템을 구매했는지 판별 가능하다.
결제 아이템들이 있는 리스트를 받아온 후에는 그것을 Adapter에 담고 다이얼로그를 준비한다.
purchaseDialog.show()하기만 하면 결제 할 수 있는 아이템을 띄울 준비가 된것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | @Override public void onBillingInitialized() { products = (ArrayList<SkuDetails>) bp.getPurchaseListingDetails(new InAppPurchaseItems().getIds()); // Sort ascending order Collections.sort(products, new Comparator<SkuDetails>() { @Override public int compare(SkuDetails o1, SkuDetails o2) { if (o1.priceLong > o2.priceLong) { return 1; } else if (o1.priceLong < o2.priceLong) { return -1; } else return 0; } }); // 결제 아이템 다이얼로그 설정 skusAdapter = new PurchaseHeartsAdapter(this); View purchaseView = getLayoutInflater().inflate(R.layout.layout_dialog_heartstore, null); ListView lvSkus = purchaseView.findViewById(R.id.lv_skus); lvSkus.setAdapter(skusAdapter); purchaseDialog = new MaterialDialog.Builder(getContext()) .customView(purchaseView, false) .negativeText(R.string.cancel) .onNegative(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { dialog.dismiss(); } }) .build(); skusAdapter.update(products); } | cs |
4) 구매하기 함수
보여주기는 끝났고, 이제 구매는 어떻게 하면 되는 걸까?
나는 purchaseProduct라는 함수를 만들어서 구매 처리를 하기로 했다.
purchaseDialog에서 구매자가 구매할 아이템을 클릭하면 해당 제품 ID를 넘겨서 purchaseProduct()를 호출한다.
만약에 이미 구매했던 제품이라면(isPurchased) 즉각 소비해서 없앤 후 구매 절차를 시작한다.
1 2 3 4 5 6 | public void purchaseProduct(final String productId) { if (bp.isPurchased(productId)) { bp.consumePurchase(productId); } bp.purchase(this, productId); // 결제창 띄움 } | cs |
구매 절차가 시작되고, 결제에 성공하면 onProductPurchased, 결제가 취소되거나 오류가 발생하면 onBillingError를 발생시킬 것이다.
각각을 구현 해주자. 자세한 설명은 주석에 달아놓았다.
구매자가 결제창을 껐을때는 딱히 할 액션이 없기때문에 그것을 제외한 오류에 대해서 메세지를 띄워주었다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | @Override public void onProductPurchased(@NonNull String productId, @Nullable TransactionDetails details) { // 구매한 아이템 정보 SkuDetails sku = bp.getPurchaseListingDetails(productId); // 하트 100개 구매에 성공하였습니다! 메세지 띄우기 String purchaseMessage = sku.title + getString(R.string.purchase_succeed); Common.showMessage(this, getCurrentFocus(), purchaseMessage); // 구매 처리 int amount = 0; try { // 사용자의 하트 100개를 추가 amount = Integer.parseInt(productId.substring(1)); userStore.purchaseHearts(amount, tvNavigationHearts); } catch (Exception e) { Toast.makeText(this, R.string.purchase_error, Toast.LENGTH_LONG).show(); e.printStackTrace(); } } @Override public void onBillingError(int errorCode, @Nullable Throwable error) { if (errorCode != Constants.BILLING_RESPONSE_RESULT_USER_CANCELED) { String errorMessage = getString(R.string.purchase_error) + " (" + errorCode + ")"; Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show(); } } | cs |
5) 구글 플레이 출시, 테스터 설정
다시 구글 플레이 콘솔로 들어가서 설정 > 테스트 참여 대상 관리 > 목록 만들기 해서
테스트할 구글 계정을 추가한다. 여러명도 가능하다. 나는 목록 이름을 purchaseTester라고 해 두었다.
알파 또는 베타 버전에 현재까지 구현한 빌드를 올린 뒤, 테스트 참여 관리 대상을 방금 만든 purchaseTester로 설정해주면
purchaseTester들은 결제 테스트를 시도 할 수 있고, 어떠한 돈도 청구되지 않는다.
테스트를 위해서는 아래 테스트 참여 URL로 들어가서 앱을 다운받으면 된다.
6) 실제 기기에 다운받아 테스트 해보기!
실제 기기에서 테스트해보자.
테스트 주문이므로 청구되지 않습니다 문구를 확인하구 마구 결제해보면된다...ㅎㅎ
그럼 오랜만의 포스팅 여기서 끝~!
'프로그래밍 > Android' 카테고리의 다른 글
안드로이드 스킬 향상을 위해 참고하면 좋은 오픈소스 프로젝트들 (1) | 2018.03.17 |
---|---|
안드로이드 Fragment 상태를 저장하고 복구하는 Best practice 소개 (0) | 2018.03.05 |
안드로이드 ListView 스크롤 끝날때 물결 없애는 방법 (0) | 2017.10.21 |
[안드로이드] Retrofit으로 API 통신하기 (4) | 2017.03.30 |
[Retrofit] 안드로이드로 HTTP 통신하는 Retrofit 소개 (0) | 2017.03.28 |