Report: State of in-app subscriptions in the US 2023 Get a report

As novidades sobre a API StoreKit 2 e como a Apple simplificou a integração de compras no aplicativo

Kirill Potekhin

Updated: March 20, 2023

Content

62fdf0da8b35ff3ba074dbce jp android tutorial 1 configuration 5

A Apple apresentou uma nova versão do StoreKit 2 durante a WWDC 2021, realizada recentemente. Trata-se de um framework responsável pela realização de compras (purchases) no iOS. Uma parte dos aplicativos (apps) com recursos de assinatura (subscription) e compra no aplicativo (in-app purchase) cresce continuamente, e a Apple simplificou significativamente a integração das compras no aplicativo, lançando o StoreKit 2. Hoje, vamos avaliar o trabalho com o StoreKit 2 por parte do servidor, em outras palavras, com a ajuda da API do App Store Server.

Solicitar autenticação

Na versão atua da API, você precisa do Segredo Compartilhado (Shared Secret) para enviar uma solicitação. Trata-se de uma string fixa secreta que pode ser obtida na App Store Connect. Uma nova versão da API usa o padrão JSON Web Token (JWT) para autenticar a solicitação.   

Geração de chaves 

Antes de tudo, crie uma chave privada que será usada para autorizar as solicitações. Abra a App Store Connect e navegue até a seção Usuários e Acesso, depois clique na aba Chaves. Selecione o tipo de chave para compras no aplicativo. Faça o download de uma nova chave. Você também vai precisar de sua ID – você pode copiá-la na mesma página do Issue ID que pode ser encontrada na aba da API da App Store Connect.

Creating a private key to work with StoreKit 2
Criação de uma chave privada para trabalhar com a API da App Store Server

Como criar um token

O próximo passo é criar um token que será usado para autorizar as solicitações. Este processo é descrito detalhadamente na documentação, portanto não é necessário dedicar muita atenção a ele neste artigo. Segue um exemplo de uma implementação já feita no Python. Vale destacar que não faz sentido gerar um novo token para cada nova solicitação. Ao criar um token, você fixa sua vida útil em até 60 minutos e usa o mesmo token durante esse período. 

import time, uuid
from authlib.jose import jwt

BUNDLE_ID = 'com.adapty.sample_app'
ISSUER_ID = '4336a124-f214-4d40-883b-6db275b5e4aa'
KEY_ID = 'J65UYBDA74'
PRIVATE_KEY = '''
-----BEGIN PRIVATE KEY-----
MIGTAgMGByqGSMBHkAQQgR/fR+3Lkg4...
-----END PRIVATE KEY-----
'''

issue_time = round(time.time())
expiration_time = issue_time + 60 * 60 # 1 hour expiration


header = {
 'alg': 'ES256',
 'kid': KEY_ID,
 'typ': 'JWT'
}

payload = {
 'iss': ISSUER_ID,
 'iat': issue_time,
 'exp': expiration_time,
 'aud': 'appstoreconnect-v1',
 'nonce': str(uuid.uuid4()),
 'bid': BUNDLE_ID
}

token_encoded = jwt.encode(header, payload, PRIVATE_KEY)
token_decoded = token_encoded.decode()

authorization_header = {
 'Authorization': f'Bearer {token_decoded}'
}

Transações assinadas

Em uma nova versão da API, todas as transações retornam no padrão JSON Web Signature (JWS). É uma string que consiste em três partes divididas por pontos.  

  1. Cabeçalho base64.
  2. Carga útil da transação com base64.
  3. Assinatura da transação.
Base64(header) + "." + Base64(payload) + "." + sign(Base64(header) + "." + Base64(payload))

Cabeçalho da transação

É necessário fazer um cabeçalho para garantir que a transação seja autêntica. A chave Alg contém um algoritmo de criptografia, a chave x5c contém uma cadeia de certificados.

{
  "kid": "AMP/DEV",
  "alg": "ES256",
  "x5c": [
    "MIIEO...",
    "MIIDK..."
  ]
}

Carga útil da transação

{
  "transactionId": "1000000831360853",
  "originalTransactionId": "1000000806937552",
  "webOrderLineItemId": "1000000063561721",
  "bundleId": "com.adapty.sample_app",
  "productId": "basic_subscription_1_month",
  "subscriptionGroupIdentifier": "27636320",
  "purchaseDate": 1624446341000,
  "originalPurchaseDate": 1619686337000,
  "expiresDate": 1624446641000,
  "quantity": 1,
  "type": "Auto-Renewable Subscription",
  "appAccountToken": "fd12746f-2d3a-46c8-bff8-55b75ed06aca",
  "inAppOwnershipType": "PURCHASED",
  "signedDate": 1624446484882,
  "offerType": 2,
  "offerIdentifier": "basic_subscription_1_month.pay_as_you_go.3_months"
}

