feat: prepare Pro licensing and Android API 36
This commit is contained in:
1 parent
b600412285
commit
11a91863a3
25 files changed
+2447
-62
No files matched your search
@@ -0,0 +1,201 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user