ios storekit_ios 14中的订阅收据和storekit

ios storekit

Of all the ways to get paid for the work you put into app development, the best by far is subscriptions. In this article, I hope to look at the best practices outlined in the WWDC2019 talk and link that to the talk on the same subject in a WWDC2020 presentation on architecting for subscriptions. I’ll be completing my discussion with a code example, of course.

在您投入应用程序开发工作的所有报酬方法中,迄今为止最好的是订阅。 在本文中,我希望了解WWDC2019演讲中概述的最佳实践,并将其与WWDC2020订阅设计演讲中的同一主题的演讲联系起来。 当然,我将通过一个代码示例来完成我的讨论。

But first, a little background. The whole IT app industry has been moving towards subscriptions for more than a decade now. Amazon was one of the first players with AWS in 2006. Microsoft and Apple launched products in 2011: Office 365 and iCloud. And even Adobe joined the party in 2013 with their Creative Suite going online.

但是首先,要有一点背景。 十多年来,整个IT应用行业一直在朝着订阅发展。 亚马逊是2006年首批使用AWS的公司之一。微软和苹果公司在2011年推出了产品:Office 365和iCloud。 甚至Adobe于2013年以其Creative Suite上线的方式加入了聚会。

It’s the gold standard cause it’s a regular income — it helps get over the bumps in the road. Of course, some industries have been doing this for decades, telecoms and publishing to name but two. And as such, an entire science has grown up around it. Let’s take a closer look.

这是黄金标准,因为它是固定收入–有助于渡过难关。 当然,几十年来,一些行业一直在这样做,电信和出版业仅举两个例子。 因此,围绕它发展出了一整套科学。 让我们仔细看看。

吸引订户 (Attracting Subscribers)

Subscriptions and other in-app purchases can be sold using one or more of these three classic routes.

订阅和其他应用内购买可以使用这三种经典途径中的一种或多种来出售。

  • With the onboarding experience when they first launch your app

    有了他们首次启动您的应用程序时的入门经验

  • Using a freemium model, offering reduced functionality but an option to update to an in-app subscription

    使用免费增值模式,功能减少,但可以选择更新到应用内订阅

  • By taking a metered-paywall approach, which will let them use the full functionality for a set period only

    通过采用计量付费方式,这将使他们仅在设定的时间段内使用全部功能

But wait — better than that — even before downloading your app, assuming we’re talking subscriptions, you can actually start to promote them on the App Store itself.

但是,要等一下-比那更好-甚至在下载您的应用程序之前,假设我们正在谈论订阅,您实际上可以开始在App Store本身上进行促销。

订阅报价类型 (Subscription-offer types)

There are three types of subscriptions you can sell: introductory offers, promotional offers, and, under iOS 14, offer codes.

您可以出售三种类型的订阅:介绍性优惠,促销性优惠,以及在iOS 14下的优惠代码。

Indeed, you can provide all three at the same time if it makes sense to do so. This table taken from the developer web page compares and contrasts the different types.

确实,您可以同时提供所有这三个选项。 该表来自开发人员网页,比较并对比了不同类型。

Image for post

保留订户 (Keeping subscribers)

Having won over the hearts of your public, your next challenge is to make sure they stay subscribed — a challenge that actually has its own term in subscriber-speak: churn. You don’t want your subscribers to churn at all. Ominous as it may sound when I say it, you want them to subscribe — forever.

赢得了公众的关注之后,您的下一个挑战是确保他们保持订阅状态-这项挑战实际上在订户说话方面有其自己的术语:流失。 您根本不希望订户流失。 正如我所说的听起来不祥,您希望他们永远订阅。

Now in the WWDC2019 presentation on the subject, they talk about no less than six ways to keep customers hooked, and they presented this iconography to illustrate their point.

现在,在有关主题的WWDC2019演讲中,他们讨论了不少于六种吸引客户的方法,并且他们展示了这种图像来说明他们的观点。

Image for post

What do these all mean? Let’s try and quickly go through them.

这些都是什么意思? 让我们尝试快速浏览它们。

Win backs

赢回

This is where a customer cancels before a subscription even expires. If this is the case, then you can/should try and find out why they cancelled. What was the reason, and, indeed, try to win them back with an offer.

这是客户在订阅到期之前就取消的地方。 如果是这种情况,那么您可以/应该尝试找出取消原因。 原因是什么,并且确实设法通过出价来赢得他们。

Retention

保留

This is where a customer changes the autorenewal of a subscription from true to false. In this case, you need to try to find out what’s wrong and, indeed, again try and get them to change their minds with an autorenewal offer.

客户在这里将订阅的自动续订从true更改为false。 在这种情况下,您需要尝试找出问题所在,并且,实际上,再次尝试通过自动续约来让他们改变主意。

Save offers

保存优惠

This is where you have good reason to think a customer may churn. It’s a fine balancing act, but you want to keep that customer and make them an offer to try preempt their departure.

在这里,您有充分的理由认为客户可能会流失。 这是一种很好的平衡行为,但您想保留该客户并向他们提供要约以尝试阻止他们离开。

Upgrade/downgrade

升级/降级

