안드로이드 – 인앱 구독 서비스 구현하기 (Java)

By | 2024년 12월 30일
Table of Contents

안드로이드 – 인앱 구독 서비스 구현하기 (Java)

구글 플레이에서의 구독취소 정보를 서버로 전송하는 부분은 누락되어 있습니다.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

SubscriptionViewModel 구현

코맨트 버전

public class SubscriptionViewModel extends AndroidViewModel {

    /**
     * ViewModel 생성자
     * BillingClient를 초기화하고 Google Play 결제 서비스에 연결을 시작합니다.
     * 
     * @param application Application 컨텍스트
     */
    public SubscriptionViewModel(@NonNull Application application) {
        super(application);
        billingClient = BillingClient.newBuilder(application)
                .setListener(purchasesUpdatedListener)
                .enablePendingPurchases()
                .build();

        connectToBillingClient();
    }

    /**
     * Google Play Billing 서비스에 연결을 시도합니다.
     * 연결이 성공하면 상품 조회와 구독 상태 확인을 수행합니다.
     * 연결이 실패하면 재연결을 시도합니다.
     */
    private void connectToBillingClient() {
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    queryAvailableProducts();
                    checkSubscriptionStatus();
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                connectToBillingClient();
            }
        });
    }

    /**
     * Google Play Store에서 제공하는 구독 상품 정보를 조회합니다.
     * 월간 구독과 연간 구독 상품의 정보를 가져옵니다.
     * 조회된 상품 정보는 monthlySubscription과 yearlySubscription 변수에 저장됩니다.
     */
    private void queryAvailableProducts() {
        List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
        productList.add(
                QueryProductDetailsParams.Product.newBuilder()
                        .setProductId("monthly_sub")
                        .setProductType(BillingClient.ProductType.SUBS)
                        .build()
        );
        // ... 구현 내용 ...
    }

    /**
     * 현재 사용자의 구독 상태를 확인합니다.
     * 구매 내역을 조회하여 유효한 구독이 있는지 확인하고,
     * 구독 상태를 LiveData를 통해 UI에 반영합니다.
     */
    private void checkSubscriptionStatus() {
        billingClient.queryPurchasesAsync(
                QueryPurchasesParams.newBuilder()
                        .setProductType(BillingClient.ProductType.SUBS)
                        .build(),
                (billingResult, purchases) -> {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        verifySubscriptionStatus(purchases);
                    }
                }
        );
    }

    /**
     * 구매 내역을 검증하고 구독 상태를 업데이트합니다.
     * PURCHASED 상태의 구매 건을 처리하고 구독 상태를 LiveData에 반영합니다.
     * 
     * @param purchases 구매 내역 리스트
     */
    private void verifySubscriptionStatus(List<Purchase> purchases) {
        boolean hasValidSubscription = false;
        for (Purchase purchase : purchases) {
            if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
                hasValidSubscription = true;
                handlePurchase(purchase);
            }
        }
        isSubscribed.postValue(hasValidSubscription);
    }

    /**
     * 구독 구매 플로우를 시작합니다.
     * 선택된 상품(월간/연간)의 구매 화면을 실행합니다.
     * 
     * @param activity 현재 Activity 컨텍스트
     * @param productId 구매할 상품 ID ("monthly_sub" 또는 "yearly_sub")
     */
    public void purchaseSubscription(Activity activity, String productId) {
        ProductDetails productDetails = "monthly_sub".equals(productId) 
                ? monthlySubscription : yearlySubscription;
        // ... 구현 내용 ...
    }

    /**
     * 구매 완료된 상품을 처리합니다.
     * 구매 확인(Acknowledge)을 수행하고, 서버에 구독 정보를 전송합니다.
     * 
     * @param purchase 구매 완료된 상품 정보
     */
    private void handlePurchase(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder()
                        .setPurchaseToken(purchase.getPurchaseToken())
                        .build();
                // ... 구현 내용 ...
            }
        }
    }

    /**
     * Google Play Billing의 구매 업데이트 리스너
     * 구매 상태가 변경될 때마다 호출되어 구매 처리를 수행합니다.
     */
    private final PurchasesUpdatedListener purchasesUpdatedListener = 
            (billingResult, purchases) -> {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK 
                && purchases != null) {
            for (Purchase purchase : purchases) {
                handlePurchase(purchase);
            }
            checkSubscriptionStatus();
        }
    };

    /**
     * 현재 구독 상태를 반환하는 LiveData를 제공합니다.
     * UI에서 이 LiveData를 관찰하여 구독 상태 변경을 감지할 수 있습니다.
     * 
     * @return 구독 상태를 나타내는 LiveData
     */
    public LiveData<Boolean> getIsSubscribed() {
        return isSubscribed;
    }

    /**
     * ViewModel이 제거될 때 호출됩니다.
     * BillingClient 연결을 종료하고 리소스를 정리합니다.
     */
    @Override
    protected void onCleared() {
        super.onCleared();
        billingClient.endConnection();
    }
}

전체소스코드

public class SubscriptionViewModel extends AndroidViewModel {
    private static final String TAG = "SubscriptionViewModel";
    private final BillingClient billingClient;
    private final MutableLiveData<Boolean> isSubscribed = new MutableLiveData<>(false);
    private ProductDetails monthlySubscription;
    private ProductDetails yearlySubscription;