A Apple mudou e ampliou o formato da transação. Do meu ponto de vista, agora, ficou mais prático trabalhar com eles. Você pode obter mais informações sobre um novo formato na documentação. A seguir, detalharei as mudanças mais importantes.    

  • A Apple adicionou o campo appAccountToken, que contém o ID de usuário do seu sistema. Este ID deve estar no formato UUID, que é definido no aplicativo móvel quando uma compra está sendo iniciada. Caso esteja definido, será retornará em todas as transações desta cadeia (renovação, problemas de cobrança, etc.), e você saberá rapidamente qual usuário fez uma compra.    
  • A Apple também adicionou os campos offerType e offerIdentifier que contêm as informações sobre uma oferta usada (se for o caso). Os valores para o campo offerType são os seguintes:
  • 1 — oferta inicial (disponível apenas para os usuários sem assinaturas ativas ou com assinaturas expiradas)
  • 2 — oferta promocional (disponível apenas para assinaturas atuais e expiradas)
  • 3 — código de oferta

Caso uma oferta promocional ou código de oferta tenha sido usado, a chave offerIdentifier conterá a identificação da oferta usada. No passado, era impossível rastrear o uso da oferta no lado do servidor (server-side), o que piorava o processo de analytics. Agora, é possível usar códigos de oferta para fins analíticos.   

  • A Apple adicionou o campo inAppOwnershipType, o que ajuda a entender se um usuário comprou um produto ou o acessou com uma assinatura familiar. Possíveis valores:
  • PURCHASED
  • FAMILY_SHARED
  • Um novo campo – tipo – inclui o tipo de transação. Possíveis valores:
  • Assinatura renovável automaticamente
  • Não consumível
  • Consumível
  • Assinatura não renovada
  • Os campos Cancellation_date e cancel_reason têm agora novos nomes: revocationDate e revocationReason. A título de lembrete, eles contêm uma data e um motivo para a revogação da assinatura como resultado de um reembolso, de modo que o novo nome parece mais lógico.
  • Todas as chaves retornam no formato camelCase (assim como em todas as solicitações da API do App Store Server).
  • Todas as datas são exibidas no formato Unix timestamp em milissegundos.   

Subscribe to Adapty newsletter

Get fresh paywall ideas, subscription insights, and mobile app news every month!

Status de assinatura do usuário

Para verificar o status da assinatura, do usuário atual, envie uma solicitação GET para https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}, onde {originalTransactionId} é a identificação de qualquer cadeia de transação do usuário. Em contrapartida, você receberá transações com status para cada grupo de assinaturas.   

{
  "environment": "Sandbox",
  "bundleId": "com.adapty.sample_app",
  "data": [
    {
      "subscriptionGroupIdentifier": "39636320",
      "lastTransactions": [
        {
          "originalTransactionId": "1000000819078552",
          "status": 2,
          "signedTransactionInfo": "eyJraWQiOi...",
          "signedRenewalInfo": "eyJraWQiOi..."
        }
      ]
    }
  ]
}

A chave de status exibe o status atual da assinatura, e com base nela, você pode decidir se deve fornecer acesso aos recursos pagos do aplicativo a um determinado usuário. Possíveis valores:

  • 1 — a assinatura está ativa, um usuário pode acessar recursos pagos.
  • 2 — a assinatura expirou, um usuário não pode acessar recursos pagos.  
  • 3 — o status da assinatura é Billing Retry (nova tentativa de cobrança), o que significa que um usuário não a cancelou, mas tem problemas com o pagamento. A Apple tentará fazer a cobrança no cartão durante 60 dias. O usuário não poderá acessar os recursos pagos.  
  • 4 — o status da assinatura é Grace Period (período de carência), o que significa que o usuário não cancelou, mas tem problemas de pagamento. O período de carência funciona na App Store Connect, portanto o usuário vai conseguir acessar os recursos pagos.    
  • 5 — a assinatura foi cancelada como resultado de um reembolso, e o usuário não poderá acessar os recursos pagos. 

A chave SignedTransactionInfo contém as informações sobre a última transação na cadeia. Você pode encontrar os detalhes sobre o formato acima.   

Informações sobre renovação de assinatura  

A chave SignedRenewalInfo contém as informações sobre a renovação da assinatura.

{
  "expirationIntent": 1,
  "originalTransactionId": "1000000819078552",
  "autoRenewProductId": "basic_subscription_1_month",
  "productId": "basic_subscription_1_month",
  "autoRenewStatus": 0,
  "isInBillingRetryPeriod": false,
  "signedDate": 1624520884048
}

As informações nos permitem entender o que acontecerá com a assinatura no próximo período de pagamento. Por exemplo, na eventualidade de um usuário ter cancelado a renovação automática, você pode oferecer uma mudança para outro plano de assinatura ou uma oferta promocional. É conveniente rastrear este tipo de eventos com a ajuda das notificações do servidor, um tema que abordarei em breve.  

Histórico de transações do usuário

Para obter o histórico de transações do usuário, envie uma solicitação GET para https://api.storekit.itunes.apple.com/inApps/v1/history/{originalTransactionId}, onde {originalTransactionId} é a identificação de qualquer cadeia de transações do usuário. Em contrapartida, as transações serão ordenadas por tempo.         