You want more money, so get them to upgrade. Make them an upgrade offer.

您想要更多的钱,所以让他们升级。 使它们成为升级产品。

Customer service

客户服务

This is where a customer complains about a loss of service. A loss that may tempt them to unsubscribe. To forestall the event, give them an olive-branch offer to make up for their loss.

这是客户抱怨服务中断的地方。 可能诱使他们退订的损失。 为了阻止事件发生,请给他们橄榄枝补偿他们的损失。

Loyalty

忠诚

This is where a customer renews the subscription again and again — a behaviour you want to reward. Give them a fidelity offer to encourage them to keep coming back.

客户在这里一次又一次地更新订阅-您想要奖励的行为。 给他们一个忠诚的提议,以鼓励他们继续回来。

This all looks and sounds great in theory, but how do we connect the dots on this? In the WWDC2020 presentation, they link these offers to the point in which your subscribe is in their journey — a journey that you can take a closer at look through the receipt of their current subscription status.

从理论上讲,所有这些看起来和听起来都很棒,但是我们如何将这些点连接起来呢? 在WWDC2020演示中,他们将这些优惠链接到您的订阅过程中的点-您可以通过接收其当前订阅状态来仔细了解这一过程。

By downloading the most recent receipt, you can work out how to tailor the offers you need to make to retain that subscriber. Here’s a list taken from the Apple documentation of what all the errors mean in said receipt.

通过下载最新的收据,您可以确定如何调整保留该订户所需的报价。 这是一份来自Apple文档的清单,其中列出了收据中所有错误的含义。

Image for post

Let’s look at a real receipt I just downloaded from my demo app — a receipt I posted in two parts because there’s a lot there.

让我们看一下我刚刚从演示应用程序下载的真实收据–我将收据分为两部分,因为那里有很多收据。

Image for post
Subscription details
订阅详情
Image for post
Subscription details continued
订阅详情继续

This is a JSON array, with each item in the array an object you can individually address. Let’s see how we can match this against the rules I talked about — connecting the dots.

这是一个JSON数组,数组中的每个项目都是一个您可以单独寻址的对象。 让我们看看如何将其与我所说的规则相匹配-连接点。

Regard the pending_renewal_info. This is currently set to 1 or true. Now if this changed to false at some point when they’re using your app, then it’d be a good time to make a retention offer.

关于pending_renewal_info 。 当前设置为1true 。 现在,如果在他们使用您的应用程序的某个时刻将其更改为false ,那么现在是提供保留报价的好时机。

Take some time to look through the receipt, and see if you can figure out some more potential subscription-offer opportunities hidden in there.

花一些时间浏览收据,看看是否可以找出其中隐藏的更多潜在订阅机会。

Note: Since we’re using the sandbox here, the purchase and expiry dates are the same — a monthly subscription, in our case, lasts just five minutes (good for testing).

注意:由于我们在这里使用沙盒,因此购买和到期日期是相同的-在我们的情况下,每月订阅仅持续5分钟(适合测试)。

Which brings me to end of this article— well, except the code, of course. Firstly, the complete code for the IAPManager classes. The first part is an observable object we’re going to use in our SwiftUI class, while the second part is the IAPManager template methods.

这使我结束了本文的篇幅,当然,除了代码。 首先, IAPManager类的完整代码。 第一部分是我们将在SwiftUI类中使用的可观察对象,而第二部分是IAPManager模板方法。

import Foundation
import StoreKit


private let allTicketIdentifiers: Set<String> = [
  "ticket.consumable",
  "ticket.non_consumable",
  "ticket.subscription",
  "ticket.limited"
]




final class ProductsDB: ObservableObject, Identifiable {
  
  static let shared = ProductsDB()
  var items: [SKProduct] = [] {
      willSet {
        DispatchQueue.main.async {
          self.objectWillChange.send()
        }
      }
  }
}


class IAPManager: NSObject {
  static let shared = IAPManager()
  
  private override init() {
    super.init()
  }
  
  func getProducts() {
    let request = SKProductsRequest(productIdentifiers: allTicketIdentifiers)
    request.delegate = self
    request.start()
  }
  
  func purchase(product: SKProduct) -> Bool {
    if !IAPManager.shared.canMakePayments() {
        return false
    } else {
      let payment = SKPayment(product: product)
      SKPaymentQueue.default().add(payment)
    }
    return true
  }


  func canMakePayments() -> Bool {
    return SKPaymentQueue.canMakePayments()
  }
  
  func verifyReceipt() {
    let verifyURL = "https://sandbox.itunes.apple.com/verifyReceipt"
    
    guard let receiptURL = Bundle.main.appStoreReceiptURL, let receiptString = try? Data(contentsOf: receiptURL).base64EncodedString() , let url = URL(string: verifyURL) else {
          return
        }
                
    print("receiptURL ",receiptString)
    
    let requestData : [String : Any] = ["receipt-data" : receiptString,
                                            "password" : "214c835c2aca4b6d966704296bf25591",
                                            "exclude-old-transactions" : false]
    let httpBody = try? JSONSerialization.data(withJSONObject: requestData, options: [])
        
    var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = httpBody
        URLSession.shared.dataTask(with: request)  { (data, response, error) in
          // convert data to Dictionary and view purchases
          DispatchQueue.main.async {
            if let data = data, let jsonData = try? JSONSerialization.jsonObject(with: data, options: .allowFragments){
              print("jsonX ",jsonData)
              // your non-consumable and non-renewing subscription receipts are in `in_app` array
              // your auto-renewable subscription receipts are in `latest_receipt_info` array
            }
          }
        }.resume()
  }
}


