Files
LMEMappeCatastaliProApp/Platforms/Android/AndroidPlayBillingService.cs

277 lines
10 KiB
C#

using Android.BillingClient.Api;
using LMEMappeCatastaliApp.Services;
using Microsoft.Maui.ApplicationModel;
namespace LMEMappeCatastaliApp.Platforms.Android;
public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingService, IPurchasesUpdatedListener
{
private readonly SemaphoreSlim _connectionLock = new(1, 1);
private readonly BillingClient _client;
private TaskCompletionSource<PlayBillingResult>? _purchaseCompletion;
private string? _lastCatalogFailure;
public AndroidPlayBillingService()
{
var pendingPurchases = PendingPurchasesParams.NewBuilder()
.EnableOneTimeProducts()
.EnablePrepaidPlans()
.Build();
_client = BillingClient.NewBuilder(global::Android.App.Application.Context)
.SetListener(this)
.EnablePendingPurchases(pendingPurchases)
.EnableAutoServiceReconnection()
.Build();
}
public async Task<IReadOnlyList<PlaySubscriptionPlan>> GetSubscriptionPlansAsync(
CancellationToken cancellationToken = default)
{
try
{
var details = await GetProductDetailsAsync(cancellationToken);
return details?.GetSubscriptionOfferDetails()?
.Where(item => !string.IsNullOrWhiteSpace(item.BasePlanId)
&& !string.IsNullOrWhiteSpace(item.OfferToken))
.GroupBy(item => item.BasePlanId, StringComparer.Ordinal)
.Select(group => group
.OrderBy(item => string.IsNullOrEmpty(item.OfferId) ? 0 : 1)
.First())
.Select(item =>
{
var phase = item.PricingPhases.PricingPhaseList.LastOrDefault();
return phase is null
? null
: new PlaySubscriptionPlan(item.BasePlanId, phase.FormattedPrice, phase.BillingPeriod);
})
.Where(item => item is not null)
.Cast<PlaySubscriptionPlan>()
.ToArray() ?? [];
}
catch (OperationCanceledException)
{
throw;
}
catch
{
return [];
}
}
public async Task<PlayBillingResult> RestoreAsync(CancellationToken cancellationToken = default)
{
try
{
await EnsureConnectedAsync(cancellationToken);
var query = QueryPurchasesParams.NewBuilder()
.SetProductType(BillingClient.ProductType.Subs)
.Build();
var result = await _client.QueryPurchasesAsync(query);
if (result.Result.ResponseCode != BillingResponseCode.Ok)
{
return PlayBillingResult.Failed(ToMessage(result.Result));
}
var purchase = result.Purchases.FirstOrDefault(IsPurchasedProSubscription);
if (purchase is null)
{
return PlayBillingResult.Base("Nessun abbonamento LME Pro attivo trovato su Google Play.");
}
var acknowledgement = await AcknowledgeIfNeededAsync(purchase);
return acknowledgement ?? PlayBillingResult.Active(
"Abbonamento LME Pro ripristinato da Google Play.",
purchase.PurchaseToken);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception error)
{
return PlayBillingResult.Failed($"Google Play Billing non disponibile: {error.GetBaseException().Message}");
}
}
public async Task<PlayBillingResult> PurchaseAsync(string basePlanId, CancellationToken cancellationToken = default)
{
try
{
if (string.IsNullOrWhiteSpace(basePlanId))
{
return PlayBillingResult.Failed("Piano LME Pro non valido.");
}
var details = await GetProductDetailsAsync(cancellationToken);
var offer = details?.GetSubscriptionOfferDetails()?
.Where(item => item.BasePlanId == basePlanId
&& !string.IsNullOrWhiteSpace(item.OfferToken))
.OrderBy(item => string.IsNullOrEmpty(item.OfferId) ? 0 : 1)
.FirstOrDefault();
if (details is null)
{
return PlayBillingResult.Failed(_lastCatalogFailure
?? "Google Play non ha restituito l'abbonamento lme_pro.");
}
if (offer is null)
{
var availablePlans = string.Join(", ", details.GetSubscriptionOfferDetails()?
.Select(item => item.BasePlanId)
.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.Ordinal) ?? []);
return PlayBillingResult.Failed(string.IsNullOrWhiteSpace(availablePlans)
? $"Google Play ha restituito lme_pro senza offerte acquistabili per il piano {basePlanId}."
: $"Piano {basePlanId} non restituito da Google Play. Piani ricevuti: {availablePlans}.");
}
var productParams = BillingFlowParams.ProductDetailsParams.NewBuilder()
.SetProductDetails(details)
.SetOfferToken(offer.OfferToken)
.Build();
var flow = BillingFlowParams.NewBuilder()
.SetProductDetailsParamsList([productParams])
.Build();
var activity = Platform.CurrentActivity
?? throw new InvalidOperationException("Activity Android non disponibile.");
var completion = new TaskCompletionSource<PlayBillingResult>(TaskCreationOptions.RunContinuationsAsynchronously);
_purchaseCompletion = completion;
using var registration = cancellationToken.Register(() => completion.TrySetCanceled(cancellationToken));
var launchResult = await MainThread.InvokeOnMainThreadAsync(() =>
_client.LaunchBillingFlow(activity, flow));
if (launchResult.ResponseCode != BillingResponseCode.Ok)
{
_purchaseCompletion = null;
return PlayBillingResult.Failed(ToMessage(launchResult));
}
return await completion.Task;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception error)
{
_purchaseCompletion = null;
return PlayBillingResult.Failed($"Acquisto non avviato: {error.GetBaseException().Message}");
}
}
public async void OnPurchasesUpdated(BillingResult billingResult, IList<Purchase>? purchases)
{
var completion = Interlocked.Exchange(ref _purchaseCompletion, null);
if (completion is null)
{
return;
}
if (billingResult.ResponseCode == BillingResponseCode.UserCancelled)
{
completion.TrySetResult(PlayBillingResult.Cancelled("Acquisto annullato."));
return;
}
if (billingResult.ResponseCode != BillingResponseCode.Ok)
{
completion.TrySetResult(PlayBillingResult.Failed(ToMessage(billingResult)));
return;
}
var purchase = purchases?.FirstOrDefault(IsPurchasedProSubscription);
if (purchase is null)
{
completion.TrySetResult(PlayBillingResult.Failed("Acquisto non ancora completato."));
return;
}
var acknowledgement = await AcknowledgeIfNeededAsync(purchase);
completion.TrySetResult(acknowledgement ?? PlayBillingResult.Active(
"Abbonamento LME Pro attivato.",
purchase.PurchaseToken));
}
private async Task<ProductDetails?> GetProductDetailsAsync(CancellationToken cancellationToken)
{
await EnsureConnectedAsync(cancellationToken);
var product = QueryProductDetailsParams.Product.NewBuilder()
.SetProductId(EntitlementService.ProductId)
.SetProductType(BillingClient.ProductType.Subs)
.Build();
var query = QueryProductDetailsParams.NewBuilder()
.SetProductList([product])
.Build();
var result = await _client.QueryProductDetailsAsync(query);
if (result.Result.ResponseCode != BillingResponseCode.Ok)
{
throw new InvalidOperationException(ToMessage(result.Result));
}
// The .NET async wrapper copies the Java result into the managed
// ProductDetails property; ProductDetailsList has no Java handle here.
var details = result.ProductDetails?.FirstOrDefault();
if (details is not null)
{
_lastCatalogFailure = null;
return details;
}
_lastCatalogFailure = "Google Play ha restituito un catalogo vuoto per lme_pro. Verifica account Play Store, adesione al test e origine dell'installazione.";
return null;
}
private async Task EnsureConnectedAsync(CancellationToken cancellationToken)
{
if (_client.IsReady)
{
return;
}
await _connectionLock.WaitAsync(cancellationToken);
try
{
if (_client.IsReady)
{
return;
}
var result = await _client.StartConnectionAsync();
if (result.ResponseCode != BillingResponseCode.Ok)
{
throw new InvalidOperationException(ToMessage(result));
}
}
finally
{
_connectionLock.Release();
}
}
private async Task<PlayBillingResult?> AcknowledgeIfNeededAsync(Purchase purchase)
{
if (purchase.IsAcknowledged)
{
return null;
}
var parameters = AcknowledgePurchaseParams.NewBuilder()
.SetPurchaseToken(purchase.PurchaseToken)
.Build();
var result = await _client.AcknowledgePurchaseAsync(parameters);
return result.ResponseCode == BillingResponseCode.Ok
? null
: PlayBillingResult.Failed($"Acquisto trovato ma non confermato: {ToMessage(result)}");
}
private static bool IsPurchasedProSubscription(Purchase purchase) =>
purchase.PurchaseState == PurchaseState.Purchased
&& purchase.Products.Contains(EntitlementService.ProductId);
private static string ToMessage(BillingResult result) =>
string.IsNullOrWhiteSpace(result.DebugMessage)
? $"Google Play Billing: {result.ResponseCode}."
: result.DebugMessage;
}