André Alves de Lima

Talking about Software Development and more…

Explorando as in-app purchases em aplicativos para a Windows Store

Um dos tópicos mais interessantes que podemos abordar quando falamos sobre o desenvolvimento de aplicativos para a Windows Store é a questão da monetização da aplicação. No universo dos aplicativos para a Windows Store, você pode monetizar o seu aplicativo de diversas formas. Você pode:

  • Cobrar pelo aplicativo (oferecendo um período de trial, caso deseje)
  • Exibir propagandas utilizando o Microsoft Advertising SDK (ou outra engine de propagandas que você preferir)
  • Cobrar para desbloquear features específicas do seu aplicativo

E é justamente esse terceiro tópico que vamos abordar no artigo de hoje: as in-app purchases, ou compras de dentro do seu aplicativo.

Essa estratégia de disponibilizar seu aplicativo gratuitamente e então cobrar para destravar features específicas é muito utilizada no desenvolvimento de aplicativos móveis, não só na plataforma Microsoft, como nos concorrentes App Store e Google Play, ou até mesmo nos apps disponíveis pelo Facebook. Muita gente tem ficado rica fazendo aplicações (e principalmente jogos) em que você paga uma mixaria para destravar certas funcionalidades, para fazer com que o jogo fique mais fácil, comprar vidas adicionais, etc. A boa notícia para nós desenvolvedores de aplicativos para a Windows Store é que a Microsoft facilitou e muito a nossa vida com a API super simples de in-app purchases no WinRT.

Para entendermos como funciona todo esse processo, vamos começar criando um projeto em branco para a Windows Store, utilizando o template “Blank App (XAML)”. Nesse projeto, vamos criar uma interface que simule um aplicativo contendo um anúncio na parte inferior da página. A ideia é que o usuário possa fazer a compra da feature “remover anúncios”, de forma que o anúncio seja escondido quando o usuário tiver comprado essa funcionalidade. Obviamente, o foco deste artigo não é abordar a questão de mostrar anúncios em um aplicativo, mas sim mostrar como lidar com compras de funcionalidades. Portanto, vamos simular um anúncio utilizando uma imagem qualquer. Altere o código do grid interno da MainPage que foi criada automaticamente para o seguinte:

<Page x:Class="ExemploWinRT_InAppPurchase.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:ExemploWinRT_InAppPurchase"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">
    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2*" />
            <ColumnDefinition Width="8*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="8*" />
            <RowDefinition Height="2*" />
        </Grid.RowDefinitions>
        <StackPanel Orientation="Vertical">
            <Button Content="Remover anúncio"
                    HorizontalAlignment="Stretch"
                    Margin="5" />
            <TextBlock TextWrapping="Wrap"
                       Text="Anúncio Removido!"
                       Style="{StaticResource SubheaderTextStyle}"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center" />
        </StackPanel>
        <StackPanel Orientation="Vertical"
                    Grid.Row="1"
                    Grid.ColumnSpan="2">
            <TextBlock TextWrapping="Wrap"
                       Text="Anúncio:"
                       Style="{StaticResource SubtitleTextStyle}"
                       VerticalAlignment="Top"
                       HorizontalAlignment="Center" />
            <Image Source="http://www.brain4dev.com/wp-content/uploads/2012/01/Brain4Dev140.png"
                   Height="116">
            </Image>
        </StackPanel>
    </Grid>
</Page>

Observe que construímos uma página bem simples, com um suposto anúncio na parte inferior e um botão para que o usuário possa fazer a compra da feature para remover o anúncio.

O próximo passo é criarmos uma classe que será responsável pelo gerenciamento da licença do aplicativo. Vamos chamar essa classe de LicenseHelper. Nessa classe, vamos ter uma propriedade read-only (somente getter) que fará a exposição da licença como um todo e outra propriedade que indicará se o usuário já comprou a feature para a remoção do anúncio. Um ponto extremamente importante nessa classe é que nós adicionamos um alias de namespace chamado CurrentAppInfo. Nós fazemos isso porque a API de licenças do WinRT possui um simulador que pode ser utilizado para testar as compras de features no seu aplicativo (CurrentAppSimulator) e também possui a classe verdadeira para gerenciar as compras (CurrentApp). Ambas as classes são estáticas e possuem a mesma estrutura, portanto, para evitar que tenhamos que fazer um find e replace de CurrentAppSimulator para CurrentApp antes de fazer o deploy para a Windows Store, podemos simplesmente alterar o apontamento nesse alias de namespace e o aplicativo se comportará da maneira esperada:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CurrentAppInfo = Windows.ApplicationModel.Store.CurrentAppSimulator; // Declaração para facilitar a troca entre CurrentApp e CurrentAppSimulator.

