Tolga Kurt
Tolga Kurt

Reputation: 1

How to add payment in subscription and product sales in my application?

This is productmanager.java class.

package com.eduplay.voicechanger.Manager;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

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.ProductDetails;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.eduplay.voicechanger.R;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ProductManager implements PurchasesUpdatedListener {

    private BillingClient billingClient;
    private final Context context;
    private final Map<String, ProductDetails> productDetailsMap = new HashMap<>();
    private boolean isBillingClientReady = false;
    private boolean isProductDetailsLoaded = false;
    private static final String YEARLY_PRODUCT_ID = "yearly_premium";
    private static final String MONTHLY_PRODUCT_ID = "monthly_premium";
    private static final String WEEKLY_PRODUCT_ID = "weekly_premium";
    private static final String[] CREDIT_PRODUCT_IDS = {
            "credits_1000", "credits_2000", "credits_3000", "credits_5000", "credits_10000",
            "credits_25000", "credits_50000", "credits_100000", "credits_200000", "credits_500000"
    };

    public interface PurchaseCallback {
        void onPurchaseCompleted(boolean isSuccess);
    }

    public interface ProductDetailsCallback {
        void onProductDetailsResponse(List<ProductDetails> productDetailsList);
    }

    public ProductManager(Context context) {
        this.context = context;
        initializeBillingClient();
    }

    private void initializeBillingClient() {
        billingClient = BillingClient.newBuilder(context)
                .enablePendingPurchases()
                .setListener(this)
                .build();

        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingServiceDisconnected() {
                isBillingClientReady = false;
            }

            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    isBillingClientReady = true;
                    Log.d("BillingClient", "BillingClient setup finished successfully");
                    refreshProductDetails();  // Ürünleri ve abonelikleri güncelle
                } else {
                    retryBillingConnection(this);
                }
            }
        });
    }

    private void retryBillingConnection(BillingClientStateListener listener) {
        billingClient.endConnection();
        billingClient.startConnection(listener);
        Log.d("BillingClient", "Retrying connection to BillingClient...");
    }

    // Ürün ve abonelik bilgilerini güncelleme metodu
    public void refreshProductDetails() {
        if (!isBillingClientReady) {
            initializeBillingClient();
            Log.d("BillingClient", "BillingClient initializing in refreshProductDetails");
        } else {
            queryCreditProductDetails();
            querySubscriptionProductDetails(response -> {
                Log.d("BillingClient", "Subscription products updated");
            });
        }
    }

    public void queryCreditProductDetails() {
        if (!isBillingClientReady) {
            return;
        }
        List<QueryProductDetailsParams.Product> creditProductList = new ArrayList<>();
        for (String productId : CREDIT_PRODUCT_IDS) {
            creditProductList.add(QueryProductDetailsParams.Product.newBuilder()
                    .setProductId(productId)
                    .setProductType(BillingClient.ProductType.INAPP)
                    .build());
        }

        QueryProductDetailsParams creditParams = QueryProductDetailsParams.newBuilder()
                .setProductList(creditProductList)
                .build();

        billingClient.queryProductDetailsAsync(creditParams, (billingResult, productDetailsList) -> {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && productDetailsList != null) {
                productDetailsMap.clear();
                for (ProductDetails details : productDetailsList) {
                    productDetailsMap.put(details.getProductId(), details);
                }
                isProductDetailsLoaded = true;
                Intent intent = new Intent("com.eduplay.voicechanger.PRODUCT_DETAILS_UPDATED");
                LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
            }
        });
    }

    public void querySubscriptionProductDetails(final ProductDetailsCallback callback) {
        List<QueryProductDetailsParams.Product> subProductList = new ArrayList<>();
        subProductList.add(QueryProductDetailsParams.Product.newBuilder()
                .setProductId(YEARLY_PRODUCT_ID)
                .setProductType(BillingClient.ProductType.SUBS)
                .build());
        subProductList.add(QueryProductDetailsParams.Product.newBuilder()
                .setProductId(MONTHLY_PRODUCT_ID)
                .setProductType(BillingClient.ProductType.SUBS)
                .build());
        subProductList.add(QueryProductDetailsParams.Product.newBuilder()
                .setProductId(WEEKLY_PRODUCT_ID)
                .setProductType(BillingClient.ProductType.SUBS)
                .build());

        QueryProductDetailsParams subParams = QueryProductDetailsParams.newBuilder()
                .setProductList(subProductList)
                .build();

        billingClient.queryProductDetailsAsync(subParams, (billingResult, productDetailsList) -> {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && productDetailsList != null) {
                for (ProductDetails details : productDetailsList) {
                    productDetailsMap.put(details.getProductId(), details);
                }
                callback.onProductDetailsResponse(productDetailsList);
            } else {
                callback.onProductDetailsResponse(new ArrayList<>());
            }
        });
    }

    public List<ProductDetails> getCachedCreditProducts() {
        if (!isProductDetailsLoaded || productDetailsMap.isEmpty()) {
            return new ArrayList<>();
        }
        return new ArrayList<>(productDetailsMap.values());
    }

    public void purchaseCreditProduct(Activity activity, String productId, PurchaseCallback callback) {
        if (!isBillingClientReady) {
            initializeBillingClient();
            Toast.makeText(context, context.getString(R.string.toast_reconnecting), Toast.LENGTH_SHORT).show();
            return;
        }

        ProductDetails productDetails = productDetailsMap.get(productId);
        if (productDetails == null) {
            Toast.makeText(context, context.getString(R.string.toast_product_not_loaded), Toast.LENGTH_SHORT).show();
            callback.onPurchaseCompleted(false);
            return;
        }

        List<BillingFlowParams.ProductDetailsParams> productDetailsParamsList = new ArrayList<>();
        productDetailsParamsList.add(BillingFlowParams.ProductDetailsParams.newBuilder()
                .setProductDetails(productDetails)
                .build());

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

        BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
        if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
            callback.onPurchaseCompleted(false);
        }
    }

    public void purchaseSubscriptionOffer(Activity activity, String offerToken, PurchaseCallback callback) {
        ProductDetails productDetails = null;
        for (ProductDetails details : productDetailsMap.values()) {
            List<ProductDetails.SubscriptionOfferDetails> offerDetailsList = details.getSubscriptionOfferDetails();
            if (offerDetailsList != null) {
                for (ProductDetails.SubscriptionOfferDetails offer : offerDetailsList) {
                    if (offer.getOfferToken().equals(offerToken)) {
                        productDetails = details;
                        break;
                    }
                }
            }
        }

        if (productDetails == null) {
            Toast.makeText(context, context.getString(R.string.toast_subscription_not_found), Toast.LENGTH_SHORT).show();
            callback.onPurchaseCompleted(false);
            return;
        }

        BillingFlowParams.ProductDetailsParams productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
                .setProductDetails(productDetails)
                .setOfferToken(offerToken)
                .build();

        BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
                .setProductDetailsParamsList(List.of(productDetailsParams))
                .build();

        BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
        if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
            callback.onPurchaseCompleted(false);
        }
    }

    @Override
    public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
        Log.d("PurchaseFlow", "onPurchasesUpdated called with response code: " + billingResult.getResponseCode());

        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            Log.d("PurchaseFlow", "Purchase successful with purchases: " + purchases);
            for (Purchase purchase : purchases) {
                handlePurchase(purchase);

                if (isSubscriptionPurchase(purchase)) {
                    int bonusCredits = determineBonusCredits(purchase.getProducts());
                    CreditsManager creditsManager = new CreditsManager(context);
                    creditsManager.addCredits(bonusCredits);

                    Intent intent = new Intent("com.eduplay.voicechanger.CREDITS_UPDATED");
                    LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
                    Toast.makeText(context, String.format(context.getString(R.string.toast_subscription_success), bonusCredits), Toast.LENGTH_SHORT).show();

                    Log.d("PurchaseFlow", "Subscription purchase successful, bonus credits added: " + bonusCredits);
                }
            }
        } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
            Log.d("PurchaseFlow", "Purchase canceled by user");
            Toast.makeText(context, context.getString(R.string.purchase_canceled), Toast.LENGTH_SHORT).show();
        } else {
            Log.e("PurchaseFlow", "Purchase failed with response code: " + billingResult.getResponseCode());
            Toast.makeText(context, context.getString(R.string.purchase_failed), Toast.LENGTH_SHORT).show();
        }
    }

    private boolean isSubscriptionPurchase(Purchase purchase) {
        for (String productId : purchase.getProducts()) {
            if (productId.equals(YEARLY_PRODUCT_ID) || productId.equals(MONTHLY_PRODUCT_ID) || productId.equals(WEEKLY_PRODUCT_ID)) {
                return true;
            }
        }
        return false;
    }

    private int determineBonusCredits(List<String> productIds) {
        for (String productId : productIds) {
            if (productId.equals(YEARLY_PRODUCT_ID)) {
                return 100000;
            } else if (productId.equals(MONTHLY_PRODUCT_ID)) {
                return 8000;
            } else if (productId.equals(WEEKLY_PRODUCT_ID)) {
                return 1500;
            }
        }
        return 0;
    }

    private void handlePurchase(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            List<String> productIds = purchase.getProducts();
            int purchaseQuantity = purchase.getQuantity();
            int totalCreditsToAdd = 0;

            for (String productId : productIds) {
                int creditsPerProduct = getCreditsAmountForProduct(productId);
                totalCreditsToAdd += creditsPerProduct * purchaseQuantity;
            }

            if (totalCreditsToAdd > 0) {
                CreditsManager creditsManager = new CreditsManager(context);
                creditsManager.addCredits(totalCreditsToAdd);

                ConsumeParams consumeParams = ConsumeParams.newBuilder()
                        .setPurchaseToken(purchase.getPurchaseToken())
                        .build();

                ConsumeResponseListener consumeListener = (billingResult, purchaseToken) -> {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    }
                };

                billingClient.consumeAsync(consumeParams, consumeListener);

                Intent intent = new Intent("com.eduplay.voicechanger.CREDITS_UPDATED");
                LocalBroadcastManager.getInstance(context).sendBroadcast(intent);

                Toast.makeText(context, totalCreditsToAdd + " " + context.getString(R.string.credits_added), Toast.LENGTH_SHORT).show();
            }
        }
    }

    private int getCreditsAmountForProduct(String productId) {
        switch (productId) {
            case "credits_1000": return 1000;
            case "credits_2000": return 2000;
            case "credits_3000": return 3000;
            case "credits_5000": return 5000;
            case "credits_10000": return 10000;
            case "credits_25000": return 25000;
            case "credits_50000": return 50000;
            case "credits_100000": return 100000;
            case "credits_200000": return 200000;
            case "credits_500000": return 500000;
            default: return 0;
        }
    }
}