{
	"revision": "1625872984000_1000000212854038",
	"bundleId": "com.adapty.sample_app",
	"environment": "Sandbox",
	"hasMore": true,
	"signedTransactions": [
		"eyJraWQiOiJ...",
		"joiRVMyNeyX...",
		"5MnkvOTlOZl...",
		...
	]
}

Uma solicitação não pode conter mais de 20 transações. Se um usuário tiver mais, o valor da bandeira hasMore (tem mais) será verdadeiro. Caso necessite da próxima página de transação, envie a solicitação novamente com o parâmetro de revisão GET. Ele conterá o valor a partir da mesma chave.  

Notificações de transações no servidor

As notificações do servidor ajudam a obter informações sobre novas compras, renovações, problemas de cobrança, etc. Este procedimento ajuda a desenvolver processos de analytics mais precisos, assim como simplifica o gerenciamento do status do assinante.   

As notificações do servidor existentes (V1) podem resolver a maioria dos problemas, mas às vezes são inapropriados. Na maioria das vezes, trata-se de uma situação em que você recebe várias notificações para uma única ação de um usuário. Por exemplo, agora, quando um usuário atualiza uma assinatura, a Apple envia duas notificações: DID_CHANGE_RENEWAL_STATUS e INTERACTIVE_RENEWAL. Atualmente, para processar este caso, é necessário salvar o status de alguma forma e verificar se a segunda notificação foi enviada. Na nova versão de notificações do servidor (V2), há apenas uma notificação para uma ação de um usuário. É muito mais prático.   

A segunda versão das notificações do servidor apresenta novos eventos – OFFER_REDEEMED, EXPIRED e GRACE_PERIOD_EXPIRED. Tais eventos facilitam muito o gerenciamento do status de assinante. Os eventos SUBSCRIBED e PRICE_INCREASE são um aprimoramento da primeira versão.

Transaction notifications in StoreKit 2 feature new events

Tipos de notificações

As notificações agora têm tipos, portanto, uma única notificação para qualquer ação de um usuário é suficiente para entender o que aconteceu.

Notifications in StoreKit 2 have types

Tipos de notificações

{
  "notificationType": "SUBSCRIBED",
  "subtype": "INITIAL_BUY",
  "version": 2,
  "data": {
    "environment": "Sandbox",
    "bundleId": "com.adapty.sample_app",
    "appAppleId": 739104078,
    "bundleVersion": 1,
    "signedTransactionInfo": "eyJraWQiOi...",
    "signedRenewalInfo": "eyJraWQiOi..."
  }
}

As notificações do servidor contêm informações sobre uma transação e uma renovação no formato JWS que já descrevi anteriormente.  

Como trabalhar com o ambiente Sandbox

Você precisa usar a URL do ambiente Sandbox para testar as compras: https://api.storekit-sandbox.itunes.apple.com.

Uma nova versão das notificações do servidor ainda não está disponível para testes. Assim que estiver disponível, será possível especificar diferentes URLs para notificações de produção e Sandbox. Você pode optar por V2 para Sandbox, e V1 para Produção para a realização de testes. 

Além disso, o App Store Connect permite agora:

  • Limpar o histórico de compras para o usuário Sandbox, o que significa que você não precisa mais criar uma nova conta para fazer isso.   
  • Mudar o país da loja para o usuário Sandbox.
  • Troque o período de renovação da assinatura no Sandbox; por exemplo, é possível fazer uma compra mensal que dura 1 hora ao invés de 5 minutos.     

Conclusão

A Apple melhorou significativamente sua atuação com assinaturas e compras no lado do servidor. Do meu ponto de vista, relaciono abaixo as novas características mais úteis:

  • Oferta promocional completa e suporte para o código de oferta;  
  • Notificações mais simples e mais informativas do servidor;   
  • Uma oportunidade de aprender sobre o status da assinatura atual com uma simples chamada de API;
  • Limpeza do histórico de compras do usuário do Sandbox.   

A mudança para uma nova API não é difícil, é o suficiente para obter aTransactionId original para cada recibo. É provável que já esteja contida em sua base.    

De qualquer forma, a parte mais difícil de integrar as assinaturas em um aplicativo móvel é construir um sistema  de analytics e otimizar a economia. A Adapty pode resolver estes problemas muito bem:

  • A análise integrada nos permite compreender rapidamente as principais métricas do aplicativo.
  • A análise de coorte ajuda a entender se há algum problema em termos de economia.
  • Os testes A/B aumentam a rentabilidade do aplicativo.   
  • As integrações com sistemas externos permitem o envio de transações para serviços de atribuição e analytics de produtos.    
  • Campanhas promocionais reduzem a perda de clientes.   
  • O SDK de código aberto permite integrar assinaturas a um aplicativo em poucas horas.  

Saiba mais sobre estas funcionalidades, para implementar assinaturas em seu aplicativo de forma mais rápida e monetizar seu aplicativo antes e melhor.