namespace ExemploWinRT_InAppPurchase
{
    /// <summary>
    /// Classe auxiliar para gerenciar as licenças do produto.
    /// </summary>
    public static class LicenseHelper
    {
        #region Atributos e Propriedades
        /// <summary>
        /// Retorna a licença (LicenseInformation) de CurrentApp (ou CurrentAppSimulator quando o simulador está sendo utilizado).
        /// </summary>
        public static Windows.ApplicationModel.Store.LicenseInformation Licenca
        {
            get
            {
                return CurrentAppInfo.LicenseInformation;
            }
        }
        /// <summary>
        /// Indica se o usuário atual adquiriu a licença "ANUNCIO_REMOVIDO" ou não.
        /// </summary>
        public static bool AnuncioRemovido
        {
            get
            {
                return Licenca.ProductLicenses["ANUNCIO_REMOVIDO"].IsActive;
            }
        }
        #endregion
    }
}

Pois bem, vamos entender com maiores detalhes o que está sendo feito no código acima apresentado. O alias de namespace chamado CurrentAppInfo está atualmente apontando para Windows.ApplicationModel.Store.CurrentAppSimulator. Essa é a classe que utilizamos quando estamos testando o nosso aplicativo (antes do deploy para a Windows Store). É importante que, caso você prossiga com o deploy para a Windows Store, você precisa alterar o apontamento desse alias de namespace para Windows.ApplicationModel.Store.CurrentApp, que é a classe real utilizada para o gerenciamento de licenças quando seu aplicativo estiver publicado na Windows Store.

As classes CurrentAppSimulator e CurrentApp possuem uma propriedade chamada LicenseInformation que, como o próprio nome diz, contém as informações da licença do usuário que está executando o aplicativo no momento. Essa propriedade possui, entre outras informações, um Dictionary contendo as informações sobre as features que o usuário comprou. Portanto, criamos também a propriedade “AnuncioRemovido” que checa se a feature “ANUNCIO_REMOVIDO” está ativa ou não para esse usuário (IsActive).

Vamos agora adicionar nessa classe também o método que possibilitará a compra dessa feature:

        #region Métodos
        /// <summary>
        /// Solicita a compra da licença "ANUNCIO_REMOVIDO" caso o usuário ainda não a tenha adquirido.
        /// </summary>
        /// <returns>A async Task do processo de compra da licença.</returns>
        public static async Task RemoverAnuncioAsync()
        {
            try
            {
                if (!AnuncioRemovido)
                    await CurrentAppInfo.RequestProductPurchaseAsync("ANUNCIO_REMOVIDO", false);
            }
            catch { }
        }
        #endregion

Veja que as classes CurrentAppSimulator e CurrentApp possuem um método chamado RequestProductPurchaseAsync, que recebe o nome da feature que está sendo solicitada para compra (no nosso caso “ANUNCIO_REMOVIDO”). Observe também que esse método é assíncrono, por isso tivemos que adicionar a keyword “async” na assinatura do método RemoverAnuncio e a keword “await” na chamada do método RequestProductPurchaseAsync. Ao utilizarmos a classe CurrentAppSimulator, esse método exibirá uma interface em que poderemos escolher entre os possíveis retornos de uma compra de feature pela Windows Store:

Já ao utilizarmos a classe CurrentApp (e não CurrentAppSimulator), o método exibirá a interface real de compra de features pela Windows Store. Note que esse método só funcionará utilizando a classe CurrentApp caso seu aplicativo esteja devidamente publicado na Windows Store e a feature esteja cadastrada no manifesto de publicação da aplicação (veremos esse procedimento mais pra frente):