package com.eduplay.voicechanger.SheetFragment;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.eduplay.voicechanger.Activity.MainActivity;
import com.eduplay.voicechanger.Manager.ProductManager;
import com.eduplay.voicechanger.R;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.ProductDetails.SubscriptionOfferDetails;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;

public class SubscriptionBottomSheetFragment extends BottomSheetDialogFragment {

    private static final String PREFS_NAME = "SubscriptionPrefs";
    private static final String KEY_SUBSCRIPTION_TYPE = "subscriptionType";
    private static final String KEY_SUBSCRIPTION_END_DATE = "subscriptionEndDate";
    private static final String KEY_SUBSCRIPTION_START_DATE = "subscriptionStartDate";

    private TextView bonusCreditsTextView;
    private ProductManager productManager;
    private LinearLayout selectedOption;
    private SubscriptionOfferDetails yearlyOfferDetails, monthlyOfferDetails, weeklyOfferDetails;

    public static SubscriptionBottomSheetFragment newInstance() {
        return new SubscriptionBottomSheetFragment();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.subscription_bottom_sheet, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        productManager = ((MainActivity) requireActivity()).getProductManager();


        ImageView closeButton = view.findViewById(R.id.close_button);
        Button continueButton = view.findViewById(R.id.continue_button);
        closeButton.setOnClickListener(v -> dismiss());

        LinearLayout yearlyOption = view.findViewById(R.id.yearly_option);
        LinearLayout monthlyOption = view.findViewById(R.id.monthly_option);
        LinearLayout weeklyOption = view.findViewById(R.id.weekly_option);

        yearlyOption.setOnClickListener(v -> selectOption(yearlyOption));
        monthlyOption.setOnClickListener(v -> selectOption(monthlyOption));
        weeklyOption.setOnClickListener(v -> selectOption(weeklyOption));

        continueButton.setText(getString(R.string.continue_button_text));
        continueButton.setOnClickListener(v -> purchaseSelectedOption());
        bonusCreditsTextView = view.findViewById(R.id.bonus_credits_text_view);
        yearlyOption.setOnClickListener(v -> {
            selectOption(yearlyOption);
            updateBonusCreditsTextView(100000);
        });

        monthlyOption.setOnClickListener(v -> {
            selectOption(monthlyOption);
            updateBonusCreditsTextView(8000);
        });

        weeklyOption.setOnClickListener(v -> {
            selectOption(weeklyOption);
            updateBonusCreditsTextView(1500);
        });
        displaySubscriptionDetails();
        fetchSubscriptionDetails();
    }

