@@ -981,6 +981,22 @@ ErrorCode SubscriptionController::processPlayMarketPurchase(const QString &userC
981981 QObject::connect (&watcher, &QFutureWatcher<QPair<bool , QString>>::finished, &waitLoop, &QEventLoop::quit);
982982
983983 QFuture<QPair<bool , QString>> future = QtConcurrent::run ([androidController, productId]() {
984+ // If the user already has an active "premium" subscription, upgrade/replace it with proration
985+ // instead of stacking a second, independent purchase that Google Play would just queue behind it.
986+ QString oldPurchaseToken;
987+ QJsonObject existingPurchasesResult = androidController->queryPurchases ();
988+ if (existingPurchasesResult.value (" responseCode" ).toInt (-1 ) == 0 ) {
989+ const QJsonArray existingPurchases = existingPurchasesResult.value (" purchases" ).toArray ();
990+ for (const QJsonValue &purchaseValue : existingPurchases) {
991+ const QJsonObject existingPurchase = purchaseValue.toObject ();
992+ if (existingPurchase.value (" purchaseState" ).toInt (-1 ) == 1 ) { // PURCHASED
993+ oldPurchaseToken = existingPurchase.value (" purchaseToken" ).toString ();
994+ qInfo () << " [Billing] Found existing active subscription, will upgrade instead of purchasing a new one" ;
995+ break ;
996+ }
997+ }
998+ }
999+
9841000 QJsonObject plansResult = androidController->getSubscriptionPlans ();
9851001 int responseCode = plansResult.value (" responseCode" ).toInt (-1 );
9861002 if (responseCode != 0 ) {
@@ -998,11 +1014,18 @@ ErrorCode SubscriptionController::processPlayMarketPurchase(const QString &userC
9981014 if (offer.value (" basePlanId" ).toString () != productId) continue ;
9991015
10001016 const QString token = offer.value (" offerToken" ).toString ();
1001- if (fallbackOfferToken.isEmpty ()) fallbackOfferToken = token;
1002-
10031017 QJsonArray pricingPhases = offer.value (" pricingPhases" ).toArray ();
10041018 const bool hasFreeTrial = !pricingPhases.isEmpty ()
10051019 && pricingPhases.first ().toObject ().value (" priceAmountMicros" ).toDouble () == 0 ;
1020+
1021+ // Google Play's subscription replacement API rejects switching to an offer with an
1022+ // introductory/trial phase ("Requested replacement mode is not supported for this
1023+ // request"), so an upgrade must always target the regular, non-trial offer - skip
1024+ // trial offers entirely here rather than just deprioritizing them.
1025+ if (hasFreeTrial && !oldPurchaseToken.isEmpty ()) continue ;
1026+
1027+ if (fallbackOfferToken.isEmpty ()) fallbackOfferToken = token;
1028+
10061029 if (hasFreeTrial) {
10071030 offerToken = token;
10081031 qInfo () << " [Billing] Found free trial offer for basePlanId:" << productId;
@@ -1016,7 +1039,9 @@ ErrorCode SubscriptionController::processPlayMarketPurchase(const QString &userC
10161039 qWarning () << " [Billing] No offer token found for basePlanId:" << productId;
10171040 return qMakePair (false , QString ());
10181041 }
1019- QJsonObject purchaseResult = androidController->purchaseSubscription (offerToken);
1042+ QJsonObject purchaseResult = oldPurchaseToken.isEmpty ()
1043+ ? androidController->purchaseSubscription (offerToken)
1044+ : androidController->upgradeSubscription (offerToken, oldPurchaseToken);
10201045 responseCode = purchaseResult.value (" responseCode" ).toInt (-1 );
10211046 if (responseCode != 0 ) {
10221047 qWarning () << " [Billing] Purchase failed, responseCode:" << responseCode;
0 commit comments