Files

191 lines
6.6 KiB
C#

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<string> 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<IReadOnlyList<PlaySubscriptionPlan>> 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<ManualLicenseActivationResult> ActivateManualLicenseAsync(string? token) =>
manualLicenses.ActivateAsync(token);
public Task RemoveManualLicenseAsync() => manualLicenses.RemoveAsync();
public async Task<PlayBillingResult> RestorePurchasesAsync()
{
var result = await playBilling.RestoreAsync();
return await ApplyPlayBillingResultAsync(result);
}
public async Task<PlayBillingResult> PurchaseGoogleProAsync(string basePlanId)
{
var result = await playBilling.PurchaseAsync(basePlanId);
return await ApplyPlayBillingResultAsync(result);
}
private async Task<PlayBillingResult> 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";
}