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? _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> GetSubscriptionPlansAsync( CancellationToken cancellationToken = default) { try { var details = await GetProductDetailsAsync(cancellationToken); return details?.GetSubscriptionOfferDetails()? .Where(item => string.IsNullOrEmpty(item.OfferId)) .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() .ToArray() ?? []; } catch (OperationCanceledException) { throw; } catch { return []; } } public async Task 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 PurchaseAsync(string basePlanId, CancellationToken cancellationToken = default) { try { var details = await GetProductDetailsAsync(cancellationToken); 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(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? 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 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)); } return result.ProductDetailsList.FirstOrDefault(); } 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 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; }