    public SubscriptionViewModel(@NonNull Application application) {
        super(application);

        billingClient = BillingClient.newBuilder(application)
                .setListener(purchasesUpdatedListener)
                .enablePendingPurchases()
                .build();

        connectToBillingClient();
    }

    private void connectToBillingClient() {
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    queryAvailableProducts();
                    checkSubscriptionStatus();
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // 재연결 로직
                connectToBillingClient();
            }
        });
    }

    private void queryAvailableProducts() {
        List<QueryProductDetailsParams.Product> productList = new ArrayList<>();

        // 월간 구독
        productList.add(
                QueryProductDetailsParams.Product.newBuilder()
                        .setProductId("monthly_sub")
                        .setProductType(BillingClient.ProductType.SUBS)
                        .build()
        );

        // 연간 구독
        productList.add(
                QueryProductDetailsParams.Product.newBuilder()
                        .setProductId("yearly_sub")
                        .setProductType(BillingClient.ProductType.SUBS)
                        .build()
        );

        QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
                .setProductList(productList)
                .build();

        billingClient.queryProductDetailsAsync(
                params,
                (billingResult, productDetailsList) -> {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        for (ProductDetails productDetails : productDetailsList) {
                            if ("monthly_sub".equals(productDetails.getProductId())) {
                                monthlySubscription = productDetails;
                            } else if ("yearly_sub".equals(productDetails.getProductId())) {
                                yearlySubscription = productDetails;
                            }
                        }
                    }
                }
        );
    }

    private void checkSubscriptionStatus() {
        billingClient.queryPurchasesAsync(
                QueryPurchasesParams.newBuilder()
                        .setProductType(BillingClient.ProductType.SUBS)
                        .build(),
                (billingResult, purchases) -> {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        verifySubscriptionStatus(purchases);
                    }
                }
        );
    }

    private void verifySubscriptionStatus(List<Purchase> purchases) {
        boolean hasValidSubscription = false;

        for (Purchase purchase : purchases) {
            if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
                hasValidSubscription = true;
                handlePurchase(purchase);
            }
        }

        isSubscribed.postValue(hasValidSubscription);
    }

    public void purchaseSubscription(Activity activity, String productId) {
        ProductDetails productDetails = "monthly_sub".equals(productId) 
                ? monthlySubscription : yearlySubscription;

        if (productDetails != null) {
            List<BillingFlowParams.ProductDetailsParams> productDetailsParamsList = 
                    List.of(
                            BillingFlowParams.ProductDetailsParams.newBuilder()
                                    .setProductDetails(productDetails)
                                    .setOfferToken(productDetails.getSubscriptionOfferDetails()
                                            .get(0).getOfferToken())
                                    .build()
                    );

            BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
                    .setProductDetailsParamsList(productDetailsParamsList)
                    .build();

            billingClient.launchBillingFlow(activity, billingFlowParams);
        }
    }

    private void handlePurchase(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder()
                        .setPurchaseToken(purchase.getPurchaseToken())
                        .build();

                billingClient.acknowledgePurchase(params, billingResult -> {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        // 서버에 구독 정보 전송
                        sendSubscriptionToServer(purchase);
                    }
                });
            }
        }
    }

    private final PurchasesUpdatedListener purchasesUpdatedListener = 
            (billingResult, purchases) -> {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK 
                && purchases != null) {
            for (Purchase purchase : purchases) {
                handlePurchase(purchase);
            }
            checkSubscriptionStatus();
        }
    };

    public LiveData<Boolean> getIsSubscribed() {
        return isSubscribed;
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        billingClient.endConnection();
    }
}

SubscriptionActivity

public class SubscriptionActivity extends AppCompatActivity {
    private SubscriptionViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_subscription);

        viewModel = new ViewModelProvider(this).get(SubscriptionViewModel.class);

        // 구독 상태 관찰
        viewModel.getIsSubscribed().observe(this, isSubscribed -> {
            if (isSubscribed) {
                // 구독 상태에 따른 UI 업데이트
                showSubscribedUI();
            } else {
                showUnsubscribedUI();
            }
        });

        // 구독 버튼 클릭 리스너
        findViewById(R.id.btnSubscribeMonthly).setOnClickListener(v -> 
                viewModel.purchaseSubscription(this, "monthly_sub"));

        findViewById(R.id.btnSubscribeYearly).setOnClickListener(v -> 
                viewModel.purchaseSubscription(this, "yearly_sub"));
    }
}

서버 통신

public class SubscriptionApiService {
    private final String baseUrl = "your_server_url";
    private final OkHttpClient client = new OkHttpClient();

    public void sendSubscriptionToServer(Purchase purchase) {
        JSONObject json = new JSONObject();
        try {
            json.put("purchaseToken", purchase.getPurchaseToken());
            json.put("productId", purchase.getProducts().get(0));
            json.put("purchaseTime", purchase.getPurchaseTime());

            RequestBody body = RequestBody.create(
                    MediaType.parse("application/json"), 
                    json.toString()
            );

            Request request = new Request.Builder()
                    .url(baseUrl + "/subscription/verify")
                    .post(body)
                    .build();

            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(@NonNull Call call, @NonNull IOException e) {
                    Log.e("SubscriptionApi", "Error sending subscription data", e);
                }

                @Override
                public void onResponse(@NonNull Call call, @NonNull Response response) {
                    // 응답 처리
                }
            });
        } catch (JSONException e) {
            Log.e("SubscriptionApi", "Error creating JSON", e);
        }
    }
}

답글 남기기