namespace LMEMappeCatastaliApp.Services; public sealed class EntitlementService( ManualLicenseService manualLicenses, IPlayBillingService playBilling, PlayPurchaseVerificationService purchaseVerification) { public const string ProductId = "lme_pro"; public const string MonthlyBasePlanId = "monthly"; public const string YearlyBasePlanId = "yearly"; public const int BaseDrawingObjectLimit = 15; private const string GoogleProActivePreference = "entitlement_google_pro_active"; private const string GoogleExpiresAtPreference = "entitlement_google_pro_expires_at"; private const string GoogleLastVerifiedAtPreference = "entitlement_google_pro_last_verified_at"; private static readonly TimeSpan OfflineGracePeriod = TimeSpan.FromDays(7); private static readonly HashSet ProFeatures = new(StringComparer.Ordinal) { AppFeatures.ProjectArchive, AppFeatures.Export, AppFeatures.CloudSync, AppFeatures.OfflineMaps, AppFeatures.AdvancedGeometry, AppFeatures.GpsTrail, AppFeatures.FiducialPoints }; public bool IsGoogleProActive { get; private set; } public DateTimeOffset? GoogleExpiresAt { get; private set; } public DateTimeOffset? GoogleLastVerifiedAt { get; private set; } public ManualLicenseInfo? ManualLicense => manualLicenses.Current; public string? ManualLicenseError => manualLicenses.LastError; public bool HasGoogleProAccess { get { var now = DateTimeOffset.UtcNow; if (!IsGoogleProActive || GoogleExpiresAt is { } expiresAt && expiresAt <= now) { return false; } return GoogleLastVerifiedAt is null || now - GoogleLastVerifiedAt <= OfflineGracePeriod; } } public bool HasManualProAccess => manualLicenses.HasActiveLicense; public bool HasProAccess => HasGoogleProAccess || HasManualProAccess; public string PlanLabel => HasProAccess ? HasGoogleProAccess && HasManualProAccess ? "LME Pro (Google + manuale)" : HasGoogleProAccess ? "LME Pro (Google Play)" : "LME Pro (licenza manuale)" : "Base"; public bool RequiresPro(string feature) => ProFeatures.Contains(feature); public bool CanUse(string feature) => !RequiresPro(feature) || HasProAccess; public Task> GetSubscriptionPlansAsync( CancellationToken cancellationToken = default) => playBilling.GetSubscriptionPlansAsync(cancellationToken); public async Task LoadAsync() { IsGoogleProActive = Preferences.Default.Get(GoogleProActivePreference, false); GoogleExpiresAt = ReadDate(GoogleExpiresAtPreference); GoogleLastVerifiedAt = ReadDate(GoogleLastVerifiedAtPreference); await manualLicenses.LoadAsync(); } public void ApplyVerifiedGooglePro(DateTimeOffset? expiresAt) { IsGoogleProActive = true; GoogleExpiresAt = expiresAt; GoogleLastVerifiedAt = DateTimeOffset.UtcNow; SaveGoogleState(); } public void ApplyGoogleBase() { IsGoogleProActive = false; GoogleExpiresAt = null; GoogleLastVerifiedAt = DateTimeOffset.UtcNow; SaveGoogleState(); } public Task ActivateManualLicenseAsync(string? token) => manualLicenses.ActivateAsync(token); public Task RemoveManualLicenseAsync() => manualLicenses.RemoveAsync(); public async Task RestorePurchasesAsync() { var result = await playBilling.RestoreAsync(); return await ApplyPlayBillingResultAsync(result); } public async Task PurchaseGoogleProAsync(string basePlanId) { var result = await playBilling.PurchaseAsync(basePlanId); return await ApplyPlayBillingResultAsync(result); } private async Task ApplyPlayBillingResultAsync(PlayBillingResult result) { if (!result.Completed) { return result; } if (result.HasActiveSubscription) { if (!string.IsNullOrWhiteSpace(result.PurchaseToken)) { var verification = await purchaseVerification.VerifyAsync(result.PurchaseToken); if (verification.Verified) { if (verification.Active) { ApplyVerifiedGooglePro(verification.ExpiresAt); return result with { Message = string.IsNullOrWhiteSpace(verification.Message) ? result.Message : verification.Message }; } ApplyGoogleBase(); return PlayBillingResult.Base(string.IsNullOrWhiteSpace(verification.Message) ? "L'abbonamento Google Play non risulta attivo." : verification.Message); } if (purchaseVerification.IsRequired) { return PlayBillingResult.Failed(verification.Message); } } ApplyVerifiedGooglePro(null); } else { ApplyGoogleBase(); } return result; } private void SaveGoogleState() { Preferences.Default.Set(GoogleProActivePreference, IsGoogleProActive); WriteDate(GoogleExpiresAtPreference, GoogleExpiresAt); WriteDate(GoogleLastVerifiedAtPreference, GoogleLastVerifiedAt); } private static DateTimeOffset? ReadDate(string key) { var value = Preferences.Default.Get(key, string.Empty); return DateTimeOffset.TryParse(value, out var parsed) ? parsed : null; } private static void WriteDate(string key, DateTimeOffset? value) { if (value is null) { Preferences.Default.Remove(key); return; } Preferences.Default.Set(key, value.Value.ToString("O")); } } public static class AppFeatures { public const string CadastreMap = "cadastre-map"; public const string BasicSearch = "basic-search"; public const string DrawingMeasurements = "drawing-measurements"; public const string ProjectArchive = "project-archive"; public const string Export = "export"; public const string CloudSync = "cloud-sync"; public const string OfflineMaps = "offline-maps"; public const string AdvancedGeometry = "advanced-geometry"; public const string GpsTrail = "gps-trail"; public const string FiducialPoints = "fiducial-points"; }