Files
LMEMappeCatastaliApp/Platforms/Android/AndroidPlayBillingService.cs

202 lines
7.5 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;
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<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.");
}
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
{
await EnsureConnectedAsync(cancellationToken);
var product = QueryProductDetailsParams.Product.NewBuilder()
.SetProductId(EntitlementService.ProductId)
.SetProductType(BillingClient.ProductType.Subs)
.Build();
var query = QueryProductDetailsParams.NewBuilder()
.SetProductList([product])
.Build();
var detailsResult = await _client.QueryProductDetailsAsync(query);
if (detailsResult.Result.ResponseCode != BillingResponseCode.Ok)
{
return PlayBillingResult.Failed(ToMessage(detailsResult.Result));
}
var details = detailsResult.ProductDetailsList.FirstOrDefault();
var offer = details?.GetSubscriptionOfferDetails()?
.FirstOrDefault(item => item.BasePlanId == basePlanId && string.IsNullOrEmpty(item.OfferId));
if (details is null || offer is null)
{
return PlayBillingResult.Failed("Piano LME Pro non disponibile. Verifica prodotto e base plan nella Play Console.");
}
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 = _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."));
}
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;
}