Agora que já temos a classe que fará o gerenciamento das licenças do nosso aplicativo, chegou a hora de fazermos a ligação das funcionalidades dessa classe com a interface do usuário. Como estamos trabalhando com um aplicativo para a Windows Store utilizando XAML, vamos utilizar as boas práticas e trabalhar com o modelo arquitetural MVVM. Esse modelo indica a necessidade da criação de uma ViewModel que fará a ligação do nosso Model (nesse caso a nossa classe LicenseHelper) e a nossa View (nesse caso a MainPage). Para facilitar esse processo, vamos utilizar o toolkit chamado MVVM Light. Vamos então adicionar esse toolkit ao nosso projeto, clicando com o botão direito em referências e escolhendo a opção “Manage NuGet Packages”:

Na janela que se abre, digite “mvvm light” na caixa de busca e escolha a opção para instalar o toolkit e siga as instruções de instalação:

Uma vez seguido esses passos, tente compilar seu projeto e veja que você receberá um erro de compilação:

Esse é um erro conhecido ao utilizarmos o toolkit MVVM Light em aplicativos para a Windows Store, porque a maneira de declaração de namespaces na versão final do WinRT foi alterado e o toolkit ainda não foi ajustado para estar de acordo com essa alteração. Porém, para consertar esse erro, basta dar um duplo clique no erro e acertar a declaração do namespace “vm” em App.xaml, substituindo:

xmlns:vm=”clr-namespace:NOMEDOSEUPROJETO.ViewModel”

por

xmlns:vm=”using:NOMEDOSEUPROJETO.ViewModel”

Outra biblioteca de terceiros que vamos precisar para facilitar a nossa vida é a WinRTConverters, uma vez que o WinRT não possui converters básicos como o BooleanToVisibilityConverter (presente no WPF por padrão). Você pode instalá-la também utilizando o NuGet Package Manager:

Pronto! Agora já temos todas as bibliotecas externas necessárias para fazer nosso aplicativo funcionar do jeito que queremos. O próximo passo é alterar a classe MainViewModel, que está localizada dentro da pasta ViewModel no nosso projeto. Nela vamos criar um atalho para a propriedade AnuncioRemovido da nossa classe LicenseHelper (para que possamos ter suporte a change notification) e também vamos criar o comando que fará a chamada do método RemoverAnuncio da nossa classe LicenseHelper:

    /// <summary>
    /// ViewModel contendo as propriedades e comandos a serem bindados na Main View.
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        #region Atributos e Propriedades
        /// <summary>
        /// Atalho para a propriedade AnuncioRemovido da classe LicenseHelper. Adicionada na ViewModel para suportar change notification quando
        /// o usuário compra a licença de "ANUNCIO_REMOVIDO".
        /// </summary>
        public bool AnuncioRemovido
        {
            get
            {
                return LicenseHelper.AnuncioRemovido;
            }
        }
        #endregion

        #region Construtores
        /// <summary>
        /// Constroi uma instância de MainViewModel, inicializando os comandos necessários.
        /// </summary>
        public MainViewModel()
        {
            RemoverAnuncio = new GalaSoft.MvvmLight.Command.RelayCommand(RemoverAnuncioAction, CanExecuteRemoverAnuncio);
        }
        #endregion

        #region Comandos
        #region Remover Anúncio
        /// <summary>
        /// Comando que pode ser utilizado para realizar a compra da licença de remoção do anúncio.
        /// </summary>
        public GalaSoft.MvvmLight.Command.RelayCommand RemoverAnuncio { get; private set; }
        /// <summary>
        /// Ação que é executada quando o comando RemoverAnuncio é disparado.
        /// </summary>
        private async void RemoverAnuncioAction()
        {
            await LicenseHelper.RemoverAnuncioAsync().ContinueWith(
                (task) => GalaSoft.MvvmLight.Threading.DispatcherHelper.RunAsync(
                    () =>
                    {
                        RaisePropertyChanged("AnuncioRemovido");
                        RemoverAnuncio.RaiseCanExecuteChanged();
                    }));
        }
        /// <summary>
        /// Indica se o comando RemoverAnuncio pode ser executado ou não.
        /// Ele pode ser executado quando a licença da remoção do anúncio ainda não tiver sido comprada.
        /// </summary>
        /// <returns>True se o comando RemoverAnuncio pode ser executado. Senão retorna false.</returns>
        private bool CanExecuteRemoverAnuncio()
        {
            return !LicenseHelper.AnuncioRemovido;
        }
        #endregion
        #endregion
    }

