Guia de Implementação de assinaturas renováveis ​​automaticamente em aplicativos iOS

imagem


Olá pessoal! Meu nome é Denis, estou desenvolvendo o Apphud , um serviço para a análise de assinaturas renováveis ​​automaticamente em aplicativos iOS.


Neste artigo, mostrarei como configurar, implementar e validar assinaturas renováveis ​​automaticamente no iOS 12 e iOS 13. Como bônus, mostrarei sobre pontos sutis e armadilhas que nem todos os desenvolvedores consideram.


Configurar assinaturas no App Store Connect


Se você já possui um ID de pacote configurável e um aplicativo criado, pode pular essas etapas. Se você estiver criando um aplicativo pela primeira vez, faça o seguinte:


Você deve criar um ID de pacote explícito (ID do aplicativo) no Apple Developer Portal . Com uma página chamada Certificados, Identificadores e Perfis abertos, vá para a guia Identificadores . Em junho de 2019, a Apple finalmente atualizou o layout do portal de acordo com o ASC (abreviação de App Store Connect).


Novo design para o Apple Developer Portal em 2019
Novo design para o Apple Developer Portal em 2019



O ID do pacote explícito geralmente é especificado no estilo do domínio ( com.apphud.subscriptionstest ). Na seção Recursos , você notará que a marca de seleção ao lado de Compras no aplicativoestá marcada. Após criar o ID do pacote ( ID do aplicativo ), acesse o App Store Connect.


Usuários de teste (usuários de sandbox)


Para testar compras futuras, você precisará criar um usuário de teste. Para fazer isso, acesse ASC na guia Usuários e acesso e , em seguida, em Sandbox Testers.


Formulário de criação de sandbox do usuário
Formulário de criação de sandbox do usuário


Ao criar um testador, você pode especificar quaisquer dados inexistentes, o mais importante, não se esqueça do e-mail e da senha!

Falarei sobre como testar compras com credenciais de teste no final do artigo.


Outro passo importante é configurar contratos e dados bancários na seção " Contratos, impostos e operações bancárias ". Se você não tiver um contrato para aplicativos pagos, não poderá testar assinaturas renováveis ​​automaticamente!


Depois disso, você pode criar um novo aplicativo na App Store Connect. Especifique um nome exclusivo e selecione seu ID do pacote como o ID do pacote.


O ID do pacote é o seu ID do pacote
O ID do pacote é o seu ID do pacote


Imediatamente após a criação do aplicativo, vá para a guia Recursos.


Se você já criou o aplicativo, poderá continuar lendo aqui.

O processo de criação de uma assinatura renovável automaticamente consiste em várias etapas:


1. Crie um identificador de assinatura e crie um grupo de assinaturas . Um grupo de assinaturas é uma coleção de assinaturas com períodos e preços diferentes, mas que abrem a mesma funcionalidade no aplicativo. Além disso, no grupo de assinaturas, você pode ativar o período de avaliação gratuita apenas uma vez e apenas uma das assinaturas pode estar ativa. Se você deseja que seu aplicativo tenha duas assinaturas diferentes ao mesmo tempo, será necessário criar dois grupos de assinaturas.


2. Preenchendo os dados da assinatura: duração, nome para exibição na App Store (não confundir apenas com o nome) e descrição. Se você adicionar a primeira assinatura ao grupo, precisará indicar o nome de exibição do grupo de assinaturas. Lembre-se de salvar suas alterações com mais frequência, o ASC pode congelar a qualquer momento e parar de responder.


Página de inscrição
Tela de inscrição


3. Preenchendo o preço da assinatura. Existem duas etapas: criar preços e ofertas especiais. Indique o preço real em qualquer moeda, ele é recalculado automaticamente para todos os outros países. Ofertas introdutórias: aqui você pode oferecer aos usuários um período de avaliação gratuita ou descontos pré-pagos. As promoções apareceram na App Store recentemente em 2019: permitem que você ofereça descontos especiais para usuários que cancelaram a inscrição e que você deseja devolver.


Geração de chave secreta compartilhada


Na página com uma lista de todas as suas assinaturas criadas, você verá a chave Compartilhada do botão do aplicativo . Essa é uma linha especial necessária para validar uma verificação em um aplicativo iOS. Precisamos validar a verificação para determinar o status da assinatura.


A chave compartilhada pode ser de dois tipos: uma chave exclusiva para seu aplicativo ou uma chave única para sua conta. Importante: em nenhum caso, recrie a chave se você já tiver o aplicativo na App Store, caso contrário, os usuários não poderão validar a verificação e seu aplicativo deixará de funcionar conforme o esperado.


Neste exemplo, três grupos de assinaturas e três assinaturas criadas são criadas.
Neste exemplo, três grupos de assinaturas e três assinaturas anuais são criados.


