Compras no aplicativo para iOS, parte 4: validação de compra do lado do servidor
Updated: March 20, 2023
O que é validação de compra no servidor?
Validação no servidor (validação do recibo do lado do servidor (server-side)) é uma forma de verificar a autenticidade da compra. Ao contrário da validação baseada no dispositivo, a validação no servidor ocorre – aguarde – no lado do servidor. A validação significa que o dispositivo ou o servidor faz uma solicitação aos servidores da Apple para descobrir se a compra realmente ocorreu e se é válida.
Por que validar as compras?
Deve-se salientar que a validação no servidor não é obrigatória — as compras no aplicativo ainda serão realizadas sem ela. No entanto, ela confere algumas vantagens:
- Fazer o analytics avançado do pagamento, o que é particularmente importante no caso de assinaturas, uma vez que tudo o que acontece após a ativação não é processado pelo dispositivo. Sem o processamento da compra no servidor, não é possível recuperar o status atual da assinatura e saber se o usuário renovou ou cancelou a assinatura, se ocorreu algum problema de cobrança, e assim por diante.
- Verificar a autenticidade da compra. Você se certificará de que a transação não é fraudulenta, e que o usuário realmente pagou pelo produto.
- Cruzar assinaturas em diferentes plataformas. Caso seja possível verificar o status da assinatura do usuário em tempo real, é possível sincronizá-la com outras plataformas. Por exemplo, o usuário que adquiriu a assinatura em um dispositivo iOS poderá utilizá-la no Android, na Web e em outras plataformas.
- Conseguir controlar o acesso ao conteúdo do lado do servidor, o que o protege dos usuários que tentam acessar os dados sem assinatura, simplesmente fazendo solicitações ao servidor.
Segundo nossa experiência, a primeira vantagem, por si só, já é suficiente para configurar o processamento de compras no servidor.
Validação de compra
Em geral, o processo de validação de recibos no iOS é assim:
Como gerar o segredo compartilhado
Para enviar um pedido de validação de pagamento, é necessário incluir o segredo compartilhado para autorizar o pedido. Você pode gerar um segredo no App Store Connect.
O segredo compartilhado pode ser criado para um aplicativo específico (segredo específico do aplicativo) ou para todos os aplicativos da conta (segredo primário).
Para gerar um segredo específico para o aplicativo, abra a página do aplicativo na App Store Connect, acesse Compras no aplicativo → Gerenciar e clique em Segredo compartilhado específico para o aplicativo. Na janela que se abre, é possível gerar um novo token ou copiar o token existente.
Para receber o segredo para todos os aplicativos em sua conta, abra a página Usuários e Acesso e selecione a aba Segredo Compartilhado.
Como solicitar uma validação de pagamento
Assim que você receber o segredo compartilhado, pode-se enviar os recibos para serem validados nos servidores da Apple. Para isso, é necessário fazer uma solicitação verifyReceipt. Você deve enviar uma solicitação POST para https://buy.itunes.apple.com/verifyReceipt. No corpo do JSON do pedido, digite o segredo compartilhado no campo. password e o recibo no campo receipt-data. Também existe o parâmetro opcional exclude-old-transactions . Caso tenha o valor true value, para cada assinatura renovável automaticamente, você receberá apenas a última transação ao invés do histórico completo de renovação.
Veja a seguir qual é a carga útil da solicitação de uma validação de compra:
{
"password": "f4d35830e3...52aae",
"receipt-data": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"exclude-old-transactions": false
}
Caso você esteja trabalhando no ambiente Sandbox – ou seja, esteja testando as compras, envie solicitações de validação para https://sandbox.itunes.apple.com/verifyReceipt. O segredo compartilhado, assim como a carga útil e os formatos de resposta permanecem os mesmos.
É importante destacar que não será possível validar um recibo criado no ambiente Sandbox em um servidor de Produção, e vice-versa. É por isso que em sistemas do mundo real, a melhor prática é direcionar a primeira solicitação ao servidor de Produção e redirecioná-la para o servidor Sandbox caso a chave de status retorne o código de erro 21007. Este comportamento é obrigatório durante a revisão do aplicativo, pois permite que os funcionários da Apple testem as compras e que os usuários reais do seu aplicativo as façam.
Entre outros erros a serem observados, há o código de erro 21004 que significa que estamos usando o segredo errado. É importante rastrear, já que isso tem um impacto não só na experiência do usuário mas também na precisão do processo de analytics. Na pior das hipóteses, o aplicativo pode ser removido da App Store caso o usuário não tenha acesso aos recursos premium depois de pagar por eles.
Caso a validação tenha sido realizada com sucesso (status=0), a resposta deve conter os detalhes das transações do usuário.
Veja a seguir a resposta à solicitação de validação do pagamento:
{
"environment": "Production",
"receipt": {
"receipt_type": "Production",
"adam_id": 123,
"app_item_id": 123,
"bundle_id": "com.adapty.sample_app",
"application_version": "1",
"download_id": 123,
"version_external_identifier": 123,
"receipt_creation_date": "2021-04-28 19:42:01 Etc/GMT",
"receipt_creation_date_ms": "1619638921000",
"receipt_creation_date_pst": "2021-04-28 12:42:01 America/Los_Angeles",
"request_date": "2021-08-09 18:26:02 Etc/GMT",
"request_date_ms": "1628533562696",
"request_date_pst": "2021-08-09 11:26:02 America/Los_Angeles",
"original_purchase_date": "2017-04-09 21:18:41 Etc/GMT",
"original_purchase_date_ms": "1491772721000",
"original_purchase_date_pst": "2017-04-09 14:18:41 America/Los_Angeles",
"original_application_version": "1",
"in_app": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "1000000831360853",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1619638918000",
"purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-05-05 19:41:58 Etc/GMT",
"expires_date_ms": "1620243718000",
"expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000397200750",
"is_trial_period": "true",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED"
}
]
},
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001020690335",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-08-04 19:41:58 Etc/GMT",
"purchase_date_ms": "1628106118000",
"purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-11 19:41:58 Etc/GMT",
"expires_date_ms": "1628710918000",
"expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000438372383",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
},
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001017218955",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-07-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1627501318000",
"purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-04 19:41:58 Etc/GMT",
"expires_date_ms": "1628106118000",
"expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000849023623",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
],
"latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"pending_renewal_info": [
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
],
"status": 0
}
A resposta é bastante complicada e foi simplificada na nova versão API do servidor da App Store, mas a implementação atual não é tão difícil de se obter.
Como recuperar o status da assinatura e o histórico de transações
Para verificar se o usuário tem acesso aos recursos premium do aplicativo, você precisa definir uma maneira de determinar o status da assinatura. Não há nenhuma solicitação dedicada para recuperar o status da assinatura na versão atual da API, portanto, você deve trabalhar com o histórico de transações em qualquer caso.
Por padrão, a matriz latest_receipt_info contém todas as transações de compra de um usuário específico, exceto aquelas para produtos consumíveis que são concluídas no lado do aplicativo. Desta forma, é possível recuperar todo o histórico de compras do usuário. Este procedimento é bastante útil para o processo de analytics e para determinar o status atual da assinatura.
Ao que parece, sempre as transações mais recentes são classificadas em primeiro lugar. De qualquer modo, recomendo que você faça a implementação de uma classificação por data para as transações.
A carga útil da transação:
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "1000000831360853",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1619638918000",
"purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-05-05 19:41:58 Etc/GMT",
"expires_date_ms": "1620243718000",
"expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000397200750",
"is_trial_period": "true",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
Para verificar o atual status da assinatura, basta acessar a última transação da cadeia e analisar a propriedade expires_date. Uma exceção importante seria o período de carência, que discutiremos posteriormente.
Em termos analíticos, recomendo ter em mente as seguintes propriedades:
- product_id: o identificador de texto do produto comprado.
- transaction_id: o identificador numérico específico da transação. Cada compra ou renovação terá seu próprio identificador que pode ser usado para verificar se a transação foi processada anteriormente.
- original_transaction_id: o identificador numérico específico da cadeia de transação. Ele deve corresponder ao transaction_id no momento da ativação da assinatura ou do período de avaliação. No entanto, após novas renovações de assinatura, o transaction_id será alterado, enquanto o original_transaction_id permanecerá o mesmo. Trata-se de uma maneira prática de rastrear quantas vezes a assinatura foi renovada.
- purchase_date e original_purchase_date: a data da transação e a data original da transação. Seu funcionamento segue a mesma lógica da propriedade anterior.
- expires_date: a data de validade da assinatura.
- cancellation_date: a data do reembolso, não o cancelamento da assinatura, como o nome pode sugerir. Caso a resposta tenha este campo, significa que você pode descontinuar o acesso do usuário à sua assinatura, bem como contabilizar o cancelamento no seu processo de analytics — você não receberá nenhum pagamento por esta transação.
- is_in_intro_offer_period: a bandeira que mostra se a oferta inicial foi utilizada no momento da ativação da assinatura.
- is_trial_period: a bandeira que mostra se houve um período de avalição após a ativação da assinatura.
- offer_code_ref_name: o código de oferta que foi usado na ativação da assinatura.
- promotional_offer_id: o identificador do texto da oferta promocional que foi usado ao iniciar o período de cobrança atual.
- in_app_ownership_type: o tipo de propriedade da assinatura. Ela revela se o próprio usuário comprou o produto ou o recebeu como parte da assinatura familiar. Alguns valores possíveis incluem:
- PURCHASED: o próprio usuário comprou o produto.
- FAMILY_SHARED: o usuário recebeu o produto como parte da assinatura familiar.
A Apple anunciou na WWDC 2021 que está planejando adicionar um campo appAccountToken às informações sobre as transações. Ele deve conter o identificador do usuário no seu sistema. Este identificador deve estar no formato UUID e é definido no lado do aplicativo quando a compra é iniciada. Caso seja definido, ele retornará em todas as transações desta cadeia (renovações de assinatura, problemas de cobrança, etc.), o que significa que ficará mais fácil para você entender qual usuário fez a compra.
Você também deve rastrear o parâmetro subscription_group_identifier . Caso o usuário tenha realizado anteriormente alguma transação em períodos de avaliação ativos ou com ofertas iniciais, ele não deve ter acesso a estas transações no mesmo grupo de assinaturas. Deve-se rastrear este aspecto no lado do servidor.
Informações sobre renovação de assinatura, período de carência e problemas de cobrança
A matriz pending_renewal_info armazena os dados de renovação da assinatura. Permite entender o que acontecerá com a assinatura no próximo período de cobrança. Por exemplo, caso você verifique que o usuário optou pela não renovação automática, é possível sugerir que ele migre para um plano diferente ou também apresentar uma oferta promocional. São eventos que podem ser rastreados manualmente através de notificações do servidor, que discutirei em breve.
Data de renovação da assinatura:
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
Assinaturas consumíveis, não consumíveis e não renováveis
Caso o usuário não tenha produtos de renovação automática, as chaves latest_receipt_info pending_renewal_info não serão retornadas. Neste caso, as transações podem ser localizadas no receipt → in_app. O formato da transação é similar ao das transações de renovação automática, mas não tem campos para expiração, renovações, ofertas e outras propriedades exclusivas para transações de renovação automática.
Deve-se notar que o receipt → in_app também aparecerá para transações de renovação automática, mas a melhor prática é usar o latest_receipt_info, pois os dados de assinatura que ele contém serão os mais atualizados.
Notificações de transações no servidor
Algum tempo atrás, você teria que desenvolver um sistema complexo para acompanhar as mudanças no status da assinatura. Por exemplo, para entender se a assinatura foi ou não renovada, você teria que enviar um pedido de status de assinatura para os servidores da Apple por hora com início 24 horas antes do vencimento da assinatura. A Apple adicionava mais e mais notificações de servidor ao longo do tempo, e estas notificações agora abrangem praticamente todos os eventos importantes relacionados com as assinaturas. Trata-se de algo muito útil: quando houver alguma mudança do lado da Apple, você receberá uma notificação a respeito no seu próprio servidor.. Ou seja, você receberá notificações sobre novas compras, renovações de assinatura, problemas de cobrança, etc. Isto permite que você colete dados de analytics muito mais precisos, assim como facilita muito o gerenciamento do status da assinatura.
Você pode habilitar as notificações do servidor no App Store Connect. Abra a página do aplicativo e acesse Geral -> Informações do aplicativo. Em seguida, coloque o link no campo URL para notificações do servidor da App Store e salve as alterações.
Notificação do servidor:
{
"notification_type": "DID_RENEW",
"password": "f4d35830e3...52aae",
"environment": "PROD",
"auto_renew_product_id": "basic_subscription_1_month",
"auto_renew_status": "true",
"unified_receipt": {
"status": 0,
"environment": "Production",
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001020690335",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-08-04 19:41:58 Etc/GMT",
"purchase_date_ms": "1628106118000",
"purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-11 19:41:58 Etc/GMT",
"expires_date_ms": "1628710918000",
"expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000438372383",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
},
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001017218955",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-07-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1627501318000",
"purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-04 19:41:58 Etc/GMT",
"expires_date_ms": "1628106118000",
"expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000849023623",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
],
"latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"pending_renewal_info": [
{
"auto_renew_status": "1",
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853"
}
]
},
"bid": "com.adapty.sample_app",
"bvrs": "0"
}
O formato da notificação do servidor é similar ao da resposta de validação de pagamento. Os detalhes da transação são armazenados em unified_receipt → latest_receipt_info. A chave do password armazena o segredo compartilhado do seu aplicativo, permitindo que você verifique a autenticidade da solicitação. A chave notification_type armazena o tipo de evento. Na minha opinião, as mais úteis são:
- DID_CHANGE_RENEWAL_STATUS: significa que o usuário desativou ou ativou a renovação automática da assinatura (a última é muito mais rara). Caso o usuário tenha optado pela não renovação automática, talvez você queira incentivá-lo a se tornar novamente um assinante ativo.
- DID_FAIL_TO_RENEW: a assinatura não foi renovada por problemas de cobrança. Você deve notificar o usuário sobre o problema para que sua assinatura não seja cancelada automaticamente.
- DID_RENEW: a assinatura foi renovada com sucesso.
- REFUND: a assinatura foi reembolsada. Você deve restringir o acesso do usuário aos recursos premium que esta compra concedia e contabilizar o reembolso (ou seja, perder este dinheiro) em seu processo de analytics.
Conclusão
A validação do servidor pode sobrecarregar seu processo de analytics dos dados que você coleta no seu aplicativo. Também dificulta o acesso de fraudadores ao seu conteúdo premium, além de permitir que você faça suas assinaturas em diferentes plataformas. Ao mesmo tempo, a implementação da validação do servidor pode consumir muito tempo, especialmente se você precisar de dados extremamente precisos. Isto exigiria levar em conta muitos casos paralelos: upgrade de assinatura, crossgrade de assinatura, períodos de avaliação, ofertas promocionais/iniciais, períodos de carência, reembolsos, assinaturas familiares, etc. Você também deve ter conhecimento e considerar algumas diferenças, por exemplo, a Apple tem uma política de reduzir a comissão de 30% para 15% para assinaturas que são renovadas regularmente por mais de um ano.