extension IAPManager: SKProductsRequestDelegate, SKRequestDelegate {


  func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    let badProducts = response.invalidProductIdentifiers
    let goodProducts = response.products
    
    if !goodProducts.isEmpty {
      ProductsDB.shared.items = response.products
      print("bon ", ProductsDB.shared.items)
    }
    
    print("badProducts ", badProducts)
  }
  
  func request(_ request: SKRequest, didFailWithError error: Error) {
    print("didFailWithError ", error)
    DispatchQueue.main.async {
      print("purchase failed")
    }
  }
  
  func requestDidFinish(_ request: SKRequest) {
    DispatchQueue.main.async {
      print("request did finish ")
    }
  }
  
  func completeTransaction(_ transaction: SKPaymentTransaction) {
    print("transaction ",transaction)


  }
  
  func startObserving() {
    SKPaymentQueue.default().add(self)
  }
 
  func stopObserving() {
    SKPaymentQueue.default().remove(self)
  }
  
}




extension IAPManager: SKPaymentTransactionObserver {
  func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
     transactions.forEach { (transaction) in
      switch transaction.transactionState {
      case .purchased:
        SKPaymentQueue.default().finishTransaction(transaction)
        print("trans ",transaction)
        verifyReceipt()
//          verifyPurchaseWithPayment()
//        purchasePublisher.send(("Purchased ", true))
      case .restored:
//        totalRestoredPurchases += 1
        SKPaymentQueue.default().finishTransaction(transaction)
//        purchasePublisher.send(("Restored ", true))
      case .failed:
        if let error = transaction.error as? SKError {
//          purchasePublisher.send(("Payment Error \(error.code) ", false))
          print("Payment Failed \(error.code)")
        }
        SKPaymentQueue.default().finishTransaction(transaction)
      case .deferred:
        print("Ask Mom ...")
//        purchasePublisher.send(("Payment Diferred ", false))
      case .purchasing:
        print("working on it...")
//        purchasePublisher.send(("Payment in Process ", false))
      default:
        break
      }
    }
  }
}


extension String {
//: ### Base64 encoding a string
    func base64Encoded() -> String? {
        if let data = self.data(using: .utf8) {
            return data.base64EncodedString()
        }
        return nil
    }


//: ### Base64 decoding a string
    func base64Decoded() -> String? {
        if let data = Data(base64Encoded: self, options: .ignoreUnknownCharacters) {
            return String(data: data, encoding: .utf8)
        }
        return nil
    }
}

And this the SwiftUI class I combined with the IAPManager.swift file above to create my simple IAP subscription demo app.

我将这个SwiftUI类与上面的IAPManager.swift文件结合在一起,创建了我的简单IAP订阅演示应用程序。

import SwiftUI


struct ContentView: View {
    @State var purchased = false
    
    init() {
    }
    var body: some View {
          BuyView(purchased: $purchased)
    }
}


struct BuyView: View {
  @Binding var purchased: Bool
  @ObservedObject var products = ProductsDB.shared
  var body: some View {
      Text("Tickets")
        .onTapGesture {
          IAPManager.shared.getProducts()
          IAPManager.shared.startObserving()
        }
      List {
        ForEach((0 ..< self.products.items.count), id: \.self) { column in
          Text(self.products.items[column].localizedDescription)
            .onTapGesture {
              let _ = IAPManager.shared.purchase(product: self.products.items[column])
            }
        }
      }
  }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Before I go, don’t forget that in order to make this work, you’re going to have to create your purchasable products using iTunes Connect; otherwise, it won’t find anything, and you won’t be able to see it in action.

在我开始之前,请不要忘记为了使这项工作有效,您将不得不使用iTunes Connect创建可购买的产品。 否则,它将找不到任何内容,并且您将无法看到实际的效果。

I was hoping to get this working with local validation, given that you can now test in-app purchases locally too; unfortunately, I didn’t do so this time around, but watch this space.

鉴于您现在也可以在本地测试应用内购买内容,因此我希望通过本地验证来实现这一目标。 不幸的是,这次我没有这样做,但是请注意这个空间。

When you you run this, initially nothing will happen. Tap on the Tickets tab to prompt it to look up the available in-app purchases, and then tap on one of the items you want to purchase that should appear in a list.

运行此命令时,最初不会发生任何事情。 点击“票证”选项卡以提示它查找可用的应用程序内购买,然后点击要显示在列表中的要购买的物品之一。

Keep calm, and keep coding.

保持冷静,并保持编码。

翻译自: https://medium.com/better-programming/subscriptions-receipts-and-storekit-in-ios-14-16194eb93963

ios storekit

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值