프로그래밍/Android

안드로이드 인앱결제 구독 구현 예제!

Lou Park 2019. 9. 20. 23:13

* 이 글은 개인적 정리겸 복습을 위해 쓰여진 글로, 설명이 미흡한 부분이있을 수 있습니다! 코드 개선점이나 질문있으시면 댓글로 달아주세요~


결제 구현을 위한 기본적인 준비물들 

제 블로그 결제 구현 글에 자세히 설명 해두었으니 여기서는 간략하게 적고 넘어가겠습니다!


1. build.gradle에 결제 라이브러리 추가

1
implementation 'com.anjlab.android.iab.v3:library:1.0.44'
cs


2. AndroidManifest.xml에 결제 권한 추가

1
<uses-permission android:name="com.android.vending.BILLING" />
cs


3. Google Play Console에 결제 권한이 추가된 APK를 알파 채널에 업로드


4. Google Play Console > 내 앱 클릭 > 개발 도구 > 서비스 및 API >'이 어플리케이션용 라이센스키' 복사 후 앱 내에 저장

(형태는 자유롭게 저장가능합니다. 저는 Config 파일을 만들어서 사용했습니다.)


5. Google Play Console > 앱 정보 > 인앱 상품 > 구독 > 새로운 구독 만들기해서 구독 상품

을 만들고, 제품 아이디(productId)를 기억해둡니다.


6. 구독 버튼 레이아웃을 준비해주세요~ 

제 것은 아래와 같이 해두었습니다. 위쪽버튼이 1개월 구독버튼이며 아래쪽은 단순 광고제거 결제입니다.




자 이제 간단히 준비가 되었으니, 결제 구현을 해보도록 합시다.

저는 BillingManager를 두고 BillingManager에서 구독및 결제관련 코드를 전부 두려고 합니다.

그래서 우선적으로 BillingManager를 구현해보도록 하겠습니다.


BillingManager.java


저는 사용자가 구매를 하려고 화면을 켜면, 1개월에 얼마인지 가격을 표시해주고,

구매버튼을 누르면 구독 결제 프로세스를 진행하려 합니다.

주석 하나하나를 보시면 이해하시는데 도움이 될 것입니다.


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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**
 * Billing manager
 */
public class BillingManager implements BillingProcessor.IBillingHandler {
    private BaseActivity activity;
    private BillingCallback billingCallback; // # 제가 정의한 콜백함수입니다. 각 화면마다 구독 했을때 원하는 결과가 다를 수도 있겠죠!
    private BillingProcessor bp;
    private AppStorage storage;
 
    /*
    - 변수 및 커스텀 클래스 참조
    Config.GP_LICENSE_KEY: 구글 플레이 라이센스 키 (비밀!)
    Config.SKU: 광고제거용 PRO 버전 (관리되는 제품)
    Config.SUBSCRIBE_SKU: 1개월 광고제거 (구독 상품)
    AdLoader(activity.mAdLoader): 구글 애드몹 광고를 보여주기 위해 제가 만든 클래스
    AppStorage: SharedPreference를 쓰기 쉽게 제가 만든 클래스 -> 간단하게 현재 구독 및 결제 상태를 저장하기 위해 사용
     */
 
    public interface BillingCallback {
        // # 원하는 함수가있다면 추가, 필요없다면 제거하기
        void onPurchased(String productId); // # 구매가 정상적으로 완료되었을때 해당 제품 아이디를 넘겨줍니다.
        void onUpdatePrice(Pair<Double, Double> prices); // # 화면에 가격을 표시하고 싶으므로 가격 정보를 넘겨줍니다.
    }
 
    public BillingManager(BaseActivity activity) {
        this.activity = activity;
        this.storage = new AppStorage(activity);
    }
 
    public BillingManager init(BillingCallback billingCallback) {
        this.billingCallback = billingCallback;
        bp = new BillingProcessor(activity, Config.GP_LICENSE_KEY, this);
        bp.initialize();
        return this;
    }
 
    /**
     * 구독 또는 구매 완료시
     * @param productId 제품 아이디
     * @param details 거래 정보
     */
    @Override
    public void onProductPurchased(String productId, TransactionDetails details) {
        bp.loadOwnedPurchasesFromGoogle(); // 구매정보 업데이트
        if (billingCallback != null) {
            billingCallback.onPurchased(productId);
        }
        onResume();
    }
 
    @Override
    public void onPurchaseHistoryRestored() {
        // # 구매 복원 호출시 이 함수가 실행됩니다.
        onResume();
    }
 
    @Override
    public void onBillingError(int errorCode, Throwable error) {
        // # 결제 오류시 따로 토스트 메세지를 표시하고 싶으시면 여기에 하시면됩니다.
    }
 
    /**
     * BillingProcessor 초기화 완료시
     */
    @Override
    public void onBillingInitialized() {
        SkuDetails details = bp.getPurchaseListingDetails(Config.SKU); // PRO 버전 정보
        SkuDetails subDetails = bp.getSubscriptionListingDetails(Config.SUBSCRIBE_SKU); // 1개월 구독 정보
        if (billingCallback != null && details != null) {
            // # SkuDetails.priceValue: ex) 1,000원일경우 => 1000.00
            Pair<Double, Double> pair = new Pair<>(details.priceValue, subDetails.priceValue);
            billingCallback.onUpdatePrice(pair);
        }
        bp.loadOwnedPurchasesFromGoogle(); // 구매정보 업데이트
        onResume();
    }
 