Copie o ID de todas as suas assinaturas e a chave compartilhada, isso será útil mais adiante no código.


Parte do software


Vamos direto à parte prática. O que é necessário para criar um gerente de compras completo? No mínimo, o seguinte deve ser implementado:


  1. Checkout


  2. Verificar status da assinatura


  3. Verificar atualização


  4. Recuperação de transação (não confunda com a atualização de um cheque!)



Checkout


Todo o processo de compra pode ser dividido em 2 etapas: recebimento de produtos (classe SKProduct ) e inicialização do processo de compra (classe SKPayment ). Primeiro de tudo, devemos especificar o delegado do protocolo SKPaymentTransactionObserver .


 // Starts products loading and sets transaction observer delegate @objc func startWith(arrayOfIds : Set<String>!, sharedSecret : String){ SKPaymentQueue.default().add(self) self.sharedSecret = sharedSecret self.productIds = arrayOfIds loadProducts() } private func loadProducts(){ let request = SKProductsRequest.init(productIdentifiers: productIds) request.delegate = self request.start() } public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { products = response.products DispatchQueue.main.async { NotificationCenter.default.post(name: IAP_PRODUCTS_DID_LOAD_NOTIFICATION, object: nil) } } func request(_ request: SKRequest, didFailWithError error: Error){ print("error: \(error.localizedDescription)") } 

A notificação IAP_PRODUCTS_DID_LOAD_NOTIFICATION usada para atualizar a interface do usuário em um aplicativo.


Em seguida, escrevemos um método para inicializar a compra:


 func purchaseProduct(product : SKProduct, success: @escaping SuccessBlock, failure: @escaping FailureBlock){ guard SKPaymentQueue.canMakePayments() else { return } guard SKPaymentQueue.default().transactions.last?.transactionState != .purchasing else { return } self.successBlock = success self.failureBlock = failure let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) } 

