Reputation: 1
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