    /**
     * 인앱 상품 구매하기
     */
    public void purchase() {
        if (bp != null && bp.isInitialized()) {
            if (bp.isSubscribed(Config.SUBSCRIBE_SKU)) {
                Toast.makeText(activity, "이미 광고 제거 상품을 구독중입니다. 이중 결제 방지를 위해 구독이 끝나면 PRO 버전을 구매 해 주십시오.", Toast.LENGTH_LONG).show();
            } else {
                bp.purchase(activity, Config.SKU);
            }
        }
    }
 
    /**
     * 구독하기
     */
    public void subscribe() {
        if (bp != null && bp.isInitialized()) {
            // # 저는 PRO 버전도 같이 팔고있기 때문에 중복 구입 방지를 위해 구매여부 체크를 해두었습니다.
            if (!bp.isPurchased(Config.SKU) && !bp.isSubscribed(Config.SUBSCRIBE_SKU)) {
                bp.subscribe(activity, Config.SUBSCRIBE_SKU);
            }
        }
    }
 
    public void onResume() {
        // # SharedPreference에 구매 여부를 저장 해 두고, 그에 따라 광고를 바로 숨기거나 보여주는 코드입니다.
        bp.loadOwnedPurchasesFromGoogle();
        // # PRO 버전 구매를 했거나 구독을 했다면!
        storage.setPurchasedProVersion(bp.isPurchased(Config.SKU) || bp.isSubscribed(Config.SUBSCRIBE_SKU));
        // # 안에는 대충 이런 코드입니다. purchased ? adView.setVisibility(View.GONE) : View.VISIBLE
        if (activity.mAdLoader != null) activity.mAdLoader.update();
    }
 
    public void onDestroy() {
        // # 꼭! 릴리즈 해주세요.
        if (bp != null) {
            bp.release();
        }
    }
 
    public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
        return bp.handleActivityResult(requestCode, resultCode, data);
    }
}
cs


이제 준비가 끝났으니 결제 정보를 보여주고, 결제를 하는 PurchaseActivity.java를 구현해보겠습니다.

PurchaseActivity.java

View 부분은 가격 표시에 필요한 뷰 두개,  누르면 구독 결제 또는 PRO 버전 결제하는 버튼 두개에 대해서

ButterKnife로 바인드 해두었습니다. 그리고 방금 만든 BillingManager를 필드에 선언해두었죠

OnCreate 부분에 BillingManager를 초기화 시켜줍시다. 

그리고 OnResume과 OnDestroy 부분도 빼먹지 않고 Override 해줍니다.

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class PurchaseActivity extends BaseActivity {
 
    @BindView(R.id.tv_price)
    TextView tvPrice;
    @BindView(R.id.tv_sub_price)
    TextView tvSubPrice;
 
    private BillingManager billingManager;
 
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_purchase);
        ButterKnife.bind(this);
 
        billingManager = new BillingManager(this).init(new BillingManager.BillingCallback() {
            @Override
            public void onPurchased(String productId) {
                if (productId.equals(Config.SUBSCRIBE_SKU)) {
                    Toast.makeText(mContext, "구독 해 주셔서 감사합니다.", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(mContext, "구매 해 주셔서 감사합니다.", Toast.LENGTH_SHORT).show();
                }
            }
 
            @Override
            public void onUpdatePrice(Pair<Double, Double> prices) {
                try {
                    tvPrice.setText(Util.priceWithoutDecimal(prices.first.intValue()));
                    tvSubPrice.setText(Util.priceWithoutDecimal(prices.second.intValue()));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
 
    @OnClick({ R.id.iv_back, R.id.ll_purchase, R.id.ll_subscribe })
    public void onClick(View v) {
        v.startAnimation(Animator.getClickAnimation(mContext));
        switch (v.getId()) {
            case R.id.iv_back:
                onBackPressed();
                break;
            case R.id.ll_subscribe:
                billingManager.subscribe();
                break;
            case R.id.ll_purchase:
                billingManager.purchase();
        }
    }
 
    @Override
    protected void onResume() {
        billingManager.onResume();
        super.onResume();
    }
 
    @Override
    protected void onDestroy() {
        billingManager.onDestroy();
        super.onDestroy();
    }
 
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (!billingManager.handleActivityResult(requestCode, resultCode, data)) {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }
}
 
cs

이제 릴리즈용 APK 또는 앱번들을 만들어서, 내부테스트/알파/베타 채널에 올려두고 구매 테스트를 직접 해보셔야합니다.

결제를 무료로 테스트를 하고 싶으시다면 개발자 계정 > 계정 세부정보 > 라이선스 테스트에 해당 GooglePlay 아이디를

등록하는것을 잊지마세요! 


그리고 테스트 구독의 경우 편의를 위해 1개월은 5분정도로 매우 짧게 시뮬레이션됩니다.

안되신다면 아래 사항들을 체크 해 보세요


- 구독 제품을 활성 APK로 돌리셨는지?

- 구글 라이선스키를 제대로 입력했는지?

- 제품 아이디에 오타 / 혼동은 없었는지?