    private void fetchSubscriptionDetails() {
        productManager.querySubscriptionProductDetails(subscriptionDetailsList -> {
            if (subscriptionDetailsList != null && !subscriptionDetailsList.isEmpty()) {
                updateSubscriptionUIWithDetails(subscriptionDetailsList);
            } else {
                Toast.makeText(requireContext(), getString(R.string.error_loading_subscriptions), Toast.LENGTH_SHORT).show();
            }
        });
    }

    private void updateBonusCreditsTextView(int bonusCredits) {
        String bonusText = getString(R.string.bonus_credits_text, bonusCredits);
        bonusCreditsTextView.setText(bonusText);
    }
    private String calculateWeeklyCost(double totalPrice, int periodWeeks) {
        double weeklyCost = totalPrice / periodWeeks;
        return String.format(Locale.getDefault(), "%.2f %s", weeklyCost, getString(R.string.per_week));
    }

    private void updateSubscriptionUIWithDetails(List<ProductDetails> subscriptionDetailsList) {
        if (getView() == null) return;

        for (ProductDetails productDetails : subscriptionDetailsList) {
            List<SubscriptionOfferDetails> offerDetailsList = productDetails.getSubscriptionOfferDetails();
            for (SubscriptionOfferDetails offerDetails : offerDetailsList) {
                String billingPeriod = offerDetails.getPricingPhases().getPricingPhaseList().get(0).getBillingPeriod();
                String price = offerDetails.getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
                double priceAmount = offerDetails.getPricingPhases().getPricingPhaseList().get(0).getPriceAmountMicros() / 1_000_000.0;

                if (billingPeriod.equals("P1Y")) {
                    yearlyOfferDetails = offerDetails;
                    TextView yearlyPriceView = getView().findViewById(R.id.yearly_price);
                    if (yearlyPriceView != null) yearlyPriceView.setText(price);

                    String weeklyCost = calculateWeeklyCost(priceAmount, 52);
                    TextView yearlyWeeklyCostView = getView().findViewById(R.id.yearly_weekly_cost);
                    if (yearlyWeeklyCostView != null) yearlyWeeklyCostView.setText(weeklyCost);
                } else if (billingPeriod.equals("P1M")) {
                    monthlyOfferDetails = offerDetails;
                    TextView monthlyPriceView = getView().findViewById(R.id.monthly_price);
                    if (monthlyPriceView != null) monthlyPriceView.setText(price);

                    String weeklyCost = calculateWeeklyCost(priceAmount, 4);
                    TextView monthlyWeeklyCostView = getView().findViewById(R.id.monthly_weekly_cost);
                    if (monthlyWeeklyCostView != null) monthlyWeeklyCostView.setText(weeklyCost);
                } else if (billingPeriod.equals("P1W")) {
                    weeklyOfferDetails = offerDetails;
                    TextView weeklyPriceView = getView().findViewById(R.id.weekly_price);
                    if (weeklyPriceView != null) weeklyPriceView.setText(price);

                    TextView weeklyWeeklyCostView = getView().findViewById(R.id.weekly_weekly_cost);
                    if (weeklyWeeklyCostView != null) weeklyWeeklyCostView.setText(price + " " + getString(R.string.per_week));
                }
            }
        }
    }