No código acima você pode notar que criamos simplesmente um atalho para a propriedade AnuncioRemovido da classe LicenseHelper. Tivemos que adicionar esse atalho porque precisamos implementar suporte a change notification nessa propriedade e, como ela é estática na classe LicenseHelper, isso não seria possível. Além disso, criamos também um comando que será utilizado para fazer a remoção do anúncio quando o usuário clicar no botão “Remover Anúncio” na nossa MainPage. Ele simplesmente chama o método RemoverAnuncioAsync da nossa classe LicenseHelper e, logo que essa operação for concluída, chamamos o método RaisePropertyChanged informando a propriedade “AnuncioRemovido” como parâmetro (esse método é herdado da classe ViewModelBase do MVVM Light toolkit). Precisamos chamar o RaisePropertyChanged para que os controles bindados com essa propriedade sejam devidamente atualizados. Também dentro do bloco ContinueWith (ou seja, quando a chamada assíncrona terminar), chamamos o método RaiseCanExecuteChanged do nosso comando, de forma que o controle que esteja bindado com esse comando (no nosso caso o botão “Remover Anúncio”) seja habilitado ou desabilitado de acordo com o conteúdo do método CanExecuteRemoverAnuncio.

Mais um detalhe importante que devemos observar no método RemoverAnuncioAction é o fato que o bloco dentro do ContinueWith precisa ser executado através do Dispatcher. Isso se deve ao fato que esse código está de certa forma interagindo com a interface do usuário e, por não estar sendo executado na thread de UI, precisamos utilizar o Dispatcher para evitar problema de cross threading.

Enfim, os últimos detalhes que temos que acertar são relacionados à nossa MainPage. Nela precisamos configurar o DataContext como sendo a nossa MainViewModel. Fazemos isso utilizando o ViewModelLocator do MVVM Light toolkit. Veja como fica a sintaxe do DataContext na declaração da nossa Page:

<Page x:Class="ExemploWinRT_InAppPurchase.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:ExemploWinRT_InAppPurchase"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"
      DataContext="{Binding Main, Source={StaticResource Locator}}">

Além disso, vamos configurar um binding na propriedade Command do nosso botão para que ela considere o comando RemoverAnuncio presente na nossa ViewModel. E também vamos configurar um binding na propriedade Visibility do TextBlock “Anúncio Removido!” de forma que ele só apareça quando o anúncio estiver, de fato, removido:

            <Button Content="Remover anúncio"
                    HorizontalAlignment="Stretch"
                    Margin="5"
                    Command="{Binding Path=RemoverAnuncio}" />
            <TextBlock TextWrapping="Wrap"
                       Text="Anúncio Removido!"
                       Style="{StaticResource SubheaderTextStyle}"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       Visibility="{Binding AnuncioRemovido, Converter={StaticResource BooleanToVisibilityConverter}}" />

Por fim, temos que configurar um binding na propriedade Visibility do StackPanel onde o nosso anúncio está localizado, de forma que o anúncio desapareça quando o usuário comprar a feature de remoção do anúncio:

        <StackPanel Orientation="Vertical"
                    Grid.Row="1"
                    Grid.ColumnSpan="2">
            <TextBlock TextWrapping="Wrap"
                       Text="Anúncio:"
                       Style="{StaticResource SubtitleTextStyle}"
                       VerticalAlignment="Top"
                       HorizontalAlignment="Center" />
            <Image Source="http://www.brain4dev.com/wp-content/uploads/2012/01/Brain4Dev140.png"
                   Height="116">
            </Image>
            <StackPanel.Visibility>
                <Binding Path="AnuncioRemovido"
                         Converter="{StaticResource BooleanToVisibilityConverter}">
                    <Binding.ConverterParameter>
                        <x:Boolean>True</x:Boolean>
                    </Binding.ConverterParameter>
                </Binding>
            </StackPanel.Visibility>
        </StackPanel>

