Add subscription pricing and Base drawing limit
This commit is contained in:
1 parent
ad64d6def0
commit
6a675ce635
15 files changed
+384
-37
No files matched your search
@@ -235,6 +235,10 @@
|
|||||||
|
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2>Disegni e misure</h2>
|
<h2>Disegni e misure</h2>
|
||||||
|
@if (!Entitlements.HasProAccess)
|
||||||
|
{
|
||||||
|
<small class="field-help">@L("drawings.base.limit")</small>
|
||||||
|
}
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button class="secondary-command" @onclick="UndoMapChange">Annulla</button>
|
<button class="secondary-command" @onclick="UndoMapChange">Annulla</button>
|
||||||
<button class="secondary-command" @onclick="RedoMapChange">Ripristina</button>
|
<button class="secondary-command" @onclick="RedoMapChange">Ripristina</button>
|
||||||
@@ -494,8 +498,8 @@
|
|||||||
<p>@L("settings.pro.note")</p>
|
<p>@L("settings.pro.note")</p>
|
||||||
<p>@L("settings.pro.offline")</p>
|
<p>@L("settings.pro.offline")</p>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button class="primary-command" @onclick='() => SubscribeGooglePro(EntitlementService.MonthlyBasePlanId)' disabled="@BillingBusy">@L("settings.pro.monthly")</button>
|
<button class="primary-command" @onclick='() => SubscribeGooglePro(EntitlementService.MonthlyBasePlanId)' disabled="@BillingBusy">@MonthlyPlanLabel</button>
|
||||||
<button class="primary-command" @onclick='() => SubscribeGooglePro(EntitlementService.YearlyBasePlanId)' disabled="@BillingBusy">@L("settings.pro.yearly")</button>
|
<button class="primary-command" @onclick='() => SubscribeGooglePro(EntitlementService.YearlyBasePlanId)' disabled="@BillingBusy">@YearlyPlanLabel</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button class="secondary-command" @onclick="RestorePurchases" disabled="@BillingBusy">@L("settings.pro.restore")</button>
|
<button class="secondary-command" @onclick="RestorePurchases" disabled="@BillingBusy">@L("settings.pro.restore")</button>
|
||||||
@@ -725,6 +729,8 @@
|
|||||||
private string MeasurementPreference { get; set; } = "auto";
|
private string MeasurementPreference { get; set; } = "auto";
|
||||||
private string ManualLicenseCode { get; set; } = string.Empty;
|
private string ManualLicenseCode { get; set; } = string.Empty;
|
||||||
private string ManualLicenseMessage { get; set; } = string.Empty;
|
private string ManualLicenseMessage { get; set; } = string.Empty;
|
||||||
|
private string MonthlyPrice { get; set; } = string.Empty;
|
||||||
|
private string YearlyPrice { get; set; } = string.Empty;
|
||||||
private bool BillingBusy { get; set; }
|
private bool BillingBusy { get; set; }
|
||||||
private string Status { get; set; } = "online";
|
private string Status { get; set; } = "online";
|
||||||
private string SelectedBaseLayer { get; set; } = "satellite";
|
private string SelectedBaseLayer { get; set; } = "satellite";
|
||||||
@@ -784,6 +790,12 @@
|
|||||||
private string UpdateDownloadUrl { get; set; } = string.Empty;
|
private string UpdateDownloadUrl { get; set; } = string.Empty;
|
||||||
private string UpdateReleaseNotes { get; set; } = string.Empty;
|
private string UpdateReleaseNotes { get; set; } = string.Empty;
|
||||||
private string ProUpgradeText => "Funzione inclusa in LME Pro. I dati esistenti restano consultabili.";
|
private string ProUpgradeText => "Funzione inclusa in LME Pro. I dati esistenti restano consultabili.";
|
||||||
|
private string MonthlyPlanLabel => string.IsNullOrWhiteSpace(MonthlyPrice)
|
||||||
|
? L("settings.pro.monthly")
|
||||||
|
: $"{L("settings.pro.monthly")} - {MonthlyPrice}/mese";
|
||||||
|
private string YearlyPlanLabel => string.IsNullOrWhiteSpace(YearlyPrice)
|
||||||
|
? L("settings.pro.yearly")
|
||||||
|
: $"{L("settings.pro.yearly")} - {YearlyPrice}/anno";
|
||||||
private CancellationTokenSource? CatalogCancellation { get; set; }
|
private CancellationTokenSource? CatalogCancellation { get; set; }
|
||||||
private CancellationTokenSource? GpsLookupCancellation { get; set; }
|
private CancellationTokenSource? GpsLookupCancellation { get; set; }
|
||||||
private CancellationTokenSource? ProjectAutoSaveCancellation { get; set; }
|
private CancellationTokenSource? ProjectAutoSaveCancellation { get; set; }
|
||||||
@@ -892,6 +904,7 @@
|
|||||||
await ApplyEntitlementsToMapAsync();
|
await ApplyEntitlementsToMapAsync();
|
||||||
await ApplyFlagPreferencesToMapAsync();
|
await ApplyFlagPreferencesToMapAsync();
|
||||||
await ApplyAdvancedOptions();
|
await ApplyAdvancedOptions();
|
||||||
|
await RefreshBillingStateAsync();
|
||||||
await CenterOnInitialLocationAsync();
|
await CenterOnInitialLocationAsync();
|
||||||
await MigrateLegacyProjectAsync();
|
await MigrateLegacyProjectAsync();
|
||||||
var offlineCount = await JS.InvokeAsync<int>("lmeMap.getOfflineTileStats");
|
var offlineCount = await JS.InvokeAsync<int>("lmeMap.getOfflineTileStats");
|
||||||
@@ -2118,9 +2131,26 @@
|
|||||||
|
|
||||||
private Task ApplyEntitlementsToMapAsync() => JS.InvokeVoidAsync("lmeMap.setEntitlements", new
|
private Task ApplyEntitlementsToMapAsync() => JS.InvokeVoidAsync("lmeMap.setEntitlements", new
|
||||||
{
|
{
|
||||||
advancedGeometry = CanUse(AppFeatures.AdvancedGeometry)
|
advancedGeometry = CanUse(AppFeatures.AdvancedGeometry),
|
||||||
|
maxDrawingObjects = Entitlements.HasProAccess ? (int?)null : EntitlementService.BaseDrawingObjectLimit,
|
||||||
|
drawingLimitMessage = L("drawings.base.limit.reached")
|
||||||
}).AsTask();
|
}).AsTask();
|
||||||
|
|
||||||
|
private async Task RefreshBillingStateAsync()
|
||||||
|
{
|
||||||
|
var plans = await Entitlements.GetSubscriptionPlansAsync();
|
||||||
|
MonthlyPrice = plans.FirstOrDefault(plan => plan.BasePlanId == EntitlementService.MonthlyBasePlanId)?.FormattedPrice
|
||||||
|
?? string.Empty;
|
||||||
|
YearlyPrice = plans.FirstOrDefault(plan => plan.BasePlanId == EntitlementService.YearlyBasePlanId)?.FormattedPrice
|
||||||
|
?? string.Empty;
|
||||||
|
|
||||||
|
if (DeviceInfo.Current.Platform == DevicePlatform.Android)
|
||||||
|
{
|
||||||
|
await Entitlements.RestorePurchasesAsync();
|
||||||
|
await ApplyEntitlementsToMapAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RestorePurchases()
|
private async Task RestorePurchases()
|
||||||
{
|
{
|
||||||
BillingBusy = true;
|
BillingBusy = true;
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
<ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId>
|
<ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId>
|
||||||
|
|
||||||
<!-- Versions -->
|
<!-- Versions -->
|
||||||
<ApplicationDisplayVersion>1.0.3</ApplicationDisplayVersion>
|
<ApplicationDisplayVersion>1.0.4</ApplicationDisplayVersion>
|
||||||
<ApplicationVersion>103</ApplicationVersion>
|
<ApplicationVersion>104</ApplicationVersion>
|
||||||
<Version>$(ApplicationDisplayVersion)</Version>
|
<Version>$(ApplicationDisplayVersion)</Version>
|
||||||
|
|
||||||
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
|
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
|
||||||
@@ -43,6 +43,22 @@
|
|||||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<PlayBillingVerificationUrl Condition="'$(PlayBillingVerificationUrl)' == ''"></PlayBillingVerificationUrl>
|
||||||
|
<PlayBillingVerificationRequired Condition="'$(PlayBillingVerificationRequired)' == ''">false</PlayBillingVerificationRequired>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||||
|
<_Parameter1>PlayBillingVerificationUrl</_Parameter1>
|
||||||
|
<_Parameter2>$(PlayBillingVerificationUrl)</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||||
|
<_Parameter1>PlayBillingVerificationRequired</_Parameter1>
|
||||||
|
<_Parameter2>$(PlayBillingVerificationRequired)</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Release' and $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
<PropertyGroup Condition="'$(Configuration)' == 'Release' and $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||||
<AndroidTargetSdkVersion>36</AndroidTargetSdkVersion>
|
<AndroidTargetSdkVersion>36</AndroidTargetSdkVersion>
|
||||||
<AndroidPackageFormat>aab</AndroidPackageFormat>
|
<AndroidPackageFormat>aab</AndroidPackageFormat>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public static class MauiProgram
|
|||||||
builder.Services.AddSingleton(IncomingFileService.Shared);
|
builder.Services.AddSingleton(IncomingFileService.Shared);
|
||||||
builder.Services.AddSingleton<AppLocalizationService>();
|
builder.Services.AddSingleton<AppLocalizationService>();
|
||||||
builder.Services.AddSingleton<ManualLicenseService>();
|
builder.Services.AddSingleton<ManualLicenseService>();
|
||||||
|
builder.Services.AddSingleton<PlayPurchaseVerificationService>();
|
||||||
#if ANDROID
|
#if ANDROID
|
||||||
builder.Services.AddSingleton<IPlayBillingService, Platforms.Android.AndroidPlayBillingService>();
|
builder.Services.AddSingleton<IPlayBillingService, Platforms.Android.AndroidPlayBillingService>();
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- Nome: LME Mappe Catastali Pro.
|
- Nome: LME Mappe Catastali Pro.
|
||||||
- Package name Android: `it.liviomerola.lmemappecatastali.pro`.
|
- Package name Android: `it.liviomerola.lmemappecatastali.pro`.
|
||||||
- Versione iniziale Play: `1.0.0` / version code `100`.
|
- Versione corrente Play: `1.0.4` / version code `104`.
|
||||||
- Formato Release Android: Android App Bundle (`.aab`).
|
- Formato Release Android: Android App Bundle (`.aab`).
|
||||||
- Target SDK Release: Android 16 / API 36.
|
- Target SDK Release: Android 16 / API 36.
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ Base gratuita:
|
|||||||
- consultazione mappa catastale;
|
- consultazione mappa catastale;
|
||||||
- ricerca localita e ricerca catastale;
|
- ricerca localita e ricerca catastale;
|
||||||
- GPS live;
|
- GPS live;
|
||||||
- disegni e misure base;
|
- fino a 15 disegni, misure, POI e punti GPS creati dall'utente;
|
||||||
- apertura e consultazione dei dati gia presenti.
|
- apertura e consultazione dei dati gia presenti.
|
||||||
|
|
||||||
Pro:
|
Pro:
|
||||||
@@ -28,6 +28,7 @@ Pro:
|
|||||||
- mappe offline;
|
- mappe offline;
|
||||||
- strumenti geometrici avanzati;
|
- strumenti geometrici avanzati;
|
||||||
- traccia GPS.
|
- traccia GPS.
|
||||||
|
- disegni, misure, POI e punti GPS senza limite applicativo.
|
||||||
|
|
||||||
Se l'abbonamento non risulta verificabile offline, l'app conserva una tolleranza locale e non elimina, sequestra o altera i dati dell'utente. Le funzioni Pro possono essere disabilitate, ma i dati esistenti devono restare consultabili.
|
Se l'abbonamento non risulta verificabile offline, l'app conserva una tolleranza locale e non elimina, sequestra o altera i dati dell'utente. Le funzioni Pro possono essere disabilitate, ma i dati esistenti devono restare consultabili.
|
||||||
|
|
||||||
@@ -43,7 +44,9 @@ Il codice contiene solo identificativi di prodotto e base plan. Non inserire chi
|
|||||||
|
|
||||||
Il client Android usa Google Play Billing 9.1 tramite `Xamarin.Android.Google.BillingClient 9.1.0.2`. Acquisto, ripristino, acknowledgement e interrogazione delle sottoscrizioni sono collegati a `EntitlementService`.
|
Il client Android usa Google Play Billing 9.1 tramite `Xamarin.Android.Google.BillingClient 9.1.0.2`. Acquisto, ripristino, acknowledgement e interrogazione delle sottoscrizioni sono collegati a `EntitlementService`.
|
||||||
|
|
||||||
Prima della produzione serve ancora la verifica server-side dei purchase token. La verifica solo sul dispositivo e adatta ai test interni, ma non e sufficiente come protezione definitiva.
|
Il client supporta la verifica server-side dei purchase token tramite un endpoint HTTPS configurato in fase di build. Il servizio di riferimento e in `tools/LmePlayVerifier` e usa la Google Play Developer API senza incorporare credenziali nell'app.
|
||||||
|
|
||||||
|
Per il test chiuso l'endpoint puo restare disattivato. Prima della produzione occorre pubblicare il servizio, autorizzare il relativo service account nella Play Console e compilare l'app con `PlayBillingVerificationUrl` e `PlayBillingVerificationRequired=true`.
|
||||||
|
|
||||||
## Licenze manuali
|
## Licenze manuali
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# Informativa sulla privacy di LME Mappe Catastali Pro
|
# Informativa sulla privacy di LME Mappe Catastali Pro
|
||||||
|
|
||||||
Data di efficacia: 29 settembre 2026
|
Data di efficacia: 30 settembre 2026
|
||||||
|
|
||||||
## Titolare e contatti
|
## Titolare e contatti
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ La sincronizzazione WebDAV e facoltativa. Endpoint, nome utente e password sono
|
|||||||
|
|
||||||
## Licenze e acquisti
|
## Licenze e acquisti
|
||||||
|
|
||||||
Gli acquisti e gli abbonamenti Google Play sono gestiti da Google. L'app riceve da Google Play le informazioni tecniche necessarie a determinare lo stato del prodotto acquistato e ad abilitare le funzioni Pro. Le informazioni dell'account e del pagamento sono trattate da Google secondo la sua informativa.
|
Gli acquisti e gli abbonamenti Google Play sono gestiti da Google. L'app riceve da Google Play le informazioni tecniche necessarie a determinare lo stato del prodotto acquistato e ad abilitare le funzioni Pro. Quando il servizio di verifica LME e configurato, l'app gli trasmette il token tecnico dell'acquisto, il pacchetto dell'app e l'identificativo del prodotto per verificare presso Google Play lo stato e la scadenza dell'abbonamento. Il token non contiene i dati della carta di pagamento e non viene usato per profilazione o pubblicita. Le informazioni dell'account e del pagamento sono trattate da Google secondo la sua informativa.
|
||||||
|
|
||||||
Le licenze manuali contengono nome, cognome, email, identificativo e scadenza in un codice cifrato e firmato. Il codice viene conservato nell'archivio sicuro del dispositivo. Il titolare registra localmente gli stessi dati, insieme a data di emissione, scadenza, note e stato amministrativo, per gestire la licenza e l'assistenza.
|
Le licenze manuali contengono nome, cognome, email, identificativo e scadenza in un codice cifrato e firmato. Il codice viene conservato nell'archivio sicuro del dispositivo. Il titolare registra localmente gli stessi dati, insieme a data di emissione, scadenza, note e stato amministrativo, per gestire la licenza e l'assistenza.
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,35 @@ public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingSe
|
|||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<PlaySubscriptionPlan>> 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<PlaySubscriptionPlan>()
|
||||||
|
.ToArray() ?? [];
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<PlayBillingResult> RestoreAsync(CancellationToken cancellationToken = default)
|
public async Task<PlayBillingResult> RestoreAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -45,7 +74,9 @@ public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
var acknowledgement = await AcknowledgeIfNeededAsync(purchase);
|
var acknowledgement = await AcknowledgeIfNeededAsync(purchase);
|
||||||
return acknowledgement ?? PlayBillingResult.Active("Abbonamento LME Pro ripristinato da Google Play.");
|
return acknowledgement ?? PlayBillingResult.Active(
|
||||||
|
"Abbonamento LME Pro ripristinato da Google Play.",
|
||||||
|
purchase.PurchaseToken);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -61,21 +92,7 @@ public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingSe
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await EnsureConnectedAsync(cancellationToken);
|
var details = await GetProductDetailsAsync(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()?
|
var offer = details?.GetSubscriptionOfferDetails()?
|
||||||
.FirstOrDefault(item => item.BasePlanId == basePlanId && string.IsNullOrEmpty(item.OfferId));
|
.FirstOrDefault(item => item.BasePlanId == basePlanId && string.IsNullOrEmpty(item.OfferId));
|
||||||
if (details is null || offer is null)
|
if (details is null || offer is null)
|
||||||
@@ -144,7 +161,28 @@ public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
var acknowledgement = await AcknowledgeIfNeededAsync(purchase);
|
var acknowledgement = await AcknowledgeIfNeededAsync(purchase);
|
||||||
completion.TrySetResult(acknowledgement ?? PlayBillingResult.Active("Abbonamento LME Pro attivato."));
|
completion.TrySetResult(acknowledgement ?? PlayBillingResult.Active(
|
||||||
|
"Abbonamento LME Pro attivato.",
|
||||||
|
purchase.PurchaseToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ProductDetails?> 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)
|
private async Task EnsureConnectedAsync(CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ Informativa privacy: [PRIVACY_POLICY.md](PRIVACY_POLICY.md).
|
|||||||
- L'accesso Pro puo derivare da Google Play oppure da un codice manuale cifrato e firmato, con ritorno automatico a Base alla scadenza. Il gestore e le istruzioni sono in `tools/LmeLicenseManager` e `MANUAL_LICENSES.md`.
|
- L'accesso Pro puo derivare da Google Play oppure da un codice manuale cifrato e firmato, con ritorno automatico a Base alla scadenza. Il gestore e le istruzioni sono in `tools/LmeLicenseManager` e `MANUAL_LICENSES.md`.
|
||||||
- Lingua e sistema di misura sono centralizzati in `Services/AppLocalizationService.cs` e propagati alla mappa JavaScript, incluse distanze, superfici e Leaflet Draw.
|
- Lingua e sistema di misura sono centralizzati in `Services/AppLocalizationService.cs` e propagati alla mappa JavaScript, incluse distanze, superfici e Leaflet Draw.
|
||||||
- La checklist Play Store, Billing, firma e privacy e in `PLAY_STORE_PRO_CHECKLIST.md`.
|
- La checklist Play Store, Billing, firma e privacy e in `PLAY_STORE_PRO_CHECKLIST.md`.
|
||||||
|
- Il piano Base consente fino a 15 disegni, misure, POI e punti GPS; LME Pro non applica questo limite.
|
||||||
- Il progetto usa .NET 10, target Android API 36 e Google Play Billing 9.1.
|
- Il progetto usa .NET 10, target Android API 36 e Google Play Billing 9.1.
|
||||||
|
|
||||||
- La ricerca nel pannello Layer conserva correttamente posizione e selezione del cursore durante il filtraggio; digitando `pio` il testo non viene piu invertito in `oip`.
|
- La ricerca nel pannello Layer conserva correttamente posizione e selezione del cursore durante il filtraggio; digitando `pio` il testo non viene piu invertito in `oip`.
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ public sealed class AppLocalizationService
|
|||||||
["settings.pro.monthly"] = "Pro mensile",
|
["settings.pro.monthly"] = "Pro mensile",
|
||||||
["settings.pro.yearly"] = "Pro annuale",
|
["settings.pro.yearly"] = "Pro annuale",
|
||||||
["settings.pro.note"] = "Il prodotto LME Pro si configura nella Play Console con piano mensile e annuale.",
|
["settings.pro.note"] = "Il prodotto LME Pro si configura nella Play Console con piano mensile e annuale.",
|
||||||
|
["drawings.base.limit"] = "Piano Base: fino a 15 disegni, misure, POI e punti GPS. LME Pro non ha limiti.",
|
||||||
|
["drawings.base.limit.reached"] = "Hai raggiunto il limite di 15 oggetti del piano Base. Elimina un disegno o passa a LME Pro per continuare senza limiti.",
|
||||||
["settings.pro.manual.code"] = "Codice licenza manuale",
|
["settings.pro.manual.code"] = "Codice licenza manuale",
|
||||||
["settings.pro.manual.help"] = "Incolla il codice cifrato ricevuto. Nome, email e scadenza vengono letti solo dopo la verifica crittografica.",
|
["settings.pro.manual.help"] = "Incolla il codice cifrato ricevuto. Nome, email e scadenza vengono letti solo dopo la verifica crittografica.",
|
||||||
["settings.pro.manual.activate"] = "Attiva codice",
|
["settings.pro.manual.activate"] = "Attiva codice",
|
||||||
@@ -92,6 +94,8 @@ public sealed class AppLocalizationService
|
|||||||
["settings.pro.monthly"] = "Monthly Pro",
|
["settings.pro.monthly"] = "Monthly Pro",
|
||||||
["settings.pro.yearly"] = "Yearly Pro",
|
["settings.pro.yearly"] = "Yearly Pro",
|
||||||
["settings.pro.note"] = "Create the LME Pro product in Play Console with monthly and yearly base plans.",
|
["settings.pro.note"] = "Create the LME Pro product in Play Console with monthly and yearly base plans.",
|
||||||
|
["drawings.base.limit"] = "Base plan: up to 15 drawings, measurements, POIs and GPS points. LME Pro is unlimited.",
|
||||||
|
["drawings.base.limit.reached"] = "You reached the Base plan limit of 15 objects. Delete a drawing or upgrade to LME Pro to continue without limits.",
|
||||||
["settings.pro.manual.code"] = "Manual license code",
|
["settings.pro.manual.code"] = "Manual license code",
|
||||||
["settings.pro.manual.help"] = "Paste the encrypted code you received. Name, email and expiry are read only after cryptographic verification.",
|
["settings.pro.manual.help"] = "Paste the encrypted code you received. Name, email and expiry are read only after cryptographic verification.",
|
||||||
["settings.pro.manual.activate"] = "Activate code",
|
["settings.pro.manual.activate"] = "Activate code",
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
namespace LMEMappeCatastaliApp.Services;
|
namespace LMEMappeCatastaliApp.Services;
|
||||||
|
|
||||||
public sealed class EntitlementService(ManualLicenseService manualLicenses, IPlayBillingService playBilling)
|
public sealed class EntitlementService(
|
||||||
|
ManualLicenseService manualLicenses,
|
||||||
|
IPlayBillingService playBilling,
|
||||||
|
PlayPurchaseVerificationService purchaseVerification)
|
||||||
{
|
{
|
||||||
public const string ProductId = "lme_pro";
|
public const string ProductId = "lme_pro";
|
||||||
public const string MonthlyBasePlanId = "monthly";
|
public const string MonthlyBasePlanId = "monthly";
|
||||||
public const string YearlyBasePlanId = "yearly";
|
public const string YearlyBasePlanId = "yearly";
|
||||||
|
public const int BaseDrawingObjectLimit = 15;
|
||||||
|
|
||||||
private const string GoogleProActivePreference = "entitlement_google_pro_active";
|
private const string GoogleProActivePreference = "entitlement_google_pro_active";
|
||||||
private const string GoogleExpiresAtPreference = "entitlement_google_pro_expires_at";
|
private const string GoogleExpiresAtPreference = "entitlement_google_pro_expires_at";
|
||||||
@@ -53,6 +57,10 @@ public sealed class EntitlementService(ManualLicenseService manualLicenses, IPla
|
|||||||
public bool RequiresPro(string feature) => ProFeatures.Contains(feature);
|
public bool RequiresPro(string feature) => ProFeatures.Contains(feature);
|
||||||
public bool CanUse(string feature) => !RequiresPro(feature) || HasProAccess;
|
public bool CanUse(string feature) => !RequiresPro(feature) || HasProAccess;
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<PlaySubscriptionPlan>> GetSubscriptionPlansAsync(
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
playBilling.GetSubscriptionPlansAsync(cancellationToken);
|
||||||
|
|
||||||
public async Task LoadAsync()
|
public async Task LoadAsync()
|
||||||
{
|
{
|
||||||
IsGoogleProActive = Preferences.Default.Get(GoogleProActivePreference, false);
|
IsGoogleProActive = Preferences.Default.Get(GoogleProActivePreference, false);
|
||||||
@@ -85,32 +93,60 @@ public sealed class EntitlementService(ManualLicenseService manualLicenses, IPla
|
|||||||
public async Task<PlayBillingResult> RestorePurchasesAsync()
|
public async Task<PlayBillingResult> RestorePurchasesAsync()
|
||||||
{
|
{
|
||||||
var result = await playBilling.RestoreAsync();
|
var result = await playBilling.RestoreAsync();
|
||||||
ApplyPlayBillingResult(result);
|
return await ApplyPlayBillingResultAsync(result);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PlayBillingResult> PurchaseGoogleProAsync(string basePlanId)
|
public async Task<PlayBillingResult> PurchaseGoogleProAsync(string basePlanId)
|
||||||
{
|
{
|
||||||
var result = await playBilling.PurchaseAsync(basePlanId);
|
var result = await playBilling.PurchaseAsync(basePlanId);
|
||||||
ApplyPlayBillingResult(result);
|
return await ApplyPlayBillingResultAsync(result);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyPlayBillingResult(PlayBillingResult result)
|
private async Task<PlayBillingResult> ApplyPlayBillingResultAsync(PlayBillingResult result)
|
||||||
{
|
{
|
||||||
if (!result.Completed)
|
if (!result.Completed)
|
||||||
{
|
{
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.HasActiveSubscription)
|
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);
|
ApplyVerifiedGooglePro(null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ApplyGoogleBase();
|
ApplyGoogleBase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveGoogleState()
|
private void SaveGoogleState()
|
||||||
|
|||||||
@@ -2,13 +2,22 @@ namespace LMEMappeCatastaliApp.Services;
|
|||||||
|
|
||||||
public interface IPlayBillingService
|
public interface IPlayBillingService
|
||||||
{
|
{
|
||||||
|
Task<IReadOnlyList<PlaySubscriptionPlan>> GetSubscriptionPlansAsync(CancellationToken cancellationToken = default);
|
||||||
Task<PlayBillingResult> RestoreAsync(CancellationToken cancellationToken = default);
|
Task<PlayBillingResult> RestoreAsync(CancellationToken cancellationToken = default);
|
||||||
Task<PlayBillingResult> PurchaseAsync(string basePlanId, CancellationToken cancellationToken = default);
|
Task<PlayBillingResult> PurchaseAsync(string basePlanId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PlayBillingResult(bool Completed, bool HasActiveSubscription, bool UserCancelled, string Message)
|
public sealed record PlaySubscriptionPlan(string BasePlanId, string FormattedPrice, string BillingPeriod);
|
||||||
|
|
||||||
|
public sealed record PlayBillingResult(
|
||||||
|
bool Completed,
|
||||||
|
bool HasActiveSubscription,
|
||||||
|
bool UserCancelled,
|
||||||
|
string Message,
|
||||||
|
string? PurchaseToken = null)
|
||||||
{
|
{
|
||||||
public static PlayBillingResult Active(string message) => new(true, true, false, message);
|
public static PlayBillingResult Active(string message, string? purchaseToken = null) =>
|
||||||
|
new(true, true, false, message, purchaseToken);
|
||||||
public static PlayBillingResult Base(string message) => new(true, false, false, message);
|
public static PlayBillingResult Base(string message) => new(true, false, false, message);
|
||||||
public static PlayBillingResult Cancelled(string message) => new(false, false, true, message);
|
public static PlayBillingResult Cancelled(string message) => new(false, false, true, message);
|
||||||
public static PlayBillingResult Failed(string message) => new(false, false, false, message);
|
public static PlayBillingResult Failed(string message) => new(false, false, false, message);
|
||||||
@@ -16,6 +25,9 @@ public sealed record PlayBillingResult(bool Completed, bool HasActiveSubscriptio
|
|||||||
|
|
||||||
public sealed class UnavailablePlayBillingService : IPlayBillingService
|
public sealed class UnavailablePlayBillingService : IPlayBillingService
|
||||||
{
|
{
|
||||||
|
public Task<IReadOnlyList<PlaySubscriptionPlan>> GetSubscriptionPlansAsync(CancellationToken cancellationToken = default) =>
|
||||||
|
Task.FromResult<IReadOnlyList<PlaySubscriptionPlan>>([]);
|
||||||
|
|
||||||
public Task<PlayBillingResult> RestoreAsync(CancellationToken cancellationToken = default) =>
|
public Task<PlayBillingResult> RestoreAsync(CancellationToken cancellationToken = default) =>
|
||||||
Task.FromResult(PlayBillingResult.Failed("Google Play Billing è disponibile solo nell'app Android installata da Google Play."));
|
Task.FromResult(PlayBillingResult.Failed("Google Play Billing è disponibile solo nell'app Android installata da Google Play."));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace LMEMappeCatastaliApp.Services;
|
||||||
|
|
||||||
|
public sealed class PlayPurchaseVerificationService
|
||||||
|
{
|
||||||
|
private const string EndpointMetadataKey = "PlayBillingVerificationUrl";
|
||||||
|
private const string RequiredMetadataKey = "PlayBillingVerificationRequired";
|
||||||
|
private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(20) };
|
||||||
|
|
||||||
|
public bool IsConfigured => VerificationEndpoint is not null;
|
||||||
|
public bool IsRequired => ReadMetadata(RequiredMetadataKey).Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static Uri? VerificationEndpoint
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var value = ReadMetadata(EndpointMetadataKey);
|
||||||
|
return Uri.TryCreate(value, UriKind.Absolute, out var endpoint)
|
||||||
|
&& endpoint.Scheme == Uri.UriSchemeHttps
|
||||||
|
? endpoint
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PlayPurchaseVerificationResult> VerifyAsync(
|
||||||
|
string purchaseToken,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var endpoint = VerificationEndpoint;
|
||||||
|
if (endpoint is null)
|
||||||
|
{
|
||||||
|
return PlayPurchaseVerificationResult.NotConfigured();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.PostAsJsonAsync(endpoint, new
|
||||||
|
{
|
||||||
|
packageName = AppInfo.Current.PackageName,
|
||||||
|
productId = EntitlementService.ProductId,
|
||||||
|
purchaseToken
|
||||||
|
}, cancellationToken);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return PlayPurchaseVerificationResult.Failed(
|
||||||
|
$"Verifica acquisto non disponibile ({(int)response.StatusCode}).");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<VerificationResponse>(cancellationToken);
|
||||||
|
return result is null
|
||||||
|
? PlayPurchaseVerificationResult.Failed("Risposta di verifica acquisto non valida.")
|
||||||
|
: new(true, true, result.Active, result.ExpiresAt, result.Message ?? string.Empty);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return PlayPurchaseVerificationResult.Failed("Il server di verifica acquisti non ha risposto in tempo.");
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
return PlayPurchaseVerificationResult.Failed(
|
||||||
|
$"Verifica acquisto non disponibile: {error.GetBaseException().Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadMetadata(string key) =>
|
||||||
|
typeof(PlayPurchaseVerificationService).Assembly
|
||||||
|
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||||
|
.FirstOrDefault(attribute => attribute.Key == key)?.Value?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
|
private sealed record VerificationResponse(bool Active, DateTimeOffset? ExpiresAt, string? Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PlayPurchaseVerificationResult(
|
||||||
|
bool Configured,
|
||||||
|
bool Verified,
|
||||||
|
bool Active,
|
||||||
|
DateTimeOffset? ExpiresAt,
|
||||||
|
string Message)
|
||||||
|
{
|
||||||
|
public static PlayPurchaseVerificationResult NotConfigured() =>
|
||||||
|
new(false, false, false, null, "Server di verifica acquisti non configurato.");
|
||||||
|
|
||||||
|
public static PlayPurchaseVerificationResult Failed(string message) =>
|
||||||
|
new(true, false, false, null, message);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Google.Apis.AndroidPublisher.v3" Version="1.76.0.4277" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using Google.Apis.AndroidPublisher.v3;
|
||||||
|
using Google.Apis.Auth.OAuth2;
|
||||||
|
using Google.Apis.Services;
|
||||||
|
|
||||||
|
const string PackageName = "it.liviomerola.lmemappecatastali.pro";
|
||||||
|
const string ProductId = "lme_pro";
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Services.AddSingleton(_ =>
|
||||||
|
{
|
||||||
|
var credential = GoogleCredential.GetApplicationDefault()
|
||||||
|
.CreateScoped(AndroidPublisherService.Scope.Androidpublisher);
|
||||||
|
return new AndroidPublisherService(new BaseClientService.Initializer
|
||||||
|
{
|
||||||
|
HttpClientInitializer = credential,
|
||||||
|
ApplicationName = "LME Play subscription verifier"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
||||||
|
app.MapPost("/v1/google-play/subscriptions/verify", async (
|
||||||
|
VerifySubscriptionRequest request,
|
||||||
|
AndroidPublisherService publisher,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
if (request.PackageName != PackageName || request.ProductId != ProductId
|
||||||
|
|| string.IsNullOrWhiteSpace(request.PurchaseToken))
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new VerifySubscriptionResponse(false, null, "Richiesta non valida."));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var purchase = await publisher.Purchases.Subscriptionsv2
|
||||||
|
.Get(PackageName, request.PurchaseToken)
|
||||||
|
.ExecuteAsync(cancellationToken);
|
||||||
|
var active = purchase.SubscriptionState is "SUBSCRIPTION_STATE_ACTIVE"
|
||||||
|
or "SUBSCRIPTION_STATE_IN_GRACE_PERIOD";
|
||||||
|
var expiresAt = purchase.LineItems?
|
||||||
|
.Select(item => item.ExpiryTimeDateTimeOffset)
|
||||||
|
.Where(value => value.HasValue)
|
||||||
|
.Max();
|
||||||
|
return Results.Ok(new VerifySubscriptionResponse(
|
||||||
|
active,
|
||||||
|
expiresAt,
|
||||||
|
active ? "Abbonamento LME Pro verificato." : "L'abbonamento Google Play non risulta attivo."));
|
||||||
|
}
|
||||||
|
catch (Google.GoogleApiException error) when ((int)error.HttpStatusCode is 400 or 404)
|
||||||
|
{
|
||||||
|
return Results.Ok(new VerifySubscriptionResponse(false, null, "Acquisto Google Play non riconosciuto."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
internal sealed record VerifySubscriptionRequest(string PackageName, string ProductId, string PurchaseToken);
|
||||||
|
internal sealed record VerifySubscriptionResponse(bool Active, DateTimeOffset? ExpiresAt, string Message);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# LME Play verifier
|
||||||
|
|
||||||
|
Servizio HTTPS per verificare i token degli abbonamenti `lme_pro` con la Google Play Developer API.
|
||||||
|
|
||||||
|
## Configurazione
|
||||||
|
|
||||||
|
1. Creare un service account in Google Cloud e concedergli nella Play Console il permesso di visualizzare ordini e abbonamenti dell'app.
|
||||||
|
2. Conservare il JSON del service account solo sul server.
|
||||||
|
3. Impostare `GOOGLE_APPLICATION_CREDENTIALS` con il percorso assoluto del JSON.
|
||||||
|
4. Pubblicare il servizio dietro HTTPS.
|
||||||
|
5. Compilare l'app con:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
-p:PlayBillingVerificationUrl=https://example.test/v1/google-play/subscriptions/verify
|
||||||
|
-p:PlayBillingVerificationRequired=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Non inserire il JSON del service account, token di acquisto o credenziali nel repository o nell'AAB.
|
||||||
+32
-1
@@ -96,7 +96,9 @@ const lmeState = {
|
|||||||
culture: "it-IT",
|
culture: "it-IT",
|
||||||
measurementSystem: "metric",
|
measurementSystem: "metric",
|
||||||
entitlements: {
|
entitlements: {
|
||||||
advancedGeometry: false
|
advancedGeometry: false,
|
||||||
|
maxDrawingObjects: 15,
|
||||||
|
drawingLimitMessage: "Hai raggiunto il limite di 15 oggetti del piano Base."
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
measureArea: "Misura area",
|
measureArea: "Misura area",
|
||||||
@@ -213,6 +215,20 @@ function requireProFeature(feature) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawingObjectCount() {
|
||||||
|
return lmeState.drawings ? managedRecordsForCategory("drawings").length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canAddDrawingObjects(amount = 1) {
|
||||||
|
const configuredMaximum = lmeState.entitlements?.maxDrawingObjects;
|
||||||
|
if (configuredMaximum === null || configuredMaximum === undefined || configuredMaximum === "") return true;
|
||||||
|
const maximum = Number(configuredMaximum);
|
||||||
|
if (!Number.isFinite(maximum) || maximum < 0) return true;
|
||||||
|
if (drawingObjectCount() + Math.max(0, Number(amount) || 0) <= maximum) return true;
|
||||||
|
setInfo(lmeState.entitlements?.drawingLimitMessage || `Limite di ${maximum} oggetti raggiunto.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function init(dotNetReference) {
|
function init(dotNetReference) {
|
||||||
if (dotNetReference) {
|
if (dotNetReference) {
|
||||||
lmeState.dotNetReference = dotNetReference;
|
lmeState.dotNetReference = dotNetReference;
|
||||||
@@ -1775,6 +1791,10 @@ function moveManagedRecordsToGroup(records, targetCategory, targetSource, target
|
|||||||
setInfo("Sblocca il gruppo prima di spostarlo.");
|
setInfo("Sblocca il gruppo prima di spostarlo.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const additionalDrawings = targetCategory === "drawings"
|
||||||
|
? movable.filter(record => record.categoryKey !== "drawings").length
|
||||||
|
: 0;
|
||||||
|
if (!canAddDrawingObjects(additionalDrawings)) return;
|
||||||
rememberMapChange();
|
rememberMapChange();
|
||||||
const features = movable.map(record => {
|
const features = movable.map(record => {
|
||||||
const feature = record.layer.toGeoJSON();
|
const feature = record.layer.toGeoJSON();
|
||||||
@@ -2684,6 +2704,7 @@ function storedFeatureStyle(feature) {
|
|||||||
function duplicateManagedRecords(records) {
|
function duplicateManagedRecords(records) {
|
||||||
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
|
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
|
||||||
if (records.some(record => record.layer?.feature?.properties?._lmeLocked)) { setInfo("Sblocca gli oggetti da duplicare."); return; }
|
if (records.some(record => record.layer?.feature?.properties?._lmeLocked)) { setInfo("Sblocca gli oggetti da duplicare."); return; }
|
||||||
|
if (!canAddDrawingObjects(records.length)) return;
|
||||||
rememberMapChange();
|
rememberMapChange();
|
||||||
records.forEach((record, index) => {
|
records.forEach((record, index) => {
|
||||||
const feature = record.layer.toGeoJSON();
|
const feature = record.layer.toGeoJSON();
|
||||||
@@ -3036,6 +3057,11 @@ function restoreLastTrashItem() {
|
|||||||
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return false; }
|
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return false; }
|
||||||
const entry = lmeState.trash.pop();
|
const entry = lmeState.trash.pop();
|
||||||
if (!entry) { setInfo("Il cestino e' vuoto."); return false; }
|
if (!entry) { setInfo("Il cestino e' vuoto."); return false; }
|
||||||
|
const drawingsToRestore = entry.items.filter(item => !["imported", "importedSheets"].includes(item.categoryKey)).length;
|
||||||
|
if (!canAddDrawingObjects(drawingsToRestore)) {
|
||||||
|
lmeState.trash.push(entry);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
rememberMapChange();
|
rememberMapChange();
|
||||||
entry.items.forEach(item => {
|
entry.items.forEach(item => {
|
||||||
if (item.categoryKey === "imported") addImportedFeatures({ type: "FeatureCollection", features: [item.feature] });
|
if (item.categoryKey === "imported") addImportedFeatures({ type: "FeatureCollection", features: [item.feature] });
|
||||||
@@ -3825,6 +3851,7 @@ function clearImported() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addProjectMarker(name) {
|
function addProjectMarker(name) {
|
||||||
|
if (!canAddDrawingObjects()) return false;
|
||||||
rememberMapChange();
|
rememberMapChange();
|
||||||
const center = lmeState.map.getCenter();
|
const center = lmeState.map.getCenter();
|
||||||
const label = (name || "Nuovo POI").trim();
|
const label = (name || "Nuovo POI").trim();
|
||||||
@@ -3840,6 +3867,7 @@ function addProjectMarker(name) {
|
|||||||
refreshLayerManager();
|
refreshLayerManager();
|
||||||
selectDrawingLayer(marker);
|
selectDrawingLayer(marker);
|
||||||
finishMapChange();
|
finishMapChange();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureDrawLabels() {
|
function configureDrawLabels() {
|
||||||
@@ -3868,6 +3896,7 @@ function bindDrawingEvents() {
|
|||||||
setInfo("Il progetto e' in sola lettura.");
|
setInfo("Il progetto e' in sola lettura.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!canAddDrawingObjects()) return;
|
||||||
snapDrawingLayer(event.layer);
|
snapDrawingLayer(event.layer);
|
||||||
annotateDrawingLayer(event.layer, event.layerType);
|
annotateDrawingLayer(event.layer, event.layerType);
|
||||||
lmeState.drawings.addLayer(event.layer);
|
lmeState.drawings.addLayer(event.layer);
|
||||||
@@ -4104,6 +4133,7 @@ function startMeasurementDrawing(mode) {
|
|||||||
if (!lmeState.map) {
|
if (!lmeState.map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!canAddDrawingObjects()) return;
|
||||||
bindDrawingEvents();
|
bindDrawingEvents();
|
||||||
hideDrawingScopeMenu();
|
hideDrawingScopeMenu();
|
||||||
if (lmeState.activeDrawHandler) {
|
if (lmeState.activeDrawHandler) {
|
||||||
@@ -5736,6 +5766,7 @@ function bearingToCardinal(value) {
|
|||||||
|
|
||||||
function saveGpsMeasurementPoint() {
|
function saveGpsMeasurementPoint() {
|
||||||
if (!lmeState.lastPosition) { setInfo("Attendi una posizione GPS valida."); return; }
|
if (!lmeState.lastPosition) { setInfo("Attendi una posizione GPS valida."); return; }
|
||||||
|
if (!canAddDrawingObjects()) return;
|
||||||
rememberMapChange();
|
rememberMapChange();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
addDrawingFeature({ type: "Feature", properties: {
|
addDrawingFeature({ type: "Feature", properties: {
|
||||||
|
|||||||
Reference in new issue
Block a user