Uno dei punti più delicati nella pubblicazione di un'applicazione Windows 8 nello Store è la scelta del giusto tipo di licenza. Nella sezione Selling details della Windows Store Dashboard, mostrata nella prossima figura, è possibile specificare il prezzo di vendita della app, selezionare un periodo di trial e scegliere i paesi nei quali vogliamo vendere l'app.
Il modo più semplice, ma probabilmente il meno efficace, è quello di vendere l'intera app per un certo prezzo. Gli utenti potranno avvalersi delle informazioni pubblicate sullo Store e dei giudizi degli utenti per decidere se valga o meno la pena spendere il prezzo richiesto. Questo approccio corrisponde all'opzione No trial mostrata nell'immagine precedente.
Una seconda strada è quella di concedere all'utente di provare l'applicazione prima di acquistarla. In questo caso, gli utenti potranno scaricare dallo Store una versione di prova dell'app, con tutte le funzionalità proprie di quella a pagamento. Quando il periodo di prova giunge a scadenza (il periodo di prova può scadere dopo 1, 7, 15 o 30 giorni), l'applicazione semplicemente smette di funzionare (si parla in questo caso di "timed-trial").
Una terza opzione consiste nel concedere all'utente la possibilità di usare indefinitamente l'app, ma subordinando una parte delle funzionalità o del contenuto dell'app all'acquisto da parte dell'utente della versione completa (feature-based trial). Questa strada corrisponde all'opzione Trial never expires mostrata nell'immagine precedente.
Molto simile alla precedente è la modalità "in-app purchase". In questo caso, ad essere distribuita (gratuitamente o meno) è una versione dell'app che contiene una serie di funzioni o contenuti di base, che gli utenti possono estendere acquistando funzionalità avanzate o contenuti addizionali (ad esempio, nuove mappe in un'app che utilizza il GPS per la navigazione). La differenza con un feature-based trial è che, in questo caso, l'applicazione presenta una struttura "modulare", in cui ciascun modulo (funzionalità avanzata o contenuto extra che sia) può essere acquistato separatamente.
Nella dashboard del Windows Store è possibile definire quali prodotti possono essere acquistati dagli utenti della propria applicazione. Per ciascuno di essi, possiamo stabilire il prezzo e la durata di validità (ossia per quanto a lungo l'utente potrà continuare a utilizzare quella funzionalità). Trascorso questo termine, la funzionalità smetterà di funzionare e il prodotto dovrà essere acquistato nuovamente.
A partire da Windows 8.1, il meccanismo dell'in-app purchase include anche la possibilità di vendere agli utenti della propria app prodotti "consumabili", ossia prodotti che possono essere acquistati, consumati e quindi consumati nuovamente. L'esempio più classico è rappresentato dall'acquisto di punti o di moneta virtuale, che possono essere quindi "spesi" nel gioco per acquistare nuovi oggetti, sbloccare nuovi personaggi, ecc.
La prossima immagine mostra le varie opzioni a disposizione per l'in-app purchase.
Esaminiamo la licenza di un'app
Grazie alle API messe a disposizione dal namespace Windows.ApplicationModel.Store
è possibile determinare lo stato della licenza di un'app, verificare la data di scadenza di un periodo di trial, acquistare un prodotto tramite in-app purchase, e altro ancora.
Le informazioni sullo stato corrente della licenza di un'app o di un prodotto acquistabile tramite in-app purchase sono accessibili tramite la proprietà LicenseInformation
della classe CurrentApp
. In particolare, questa proprietà espone le seguenti proprietà (in sola lettura):
Proprietà | Descrizione |
---|---|
IsActive | descrive lo stato corrente della licenza. Un valore di true indica una licenza valida, a prescindere dal fatto che l'applicazione sia o meno in modalità trial. Se il valore è false , significa invece che lo stato della licenza è invalido, perché mancante, scaduta o revocata. |
IsTrial | indica se un'app si trova in modalità trial o meno. Un valore di false significa che l'utente ha acquistato la versione completa dell'app, mentre true indica che l'app si trova ancora in modalità di prova. È importante ricordare che questa proprietà restituisce true anche se il periodo di prova è scaduto. Per questa ragione, è necessario sempre controllare questa proprietà in combinazione con la proprietà IsActive (vedi oltre per gli esempi). |
ExpirationDate | indica la data di scadenza per il periodo di prova. La data è espressa usando il formato ISO 8601, ossia yyyy-mm-ddThh:mm:ss.ssZ. Per esempio, la data 2014-06-19T09:00:00.00Z significa che il periodo di prova scadrà il 19 giugno 2014 alle 9 di mattina. |
ProductLicenses | contiene la lista delle licenze per le feature acquistabili tramite in-app purchase. |
È importante capire che la classe CurrentApp
consente di recuperare dati e informazioni dal Windows Store, per cui per accedervi è necessario che l'app sia stata già pubblicata con successo sullo store. Per testare il comportamento della tua app con trial e in-app purchase prima della pubblicazione, WinRT espone anche una classe CurrentAppSimulator
, la quale definisce metodi e proprietà che "mimano" quelli esposti dalla classe CurrentApp
, ma all'interno di un ambiente simulato. Per questo motivo, prima di pubblicare l'app sullo store è necessario sostituire ogni riferimento alla classe CurrentAppSimulator
, usata per testare l'applicazione durante il processo di sviluppo, con la classe CurrentApp
, altrimenti il processo di certificazione non potrà essere superato.
Il prossimo listato mostra come recuperare lo stato di una licenza (simulata) tramite la classe CurrentAppSimulator
.
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
await this.DisplayLicenseInfo();
}
private async Task DisplayLicenseInfo()
{
try
{
var licenseInfo = CurrentAppSimulator.LicenseInformation;
if (licenseInfo.IsActive)
{
if (licenseInfo.IsTrial)
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
() =>
{
LicenseState.Text = "Stato della licenza: Trial";
var remainingDays = (licenseInfo.ExpirationDate - DateTime.Now).Days;
LicenseRemainingDays.Text = String.Format("Data di scadenza: {0:MM/dd/yyyy} - Giorni rimanenti: {1}", licenseInfo.ExpirationDate, remainingDays);
});
}
else
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
LicenseState.Text = "Stato della licenza: Full license";
LicenseRemainingDays.Text = "nessuna scadenza";
});
}
}
else
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
LicenseState.Text = "Stato della licenza: il trial è terminato, per favore acquista l’app!";
});
}
}
catch (Exception ex)
{
// impossibile recuperare le informazioni
}
}
Il codice, dopo aver recuperato le informazioni contenute nella proprietà LicenseInformation
della classe CurrentAppSimulator
, verifica per prima cosa che l'app sia ancora attiva: se il periodo di trial non è ancora scaduto (ossia se la proprietà IsActive
restituisce true
), il codice procede a verificare se l'app si trovi ancora in modalità trial (mostrando in questo caso il numero di giorni che mancano alla scadenza) o se, al contrario, l'utente abbia acquistato la versione full.
Possiamo usare la seguente definizione XAML come riferimento per testare il codice sopra proposto:
<Page
x:Class="Demo.Html.it.TrialSample.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Demo.Html.it.TrialSample"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<TextBlock x:Name="LicenseState" FontSize="20" Margin="20, 5"/>
<TextBlock x:Name="LicenseRemainingDays" FontSize="20" Margin="20, 5" />
</StackPanel>
</Grid>
</Page>
Se adesso eseguite l'app, dovreste ottenere un risultato simile al seguente:
Queste informazioni provengono da un file XML denominato WindowsStoreProxy.xml file che è localizzato nella cartella:
%userprofile%\appdata\local\packages\<package-moniker>\localstate\microsoft\Windows Store\Apidata
Il prossimo listato mostra il contenuto del file auto-generato WindowsStoreProxy.xml di un'app installata nella macchina locale (la definizione della licenza in Windows 8 era leggermente diversa rispetto a quella introdotta con Windows 8.1, qui sotto riportata).
<?xml version="1.0" encoding="utf-16" ?>
<CurrentApp>
<ListingInformation>
<App>
<AppId>00000000-0000-0000-0000-000000000000</AppId>
<LinkUri>http://apps.microsoft.com/webpdp/app/00000000-0000-0000-0000-000000000000</LinkUri>
<CurrentMarket>en-US</CurrentMarket>
<AgeRating>3</AgeRating>
<MarketData xml:lang="en-us">
<Name>AppName</Name>
<Description>AppDescription</Description>
<Price>1.00</Price>
<CurrencySymbol>$</CurrencySymbol>
<CurrencyCode>USD</CurrencyCode>
</MarketData>
</App>
<Product ProductId="1" LicenseDuration="0" ProductType="Durable">
<MarketData xml:lang="en-us">
<Name>Product1Name</Name>
<Price>1.00</Price>
<CurrencySymbol>$</CurrencySymbol>
<CurrencyCode>USD</CurrencyCode>
</MarketData>
</Product>
<Product ProductId="2" LicenseDuration="0" ProductType="Consumable">
<MarketData xml:lang="en-us">
<Name>Product2Name</Name>
<Price>1.00</Price>
<CurrencySymbol>$</CurrencySymbol>
<CurrencyCode>USD</CurrencyCode>
</MarketData>
</Product>
</ListingInformation>
<LicenseInformation>
<App>
<IsActive>true</IsActive>
<IsTrial>true</IsTrial>
</App>
<Product ProductId="1">
<IsActive>true</IsActive>
</Product>
</LicenseInformation>
<ConsumableInformation>
<Product ProductId="2" TransactionId="00000000-0000-0000-0000-000000000000" Status="Active" />
</ConsumableInformation>
</CurrentApp>
Come si può notare, il file XML contiene un elemento LicenseInformation
che contiene le informazioni relative allo stato della licenza dell'app e dei prodotti acquistabili tramite in-app purchase. Queste informazioni sono accessibili tramite la proprietà LicenseInformation
vista sopra.
Oltre alle informazioni sullo stato della licenza, il file XML contiene anche un altro elemento, ListingInformation
, che elenca una serie di dati relativi all'app e ai prodotti acquistabili tramite in-app purchase. Alcune di queste informazioni possono essere recuperate tramite le corrispondenti proprietà della classe CurrentApp
(o della classe CurrentAppSimulator
se, come in questo caso, ci troviamo in un ambiente di sviluppo), come la proprietà AppId
, che rappresenta l'identificativo univoco dell'applicazione sul Windows Store, o la proprietà LinkUri
, che contiene il link alla pagina dell'app sullo store.
Le altre informazioni contenute nell'elemento ListingInformation
includono il nome dell'applicazione sullo store e dei vari prodotti acquistabili, i limiti d'età, il mercato corrente, il prezzo dell'app e dei vari prodotti e così via. Queste informazioni possono essere recuperate dal Windows Store (vero o simulato) tramite il metodo LoadListingInformationAsync
della classe CurrentApp
(o CurrentAppSimulator
). Questo metodo restituisce infatti un'istanza della classe ListingInformation
.
Il seguente listato mostra le relative informazioni a schermo :
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
await this.DisplayListingInformation();
await this.DisplayLicenseInfo();
}
private async Task DisplayListingInformation()
{
AppId.Text = String.Format("App ID: {0}", CurrentAppSimulator.AppId);
StoreLink.Text = String.Format("Link per il Windows Store: {0}", CurrentAppSimulator.LinkUri);
try
{
ListingInformation listingInfo = await CurrentAppSimulator.LoadListingInformationAsync();
AppName.Text = String.Format("Nome dell'app sul Windows Store: {0}", listingInfo.Name);
}
catch (Exception ex)
{
AppName.Text = String.Format("Informazioni non disponibili: {0}", ex.Message);
}
}
Per visualizzare le nuove informazioni a schermo, è sufficiente modificare la pagina XAML come segue:
<StackPanel Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<TextBlock x:Name="AppId" FontSize="20" Margin="20, 5" />
<TextBlock x:Name="StoreLink" FontSize="20" Margin="20, 5" />
<TextBlock x:Name="AppName" FontSize="20" Margin="20, 5" />
<TextBlock x:Name="LicenseState" FontSize="20" Margin="20, 5"/>
<TextBlock x:Name="LicenseRemainingDays" FontSize="20" Margin="20, 5" />
</StackPanel>
Se adesso eseguite l'applicazione, dovreste ottenere un risultato simile a quello mostrato nella prossima immagine.
È importante comprendere che qualunque modifica apportata allo stato della licenza non modifica il file WindowsStoreProxy.xml, ma soltanto l'oggetto in memoria. Questo significa che, la prossima volta che l'app verrà lanciata, troverai che il contenuto originale del file non è cambiato.
Usare una licenza custom
Per poter testare i diversi stati possibili della licenza di un'app, è possibile creare la propria definizione XML per usarla assieme all'oggetto CurrentAppSimulator
. Il prossimo esempio mostra un esempio di licenza custom che definisce un semplice trial a tempo, la cui scadenza è fissata per il 19 giugno 2014, e senza prodotti acquistabili tramite in-app purchase (su questi ultimi vedremo più avanti esempi specifici).
<?xml version="1.0" encoding="utf-16" ?>
<CurrentApp>
<ListingInformation>
<App>
<AppId>01234567-1234-1234-1234-0123456789AB</AppId>
<LinkUri>http://apps.microsoft.com/webpdp/app/01234567-1234-1234-1234-0123456789AB</LinkUri>
<CurrentMarket>en-US</CurrentMarket>
<AgeRating>3</AgeRating>
<MarketData xml:lang="en-us">
<Name>Timed Trial</Name>
<Description>Timed Trial Sample</Description>
<Price>1.00</Price>
<CurrencySymbol>$</CurrencySymbol>
<CurrencyCode>USD</CurrencyCode>
</MarketData>
</App>
</ListingInformation>
<LicenseInformation>
<App>
<IsActive>true</IsActive>
<IsTrial>true</IsTrial>
<ExpirationDate>2014-06-19T09:00:00.00Z</ExpirationDate>
</App>
</LicenseInformation>
</CurrentApp>
Il metodo ReloadSimulatorAsync
della classe CurrentAppSimulator
accetta come parametro un oggetto StorageFile
che rappresenta il file XML custom contenente i valori da simulare in un particolare scenario. Per mostrare le nuove informazioni, è sufficiente modificare il codice precedente come segue:
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
await this.LoadCustomSimulator();
await this.DisplayListingInformation();
await this.DisplayLicenseInfo();
}
private async Task LoadCustomSimulator()
{
StorageFolder proxyDataFolder = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFolderAsync("trial-configs");
StorageFile proxyFile = await proxyDataFolder.GetFileAsync("myLicense.xml");
await CurrentAppSimulator.ReloadSimulatorAsync(proxyFile);
}
La prossima immagine mostra il risultato.
L'acquisto di un'app
Adesso che la nostra versione di prova è pronta, il passo successivo è quello di permettere all'utente di acquistare la versione full dell'applicazione. Per raggiungere questo risultato, la classe CurrentApp
(o CurrentAppSimulator
) espone il metodo statico RequestAppPurchaseAsync
. Questo metodo accetta come parametro un valore booleano che indica se il metodo debba o meno restituire una ricevuta per l'acquisto (vedremo più avanti come gestire le ricevute, per il momento passiamo un valore false
al metodo).
private async void BuyButton_Click(object sender, RoutedEventArgs e)
{
try
{
await CurrentAppSimulator.RequestAppPurchaseAsync(false);
}
catch (Exception ex)
{
PurchaseErrorMessage.Text = String.Format("Impossibile acquistare: {0}", ex.Message);
PurchaseErrorMessage.Visibility = Windows.UI.Xaml.Visibility.Visible;
}
}
Nella pagina XAML, aggiungete le righe evidenziate:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<TextBlock x:Name="AppId" FontSize="20" Margin="20, 5" />
<TextBlock x:Name="StoreLink" FontSize="20" Margin="20, 5" />
<TextBlock x:Name="AppName" FontSize="20" Margin="20, 5" />
<TextBlock x:Name="LicenseState" FontSize="20" Margin="20, 5"/>
<TextBlock x:Name="LicenseRemainingDays" FontSize="20" Margin="20, 5" />
<Button x:Name="BuyButton" Click="BuyButton_Click" Content="Acquista l'app" Margin="20, 5" />
<TextBlock x:Name="PurchaseErrorMessage" FontSize="20" Margin="20, 5" Visibility="Collapsed" />
</StackPanel>
</Grid>
Se adesso lanciate l'applicazione e cliccate sul pulsante, vedrete apparire un dialog in cui vi verrà richiesto di scegliere il codice di errore da restituire al chiamante. In questo modo, è possibile simulare e testare i vari scenari possibili. Per il momento, lasciate sul valore di default (S_OK, che significa che l'operazione di acquisto è riuscita) e cliccate su Continue.
Dopo aver acquistato la versione full dell'app, è necessario aggiornare le informazioni a schermo in modo che riflettano il nuovo stato della licenza. Una strada è quella di sottoscrivere l'evento LicenseChanged
esposto dalla classe LicenseInformation.
Questo evento viene sollevato ogni volta che lo stato della licenza viene modificato.
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
await this.LoadCustomSimulator();
await this.DisplayListingInformation();
await this.DisplayLicenseInfo();
CurrentAppSimulator.LicenseInformation.LicenseChanged += LicenseInformation_LicenseChanged;
}
private async void LicenseInformation_LicenseChanged()
{
await this.DisplayLicenseInfo();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
CurrentAppSimulator.LicenseInformation.LicenseChanged -= LicenseInformation_LicenseChanged;
}
La prossima immagine mostra lo stato della licenza dopo che l'app è stata acquistata dallo Store.
Finora abbiamo visto come implementare un semplice trial a tempo. In questo scenario, la proprietà IsActive
indica se il periodo di prova è o meno concluso (o, in rari casi, se la licenza è mancante o è stata revocata). La proprietà IsTrial
, invece, serve per verificare se l'utente abbia o meno acquistato la versione full dell'applicazione ovvero se si trova ancora in modalità trial (ricorda che questa proprietà è impostata a true
anche se il periodo di prova è scaduto). Nel caso in cui l'app si trovi ancora in modalità di prova, è buona prassi informare gli utenti circa il numero di giorni che mancano alla scadenza del periodo di prova. Il pattern da usare in un trial a tempo è il seguente:
if (licenseInfo.IsActive)
{
if (licenseInfo.IsTrial)
{
// L'app si trova ancora in modalità trial.
// Ricorda di informare gli utenti circa il numero di giorni rimanenti
}
else
{
// L'utente ha comprato la versione full dell'app
}
}
else
{
// Il periodo di prova è scaduto (o la licenza è mancante o è stata revocata)
// Ricorda di invitare gli utenti ad acquistare la versione full dell'app
}
Nel caso di un feature-based trial, invece, il pattern da seguire è parzialmente diverso, perché in questo caso non c'è alcuna data di scadenza da verificare. In questo scenario, la proprietà IsActive
dovrebbe sempre essere true
(a meno che non sia successo qualcosa di grave: la licenza è mancante o è stata revocata). In questo caso, quello che vogliamo sapere è solo se l'utente ha comprato o meno la versione full dell'app in modo da decidere quali funzionalità abilitare. Il pattern si presenta come segue:
if (licenseInfo.IsActive)
{
if (licenseInfo.IsTrial)
{
// L'app si trova in modalità trial.
// Abilita solo una parte delle funzionalità disponibili.
}
else
{
// L'utente ha comprato la versione full dell'app.
// Puoi abilitare tutte le funzionalità
}
}
else
{
// Qualcosa è andato storto: la licenza manca o è stata revocata
}
Gestire gli errori
Dal momento che la classe CurrentApp
interagisce con le API del Windows Store, è importante essere pronti a intercettare e gestire qualunque eccezione. Per questo motivo, ogni chiamata ai metodi della classe dovrebbe essere inserita in un blocco try/catch (alla gestione delle eccezioni, in particolare quelle asincrone, è appositamente dedicato un articolo di questa guida).
Per essere sicuri che la nostra app sia in grado di gestire eventuali errori nel processo di acquisto, la strada più semplice è quella di sfruttare il dialog con i codici di errore visto sopra, il quale viene mostrato da WinRT ogni volta che usiamo la classe CurrentAppSimulator
per acquistare un'app o un prodotto.
Questo approccio, oltre che limitato, è anche impossibile da utilizzare nell'ambito di una strategia di test automatizzata. Per ovviare a questo limite, è possibile passare all'oggetto CurrentAppSimulator
una definizione XML custom della licenza con dentro un elemento Simulation
che specifichi quale tipo codice di errore ciascun metodo dovrebbe restituire (il codice S_OK indica invece che l'operazione si è conclusa senza errori). Il prossimo snippet mostra una versione modificata della definizione custom utilizzata in precedenza.
<?xml version="1.0" encoding="utf-16" ?>
<CurrentApp>
<ListingInformation>
<App>
<AppId>01234567-1234-1234-1234-0123456789AB</AppId>
<LinkUri>http://apps.microsoft.com/webpdp/app/01234567-1234-1234-1234-0123456789AB</LinkUri>
<CurrentMarket>en-US</CurrentMarket>
<AgeRating>3</AgeRating>
<MarketData xml:lang="en-us">
<Name>Timed Trial</Name>
<Description>Timed Trial Sample</Description>
<Price>1.00</Price>
<CurrencySymbol>$</CurrencySymbol>
<CurrencyCode>USD</CurrencyCode>
</MarketData>
</App>
</ListingInformation>
<LicenseInformation>
<App>
<IsActive>true</IsActive>
<IsTrial>true</IsTrial>
<ExpirationDate>2014-06-19T09:00:00.00Z</ExpirationDate>
</App>
</LicenseInformation>
<Simulation SimulationMode="Automatic">
<DefaultResponse MethodName="RequestAppPurchaseAsync_GetResult" HResult="E_FAIL"/>
<DefaultResponse MethodName="RequestProductPurchaseAsync_GetResult" HResult="E_FAIL"/>
<DefaultResponse MethodName="LoadListingInformationAsync_GetResult" HResult="E_FAIL"/>
</Simulation>
</CurrentApp>
L'elemento Simulation
specifica il codice di errore da restituire quando uno dei metodi elencati viene invocato. Quando l'attributo SimulationMode
è impostato su Automatic
(l'alternativa è Interactive
), il codice di errore viene restituito automaticamente non appena il metodo indicato nell'attributo MethodName
viene invocato. In questo caso, il dialog box con i codici di errore non verrà mostrato.
Sono solo tre i metodi della classe CurrentAppSimulator
per i quali è possibile utilizzare questo meccanismo: RequestAppPurchaseAsync
, RequestProductPurchaseAsync
e LoadListingInformationAsync
(tutti e tre indicati nel file XML con il suffisso _GetResult
). Nello sviluppare la vostra app, tenete tuttavia presente che, stando alla documentazione ufficiale su MSDN, il metodo RequestAppPurchaseAsync
non solleva alcuna eccezione se l'operazione non è stata completata perché la connessione a internet non era disponibile, l'utente ha cancellato l'operazione o l'autenticazione dell'utente non è riuscita. In altri termini, il successo dell'operazione significa solamente che non ci sono stati errori. Per questa ragione, è necessario sempre controllare successivamente lo stato della licenza per verificare che l'acquisto sia andato a buon fine.