    private void selectOption(LinearLayout option) {
        if (selectedOption != null) {
            selectedOption.setBackgroundResource(R.drawable.subscription_options_background);
        }
        selectedOption = option;
        selectedOption.setBackgroundResource(R.drawable.subscription_option_pressed);
    }

    private void purchaseSelectedOption() {
        if (selectedOption == null) {
            Toast.makeText(getContext(), getString(R.string.select_subscription_prompt), Toast.LENGTH_SHORT).show();
            Log.d("Subscription", "No subscription option selected");
            return;
        }

        String offerToken = null;
        String subscriptionType;
        Calendar endDate = Calendar.getInstance();

        if (selectedOption.getId() == R.id.yearly_option && yearlyOfferDetails != null) {
            offerToken = yearlyOfferDetails.getOfferToken();
            subscriptionType = getString(R.string.yearly_subscription);
            endDate.add(Calendar.YEAR, 1);
            Log.d("Subscription", "Yearly subscription selected with offer token: " + offerToken);
        } else if (selectedOption.getId() == R.id.monthly_option && monthlyOfferDetails != null) {
            offerToken = monthlyOfferDetails.getOfferToken();
            subscriptionType = getString(R.string.monthly_subscription);
            endDate.add(Calendar.MONTH, 1);
            Log.d("Subscription", "Monthly subscription selected with offer token: " + offerToken);
        } else if (selectedOption.getId() == R.id.weekly_option && weeklyOfferDetails != null) {
            offerToken = weeklyOfferDetails.getOfferToken();
            subscriptionType = getString(R.string.weekly_subscription);
            endDate.add(Calendar.WEEK_OF_YEAR, 1);
            Log.d("Subscription", "Weekly subscription selected with offer token: " + offerToken);
        } else {
            subscriptionType = "";
            Log.d("Subscription", "Subscription details not loaded or no valid offer token found");
        }

        if (offerToken != null) {
            Log.d("Subscription", "Attempting to start purchase flow");
            productManager.purchaseSubscriptionOffer(requireActivity(), offerToken, isSuccess -> {
                if (isSuccess) {
                    Log.d("Subscription", "Purchase successful, saving subscription details");
                    saveSubscriptionDetails(subscriptionType, endDate.getTimeInMillis());
                    displaySubscriptionDetails();
                } else {
                    Log.e("Subscription", "Purchase failed");
                    Toast.makeText(getContext(), getString(R.string.purchase_failed), Toast.LENGTH_SHORT).show();
                }
            });
        } else {
            Log.e("Subscription", "Offer token is null, cannot proceed with purchase");
            Toast.makeText(getContext(), getString(R.string.subscription_details_not_loaded), Toast.LENGTH_SHORT).show();
        }
    }