O delegado SKPaymentTransactionObserver tem a SKPaymentTransactionObserver aparência:


 extension IAPManager: SKPaymentTransactionObserver { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch (transaction.transactionState) { case .purchased: SKPaymentQueue.default().finishTransaction(transaction) notifyIsPurchased(transaction: transaction) break case .failed: SKPaymentQueue.default().finishTransaction(transaction) print("purchase error : \(transaction.error?.localizedDescription ?? "")") self.failureBlock?(transaction.error) cleanUp() break case .restored: SKPaymentQueue.default().finishTransaction(transaction) notifyIsPurchased(transaction: transaction) break case .deferred, .purchasing: break default: break } } } private func notifyIsPurchased(transaction: SKPaymentTransaction) { refreshSubscriptionsStatus(callback: { self.successBlock?() self.cleanUp() }) { (error) in // couldn't verify receipt self.failureBlock?(error) self.cleanUp() } } func cleanUp(){ self.successBlock = nil self.failureBlock = nil } } 

Após a assinatura bem-sucedida, o método delegado é chamado no qual a transação tem o status purchased .


Mas como determinar a data de validade de uma assinatura? Para fazer isso, faça uma solicitação separada para a Apple.


Verificar status da assinatura


A verificação é validada usando a solicitação verifyReceipt POST para a Apple, enviamos a verificação criptografada como uma string codificada em base64 como parâmetro e, na resposta, recebemos a mesma verificação no formato JSON. Na matriz, a chave latest_receipt_info listará todas as transações de cada período de cada assinatura, incluindo períodos de avaliação. Só podemos analisar a resposta e obter a data de vencimento atual de cada produto.


Na WWDC 2017, eles adicionaram a capacidade de receber apenas verificações atuais para cada assinatura usando a chave exclude-old-transactions na solicitação verifyReceipt .

 func refreshSubscriptionsStatus(callback : @escaping SuccessBlock, failure : @escaping FailureBlock){ // save blocks for further use self.refreshSubscriptionSuccessBlock = callback self.refreshSubscriptionFailureBlock = failure guard let receiptUrl = Bundle.main.appStoreReceiptURL else { refreshReceipt() // do not call block yet return } #if DEBUG let urlString = "https://sandbox.itunes.apple.com/verifyReceipt" #else let urlString = "https://buy.itunes.apple.com/verifyReceipt" #endif let receiptData = try? Data(contentsOf: receiptUrl).base64EncodedString() let requestData = ["receipt-data" : receiptData ?? "", "password" : self.sharedSecret, "exclude-old-transactions" : true] as [String : Any] var request = URLRequest(url: URL(string: urlString)!) request.httpMethod = "POST" request.setValue("Application/json", forHTTPHeaderField: "Content-Type") let httpBody = try? JSONSerialization.data(withJSONObject: requestData, options: []) request.httpBody = httpBody URLSession.shared.dataTask(with: request) { (data, response, error) in DispatchQueue.main.async { if data != nil { if let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments){ self.parseReceipt(json as! Dictionary<String, Any>) return } } else { print("error validating receipt: \(error?.localizedDescription ?? "")") } self.refreshSubscriptionFailureBlock?(error) self.cleanUpRefeshReceiptBlocks() } }.resume() } 

No início do método, você pode ver que há uma verificação da existência de uma cópia local da verificação. Uma verificação local pode não existir, por exemplo, se o aplicativo foi instalado via iTunes. Se não houver verificação, não podemos executar a solicitação verifyReceipt . Primeiro, precisamos obter a verificação local atual e tentar validá-la novamente. A atualização da verificação é feita usando a classe SKReceiptRefreshRequest :


 private func refreshReceipt(){ let request = SKReceiptRefreshRequest(receiptProperties: nil) request.delegate = self request.start() } func requestDidFinish(_ request: SKRequest) { // call refresh subscriptions method again with same blocks if request is SKReceiptRefreshRequest { refreshSubscriptionsStatus(callback: self.successBlock ?? {}, failure: self.failureBlock ?? {_ in}) } } func request(_ request: SKRequest, didFailWithError error: Error){ if request is SKReceiptRefreshRequest { self.refreshSubscriptionFailureBlock?(error) self.cleanUpRefeshReceiptBlocks() } print("error: \(error.localizedDescription)") } 

A atualização de verificação é implementada na função refreshReceipt() . Se a verificação foi atualizada com êxito, o método delegado requestDidFinish(_ request : SKRequest) é chamado, que chama o método refreshSubscriptionsStatus .


Como a análise das informações de compra é implementada? latest_receipt_info um objeto JSON no qual há uma matriz aninhada de transações (pela chave latest_receipt_info ). Analisamos a matriz, obtemos a data de validade usando a chave expires_date e a salvamos se essa data ainda não chegou.


 private func parseReceipt(_ json : Dictionary<String, Any>) { // It's the most simple way to get latest expiration date. Consider this code as for learning purposes. Do not use current code in production apps. guard let receipts_array = json["latest_receipt_info"] as? [Dictionary<String, Any>] else { self.refreshSubscriptionFailureBlock?(nil) self.cleanUpRefeshReceiptBlocks() return } for receipt in receipts_array { let productID = receipt["product_id"] as! String let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV" if let date = formatter.date(from: receipt["expires_date"] as! String) { if date > Date() { // do not save expired date to user defaults to avoid overwriting with expired date UserDefaults.standard.set(date, forKey: productID) } } } self.refreshSubscriptionSuccessBlock?() self.cleanUpRefeshReceiptBlocks() } 

Dei um exemplo simples de como extrair a data de vencimento atual de uma assinatura. Não há tratamento de erros e, por exemplo, não há verificação para o retorno de uma compra (a data de cancelamento é adicionada).


Para determinar se uma assinatura está ativa ou não, basta comparar a data atual com a data em Padrões do usuário por chave do produto. Se estiver ausente ou for menor que a data atual, a assinatura será considerada inativa.


 func expirationDateFor(_ identifier : String) -> Date?{ return UserDefaults.standard.object(forKey: identifier) as? Date } let subscriptionDate = IAPManager.shared.expirationDateFor("YOUR_PRODUCT_ID") ?? Date() let isActive = subscriptionDate > Date() 

A recuperação da transação é realizada em uma única linha SKPaymentQueue.default().restoreCompletedTransactions() . Essa função restaura todas as transações concluídas chamando o método delegado func paymentQueue(**_** queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) .


Qual é a diferença entre atualizar uma verificação da recuperação de transações?


Ambos os métodos ajudam a restaurar seus dados de compra. Mas quais são as diferenças? Há uma mesa maravilhosa com vídeo wwdc :


Tabela de diferenças de duas maneiras de restaurar compras da WWDC
Tabela de diferenças de duas maneiras de restaurar compras da WWDC


Na maioria dos casos, você só precisa usar SKReceiptRefreshRequest() , porque estamos interessados ​​apenas em receber uma verificação para o cálculo subsequente da data de validade.


No caso de assinaturas renováveis ​​automaticamente, as transações em si não são do nosso interesse, portanto, basta usar apenas a atualização de cheques. No entanto, há casos em que você precisa usar o método de recuperação de transação: se o seu aplicativo baixa conteúdo na compra (conteúdo hospedado pela Apple) ou se você ainda suporta versões abaixo do iOS 7.


Teste de compras (Sandbox Testing)


Antes, para testar as compras, era necessário fazer login na App Store nas configurações do seu iPhone. Isso foi muito inconveniente (por exemplo, toda a Apple Music Library foi apagada). No entanto, isso não precisa ser feito agora: a conta da sandbox agora existe separadamente da conta principal.



O processo de compra é semelhante em comparação com compras reais na App Store, mas existem alguns pontos:


  • Você sempre precisará inserir a senha de login através da janela do sistema. Compras usando o Touch ID / Face ID ainda não são suportadas.


  • Se, ao inserir o login e a senha corretamente, o sistema solicitar repetidamente a senha de login, clique em "Cancelar" , minimize o aplicativo e tente novamente. Parece bobagem, mas funciona para muitos. Às vezes, porém, após a segunda entrada da senha, o processo continua.


  • Você não poderá testar o processo de cancelamento de inscrição de nenhuma maneira.


  • A duração dos períodos de assinatura é muito menor que a real. E eles são atualizados não mais que 6 vezes por dia.



Duração realDuração do teste
1 semana3 minutos
1 mês5 minutos
2 meses10 minutos
3 meses15 minutos
6 meses30 minutos
1 ano1 hora

O que há de novo no StoreKit no iOS 13?


Dos novos - apenas a classe SKStorefront , que fornece informações sobre em que país o usuário está registrado na App Store. Isso pode ser útil para os desenvolvedores que usam assinaturas diferentes para diferentes países. Anteriormente, todos eram verificados por localização geográfica ou por região do dispositivo, mas isso não dava um resultado preciso. Agora é muito fácil descobrir o país na App Store: SKPaymentQueue.default().storefront?.countryCode . Um delegado de método também foi adicionado se o país na App Store mudasse durante o processo de compra. Nesse caso, você pode continuar ou cancelar o processo de compra.


Armadilhas ao trabalhar com assinaturas


  • A verificação de um cheque diretamente de um dispositivo não é recomendada pela Apple. Eles falaram sobre isso várias vezes na WWDC (das 5:50) e isso é indicado na documentação . Isso é inseguro porque um invasor pode interceptar dados usando um ataque man-in-the-middle. A maneira correta de verificar as verificações é a validação local usando o servidor.
  • Há um problema ao verificar a data de validade. Se você não estiver usando o servidor, a hora do sistema no dispositivo poderá ser alterada para antiga e, em seguida, nosso código fornecerá o resultado errado - a assinatura será considerada ativa. Se isso não lhe agradar, você poderá usar qualquer serviço que emita um horário mundial preciso.
  • Nem todos os usuários podem ter uma avaliação gratuita. O usuário pode reinstalar o aplicativo após algum tempo e o aplicativo mostrará que a avaliação está disponível como de costume. Será correto atualizar a verificação, validá-la e verificar no JSON a disponibilidade da avaliação para este usuário. Muitos não.
  • Se o usuário solicitou um reembolso, cancellation_date será adicionado à assinatura JSON, mas expires_date permanecerá inalterado. Portanto, é importante sempre verificar a presença do campo cancellation_date , que é preferível a expires_date .
  • Não vale a pena atualizar a verificação toda vez que o aplicativo for iniciado, porque, em primeiro lugar, isso não faz sentido e, em segundo lugar, provavelmente o usuário receberá uma janela de entrada de senha do Apple ID. Vale a pena atualizar a verificação, por exemplo, quando o próprio usuário clicou no botão restaurar compras.
  • Como determinar em que pontos vale a pena validar um cheque para obter a data de vencimento atual de uma assinatura? Você pode validar a verificação em cada início ou apenas no final da assinatura. No entanto, se você marcar a verificação apenas no final da assinatura, o usuário que emitiu o reembolso poderá usar seu aplicativo gratuitamente até o final do período.

Conclusão


Espero que este artigo seja útil para você. Tentei adicionar não apenas o código, mas também explicar os pontos sutis no desenvolvimento. O código completo da turma pode ser baixado aqui . Esta classe será muito útil para familiarizar com desenvolvedores iniciantes e aqueles que desejam aprender mais sobre como tudo funciona. Para aplicativos ao vivo, é recomendável usar soluções mais sérias, por exemplo, SwiftyStoreKit .


Deseja implementar assinaturas no seu aplicativo iOS em 10 minutos? Integre o Apphud e:
  • Faça compras usando apenas um método;
  • rastreia automaticamente o status da assinatura de cada usuário;
  • Integre facilmente as ofertas de assinatura
  • enviar eventos de assinatura para Amplitude, Mixpanel, Slack e Telegram, levando em consideração a moeda local do usuário;
  • diminua a taxa de rotatividade de aplicativos e retorne usuários não inscritos.

Source: https://habr.com/ru/post/pt456602/


All Articles