Agora, no code behind da nossa MainPage, temos que inicializar o DispatcherHelper do MVVM Light toolkit. Isso é muito simples de ser feito, simplesmente precisamos adicionar a chamada do método DispatcherHelper.Initialize no construtor da MainPage:

        public MainPage()
        {
            this.InitializeComponent();
            GalaSoft.MvvmLight.Threading.DispatcherHelper.Initialize();
        }

Se você tentar compilar o código do jeito que está, provavelmente ele não compilará. Ou, se ele compilar, você receberá uma exceção em tempo de execução. Isso se deve ao fato que nós utilizamos o BooleanToVisibilityConverter em alguns dos nossos bindings, mas, nós ainda não o declaramos. Portanto, vá até App.xaml e adicione a declaração de uma instância de BooleanToVisibilityConverter na tag de resources. E não esqueça da declaração do namespace “visibilityconverters” no header da Application:

<?xml version="1.0" encoding="utf-8"?>
<Application x:Class="ExemploWinRT_InAppPurchase.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="using:ExemploWinRT_InAppPurchase"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="using:ExemploWinRT_InAppPurchase.ViewModel"
             xmlns:visibilityconverters="using:WinRTConverters.Visibility"
             mc:Ignorable="d">

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>

                <!-- 
                    Styles that define common aspects of the platform look and feel
                    Required by Visual Studio project and item templates
                 -->
                <ResourceDictionary Source="Common/StandardStyles.xaml" />
            </ResourceDictionary.MergedDictionaries>

            <vm:ViewModelLocator x:Key="Locator"
                                 d:IsDataSource="True" />
            <visibilityconverters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
        </ResourceDictionary>
    </Application.Resources>
</Application>

Pronto! Agora sim o nosso projeto de exemplo está pronto. Tente executá-lo e clicar no botão “Remover Anúncio”. Mas, por que é que não funcionou? Porque, não sei por qual razão, o default do CurrentAppSimulator é considerar que o aplicativo é trial. Nesse cenário, as compras de features não funcionam no simulador. Enfim, para resolver essa questão, precisamos editar o arquivo utilizado pelo simulador e indicar que o aplicativo não é trial. O arquivo está localizado dentro da seguinte pasta:

%LocalAppData%\Packages\<PACKAGE FAMILY NAME>\LocalState\Microsoft\Windows Store\ApiData

Você encontra o Package Family Name no manifesto do projeto, dentro da seção Packaging:

Dentro dessa pasta, abra o arquivo WindowsStoreProxy.xml e altere a tag CurrentApp.LicenseInformation.App.IsTrial para false:

Execute novamente o projeto e veja que agora ele funciona. Porém, como você pode notar a informação de que o usuário comprou a feature de remover os anúncios não é salva no arquivo. Esse é o comportamento do CurrentAppSimulator. Se você quiser simular a situação em que o usuário executa a aplicação e já tenha comprado essa feature anteriormente, você pode adicionar o seguinte bloco no arquivo WindowsStoreProxy.xml:

Até aqui trabalhamos somente com o CurrentAppSimulator. Entretanto, como mencionei anteriormente, antes de fazer o deploy para a Windows Store, você precisa alterar o alias de namespace para utilizar o CurrentApp, e não o Simulator. Além disso, você precisa ir até o DevCenter para Windows Store Apps, editar o release da sua aplicação e, na seção de Services, adicione a in-app purchase desejada, definindo o preço e outros detalhes:

E aí é só publicar o aplicativo e aguardar a aprovação da Windows Store. Finalmente você acabou de implementar com sucesso sua primeira in-app purchase! Espero que vocês tenham gostado e que ganhem muito dinheiro implementando essa funcionalidade no aplicativo de vocês.

Como sempre, você encontra o código desse exemplo na MSDN Code Gallery, além de encontrá-lo também no GitHub, onde você pode clonar o repositório e fazer as alterações que quiser.

Até o próximo artigo!

André Lima

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *