202 lines
7.5 KiB
C#
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;
|
|
}
|