    private void displaySubscriptionDetails() {
        SharedPreferences preferences = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
        String subscriptionType = preferences.getString(KEY_SUBSCRIPTION_TYPE, getString(R.string.no_subscription));
        long endDateMillis = preferences.getLong(KEY_SUBSCRIPTION_END_DATE, -1);
        long startDateMillis = preferences.getLong(KEY_SUBSCRIPTION_START_DATE, -1);
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault());
        String subscriptionInfo = getString(R.string.subscription_type, subscriptionType);

        if (startDateMillis != -1 && endDateMillis != -1) {
            String startDate = dateFormat.format(new Date(startDateMillis));
            String endDate = dateFormat.format(new Date(endDateMillis));
            subscriptionInfo += getString(R.string.subscription_period, startDate, endDate);
        } else {
            subscriptionInfo += "\n" + getString(R.string.no_active_subscription);
        }
        TextView subscriptionDetailsTextView = requireView().findViewById(R.id.subscription_details_text_view);
        subscriptionDetailsTextView.setText(subscriptionInfo);
    }

    private void saveSubscriptionDetails(String subscriptionType, long endDateMillis) {
        SharedPreferences preferences = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = preferences.edit();

        editor.putString(KEY_SUBSCRIPTION_TYPE, subscriptionType);
        editor.putLong(KEY_SUBSCRIPTION_END_DATE, endDateMillis);
        editor.putLong(KEY_SUBSCRIPTION_START_DATE, System.currentTimeMillis());

        editor.apply();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

When using my application in a closed test version, the payment screen is displayed on my test device and the payment happens successfully. Successful payment

However, when using my application in an open test version, I get the error displayed in the following screenshot Failing Payment. This happens when removing my application from the test version.

I don't understand why it behaves like this.

Upvotes: 0

Views: 24

Answers (0)

Related Questions