【问题标题】:How do you add an in-app purchase to an iOS application?如何将应用内购买添加到 iOS 应用程序?
【发布时间】:2013-11-02 14:15:21
【问题描述】:

如何将应用内购买添加到 iOS 应用?详细信息是什么,是否有示例代码?

这是关于如何将应用内购买添加到 iOS 应用程序的各种信息

【问题讨论】:

标签: ios objective-c swift cocoa-touch in-app-purchase


【解决方案1】:

Swift 用户

Swift 用户可以查看 My Swift Answer for this question。
或者,查看 Yedidya Reiss's Answer,它将此 Objective-C 代码转换为 Swift。

Objective-C 用户

这个答案的其余部分是用 Objective-C 编写的

App Store 连接

  1. 转到appstoreconnect.apple.com 并登录
  2. 点击My Apps,然后点击您要购买的应用程序
  3. 点击Features标题,然后选择左侧的In-App Purchases
  4. 点击中间的+图标
  5. 在本教程中,我们将添加应用内购买来移除广告,因此请选择non-consumable。如果您要向用户发送实物物品,或者给他们一些他们可以多次购买的东西,您会选择consumable。
  6. 对于参考名称,请随意填写(但请确保您知道它是什么)
  7. 对于产品 id 输入tld.websitename.appname.referencename 效果最好,例如,您可以使用com.jojodmo.blix.removeads
  8. 选择cleared for sale,然后选择价格等级为 1 (99¢)。第 2 层为 1.99 美元,第 3 层为 2.99 美元。如果您点击view pricing matrix,则可以查看完整列表我建议您使用第 1 层,因为这通常是任何人为移除广告所支付的最高费用。
  9. 点击蓝色的add language按钮,输入信息。这将全部显示给客户,所以不要放任何你不希望他们看到的东西
  10. 对于hosting content with Apple,请选择否
  11. 您可以将评论备注留空现在。
  12. 跳过screenshot for review现在,我们跳过的所有内容都会回来。
  13. 点击“保存”

您的产品 ID 可能需要几个小时才能在 App Store Connect 中注册,因此请耐心等待。

设置您的项目

现在您已经在 App Store Connect 上设置了应用内购买信息,进入您的 Xcode 项目,然后转到应用程序管理器(方法和头文件所在位置顶部的蓝色页面状图标) 在目标下单击您的应用程序(应该是第一个),然后转到常规。在底部,您应该会看到 linked frameworks and libraries 单击小加号并添加框架 StoreKit.framework 如果您不这样做,应用内购买将不起作用!

如果您使用 Objective-C 作为应用程序的语言,您应该跳过这五个步骤。否则,如果您使用 Swift,您可以关注 My Swift Answer for this question, here,或者,如果您更喜欢使用 Objective-C 作为应用内购买代码,但在您的应用中使用 Swift,您可以执行以下操作:

  1. 通过转到File > New > File...(Command ⌘ + N)创建一个新的.h(标题)文件。在本教程的其余部分中,此文件将被称为“您的 .h 文件”

  2. 出现提示时,单击创建桥接头。这将是我们的桥接头文件。如果您没有提示,请转到第 3 步。如果您有提示,请跳过第 3 步并直接转到第 4 步。

  3. 在主项目文件夹中创建另一个名为Bridge.h 的.h 文件,然后转到应用程序管理器(蓝色页面状图标),然后在Targets 部分中选择您的应用程序,然后单击Build Settings .找到显示 Swift Compiler - Code Generation 的选项,然后将 Objective-C Bridging Header 选项设置为 Bridge.h

  4. 在您的桥接头文件中,添加行#import "MyObjectiveCHeaderFile.h",其中MyObjectiveCHeaderFile 是您在第一步中创建的头文件的名称。因此,例如,如果您将头文件命名为 InAppPurchase.h,则应将行 #import "InAppPurchase.h" 添加到您的网桥头文件中。

  5. 转到File > New > File... (Command ⌘ + N) 创建一个新的 Objective-C 方法 (.m) 文件.将其命名为与您在第 1 步中创建的头文件相同。例如,如果您在第 1 步中调用该文件 InAppPurchase.h,您将将此新文件命名为 InAppPurchase.m时间>。在本教程的其余部分中,此文件将被称为“您的 .m 文件”。

编码

现在我们将进入实际的编码。将以下代码添加到您的 .h 文件中:

BOOL areAdsRemoved;

- (IBAction)restore;
- (IBAction)tapsRemoveAds;

接下来,您需要将StoreKit 框架导入您的.m 文件,并在您的@interface 声明后添加SKProductsRequestDelegate 和SKPaymentTransactionObserver:

#import <StoreKit/StoreKit.h>

//put the name of your view controller in place of MyViewController
@interface MyViewController() <SKProductsRequestDelegate, SKPaymentTransactionObserver>

@end

@implementation MyViewController //the name of your view controller (same as above)
  //the code below will be added here
@end

现在将以下内容添加到您的.m文件中,这部分变得复杂,所以我建议您阅读代码中的cmets:

//If you have more than one in-app purchase, you can define both of
//of them here. So, for example, you could define both kRemoveAdsProductIdentifier
//and kBuyCurrencyProductIdentifier with their respective product ids
//
//for this example, we will only use one product

#define kRemoveAdsProductIdentifier @"put your product id (the one that we just made in App Store Connect) in here"

- (IBAction)tapsRemoveAds{
    NSLog(@"User requests to remove ads");

    if([SKPaymentQueue canMakePayments]){
        NSLog(@"User can make payments");
    
        //If you have more than one in-app purchase, and would like
        //to have the user purchase a different product, simply define 
        //another function and replace kRemoveAdsProductIdentifier with 
        //the identifier for the other product

        SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:kRemoveAdsProductIdentifier]];
        productsRequest.delegate = self;
        [productsRequest start];
    
    }
    else{
        NSLog(@"User cannot make payments due to parental controls");
        //this is called the user cannot make payments, most likely due to parental controls
    }
}

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    SKProduct *validProduct = nil;
    int count = [response.products count];
    if(count > 0){
        validProduct = [response.products objectAtIndex:0];
        NSLog(@"Products Available!");
        [self purchase:validProduct];
    }
    else if(!validProduct){
        NSLog(@"No products available");
        //this is called if your product id is not valid, this shouldn't be called unless that happens.
    }
}

- (void)purchase:(SKProduct *)product{
    SKPayment *payment = [SKPayment paymentWithProduct:product];

    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

- (IBAction) restore{
    //this is called when the user restores purchases, you should hook this up to a button
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

- (void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
    NSLog(@"received restored transactions: %i", queue.transactions.count);
    for(SKPaymentTransaction *transaction in queue.transactions){
        if(transaction.transactionState == SKPaymentTransactionStateRestored){
            //called when the user successfully restores a purchase
            NSLog(@"Transaction state -> Restored");

            //if you have more than one in-app purchase product,
            //you restore the correct product for the identifier.
            //For example, you could use
            //if(productID == kRemoveAdsProductIdentifier)
            //to get the product identifier for the
            //restored purchases, you can use
            //
            //NSString *productID = transaction.payment.productIdentifier;
            [self doRemoveAds];
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            break;
        }
    }   
}

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
    for(SKPaymentTransaction *transaction in transactions){
        //if you have multiple in app purchases in your app,
        //you can get the product identifier of this transaction
        //by using transaction.payment.productIdentifier
        //
        //then, check the identifier against the product IDs
        //that you have defined to check which product the user
        //just purchased            

        switch(transaction.transactionState){
            case SKPaymentTransactionStatePurchasing: NSLog(@"Transaction state -> Purchasing");
                //called when the user is in the process of purchasing, do not add any of your own code here.
                break;
            case SKPaymentTransactionStatePurchased:
            //this is called when the user has successfully purchased the package (Cha-Ching!)
                [self doRemoveAds]; //you can add your code for what you want to happen when the user buys the purchase here, for this tutorial we use removing ads
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                NSLog(@"Transaction state -> Purchased");
                break;
            case SKPaymentTransactionStateRestored:
                NSLog(@"Transaction state -> Restored");
                //add the same code as you did from SKPaymentTransactionStatePurchased here
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                //called when the transaction does not finish
                if(transaction.error.code == SKErrorPaymentCancelled){
                    NSLog(@"Transaction state -> Cancelled");
                    //the user cancelled the payment ;(
                }
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
        }
    }
}

现在您想为用户完成交易时发生的情况添加代码,在本教程中,我们使用删除添加,您必须添加自己的代码来了解横幅视图加载时发生的情况。

- (void)doRemoveAds{
    ADBannerView *banner;
    [banner setAlpha:0];
    areAdsRemoved = YES;
    removeAdsButton.hidden = YES;
    removeAdsButton.enabled = NO;
    [[NSUserDefaults standardUserDefaults] setBool:areAdsRemoved forKey:@"areAdsRemoved"];
    //use NSUserDefaults so that you can load whether or not they bought it
    //it would be better to use KeyChain access, or something more secure
    //to store the user data, because NSUserDefaults can be changed.
    //You're average downloader won't be able to change it very easily, but
    //it's still best to use something more secure than NSUserDefaults.
    //For the purpose of this tutorial, though, we're going to use NSUserDefaults
    [[NSUserDefaults standardUserDefaults] synchronize];
}

如果您的应用程序中没有广告,您可以使用任何其他您想要的东西。例如,我们可以将背景的颜色设为蓝色。为此,我们希望使用:

- (void)doRemoveAds{
    [self.view setBackgroundColor:[UIColor blueColor]];
    areAdsRemoved = YES
    //set the bool for whether or not they purchased it to YES, you could use your own boolean here, but you would have to declare it in your .h file

    [[NSUserDefaults standardUserDefaults] setBool:areAdsRemoved forKey:@"areAdsRemoved"];
    //use NSUserDefaults so that you can load wether or not they bought it
    [[NSUserDefaults standardUserDefaults] synchronize];
}

现在,在您的 viewDidLoad 方法的某处,您将要添加以下代码:

areAdsRemoved = [[NSUserDefaults standardUserDefaults] boolForKey:@"areAdsRemoved"];
[[NSUserDefaults standardUserDefaults] synchronize];
//this will load wether or not they bought the in-app purchase

if(areAdsRemoved){
    [self.view setBackgroundColor:[UIColor blueColor]];
    //if they did buy it, set the background to blue, if your using the code above to set the background to blue, if your removing ads, your going to have to make your own code here
}

现在您已经添加了所有代码,进入您的.xib 或storyboard 文件,并添加两个按钮,一个表示购买,另一个表示恢复。将tapsRemoveAds IBAction 连接到您刚刚制作的购买按钮,将restore IBAction 连接到恢复按钮。 restore 操作将检查用户之前是否购买过应用内购买,如果他们还没有应用内购买,则免费提供给他们。

提交审核

接下来,进入App Store Connect,单击Users and Access,然后单击Sandbox Testers 标题,然后单击左侧显示Testers 的+ 符号。您可以随意输入名字和姓氏,电子邮件不必是真实的 - 您只需要能够记住它即可。输入密码(您必须记住)并填写其余信息。我建议您将Date of Birth 设为用户年满 18 岁的日期。 App Store Territory 必须在正确的国家。接下来,退出您现有的 iTunes 帐户(您可以在完成本教程后重新登录)。

现在,在您的 iOS 设备上运行您的应用程序,如果您尝试在模拟器上运行它,购买将总是错误,您必须在您的 iOS 设备上运行它设备。应用程序运行后,点击购买按钮。当系统提示您登录 iTunes 帐户时,请以我们刚刚创建的测试用户身份登录。接下来,当它要求您确认购买 99 美分或您设置的任何价格等级时,对其进行屏幕截图这就是您要在 App Store 上为您的 screenshot for review 使用的内容连接。现在取消付款。

现在,转到App Store Connect,然后转到My Apps > the app you have the In-app purchase on > In-App Purchases。然后单击您的应用内购买,然后单击应用内购买详细信息下的编辑。完成后,将您刚刚在 iPhone 上拍摄的照片导入计算机,并将其作为屏幕截图上传以供审核,然后在审核备注中输入您的 TEST USER 电子邮件和密码。这将有助于苹果在审核过程中。

完成此操作后,返回 iOS 设备上的应用程序,仍以测试用户帐户登录,然后单击购买按钮。这次,确认付款别担心,这不会向您的帐户收取任何费用,测试用户帐户免费获得所有应用内购买确认付款后,请确保发生了什么当用户真正购买你的产品时。如果没有,那么这将是您的 doRemoveAds 方法的错误。同样,我建议使用将背景更改为蓝色来测试应用内购买,但这不应该是您实际的应用内购买。如果一切正常,你就可以开始了!只需确保在将新二进制文件上传到 App Store Connect 时将应用内购买包含在其中!


以下是一些常见错误:

已记录: No Products Available

这可能意味着四件事:

  • 您没有在代码中输入正确的应用内购买 ID(对于上述代码中的标识符 kRemoveAdsProductIdentifier
  • 您没有清除在App Store Connect 上出售的应用内购买项目
  • 您没有等待应用内购买 ID 在App Store Connect 中注册。创建 ID 后等待几个小时,您的问题应该会得到解决。
  • 您没有完成填写协议、税务和银行信息。

如果第一次没有成功,不要沮丧!不要放弃!我花了大约 5 个小时才可以让它工作,大约 10 个小时寻找正确的代码!如果您完全使用上面的代码,它应该可以正常工作。如果您有任何问题,请随时发表评论。

我希望这对所有希望在他们的 iOS 应用程序中添加应用内购买的人有所帮助。干杯!

【讨论】:

  • 但是如果我不添加那行,当我点击恢复按钮时什么也没有发生。无论如何,非常感谢你的教程;)
  • "if(*transaction* == SKPaymentTransactionStateRestored){ " 应该是 if(*transaction.transactionState* == SKPaymentTransactionStateRestored){跨度>
  • Apple 的最佳实践建议您将事务观察器添加到 AppDelegate,而不是视图控制器操作。 developer.apple.com/library/ios/technotes/tn2387/_index.html
  • 我统计了 0 件产品,但我已经检查了您列出的 3 个可能原因。唯一想到的是,如果我没有在itunes connect中的“ios付费应用合同”上设置联系信息、银行信息和税务信息,这可能是原因吗?
  • 您应该解释在第 9 步中,显示名称是呈现给用户的内容。它以这种方式呈现:“你想以 0.99 美元的价格购买一个 DISPLAY NAME 吗?”。这很重要,因为我将显示名称设为“删除广告”,然后我的应用程序被拒绝,因为我在弹出窗口中使用了不正确的语法!我不得不将我的显示名称更改为“广告删除包”。
【解决方案2】:

快速回答

这是为 Swift 用户补充 my Objective-C answer,以防止 Objective-C 答案变得太大。

设置

首先,在appstoreconnect.apple.com 上设置应用内购买。按照my Objective-C answer 的开头部分(步骤 1-13,在 App Store Connect 标题下)获取有关操作的说明。

您的产品 ID 可能需要几个小时才能在 App Store Connect 中注册,因此请耐心等待。

现在您已经在 App Store Connect 上设置了应用内购买信息,我们需要将 Apple 的应用内购买框架 StoreKit 添加到应用中。

进入您的 Xcode 项目,然后转到应用程序管理器(左侧栏顶部的蓝色页面状图标,您的应用程序文件所在的位置)。单击左侧目标下的应用程序(它应该是第一个选项),然后转到顶部的“功能”。在列表中,您应该会看到“应用内购买”选项。打开此功能,Xcode 会将StoreKit 添加到您的项目中。

编码

现在,我们要开始编码了!

首先,创建一个新的 swift 文件来管理您的所有应用内购买。我将称之为IAPManager.swift。

在这个文件中,我们将创建一个名为IAPManager 的新类,即SKProductsRequestDelegate 和SKPaymentTransactionObserver。在顶部,确保导入 Foundation 和 StoreKit

import Foundation
import StoreKit

public class IAPManager: NSObject, SKProductsRequestDelegate,
                         SKPaymentTransactionObserver {
}

接下来,我们将添加一个变量来定义应用内购买的标识符(您也可以使用 enum,如果您有多个 IAP,这将更容易维护)。

// This should the ID of the in-app-purchase you made on AppStore Connect.
// if you have multiple IAPs, you'll need to store their identifiers in
// other variables, too (or, preferably in an enum).
let removeAdsID = "com.skiplit.removeAds"

接下来为我们的类添加一个初始化器:

// This is the initializer for your IAPManager class
//
// A better, and more scaleable way of doing this
// is to also accept a callback in the initializer, and call
// that callback in places like the paymentQueue function, and
// in all functions in this class, in place of calls to functions
// in RemoveAdsManager (you'll see those calls in the code below).

let productID: String
init(productID: String){
    self.productID = productID
}

现在,我们将添加 SKProductsRequestDelegate 和 SKPaymentTransactionObserver 所需的函数来工作:

我们稍后会添加RemoveAdsManager 类

// This is called when a SKProductsRequest receives a response
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse){
    // Let's try to get the first product from the response
    // to the request
    if let product = response.products.first{
        // We were able to get the product! Make a new payment
        // using this product
        let payment = SKPayment(product: product)

        // add the new payment to the queue
        SKPaymentQueue.default().add(self)
        SKPaymentQueue.default().add(payment)
    }
    else{
        // Something went wrong! It is likely that either
        // the user doesn't have internet connection, or
        // your product ID is wrong!
        //
        // Tell the user in requestFailed() by sending an alert,
        // or something of the sort

        RemoveAdsManager.removeAdsFailure()
    }
}

// This is called when the user restores their IAP sucessfully
private func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue){
    // For every transaction in the transaction queue...
    for transaction in queue.transactions{
        // If that transaction was restored
        if transaction.transactionState == .restored{
            // get the producted ID from the transaction
            let productID = transaction.payment.productIdentifier

            // In this case, we have only one IAP, so we don't need to check
            // what IAP it is. However, this is useful if you have multiple IAPs!
            // You'll need to figure out which one was restored
            if(productID.lowercased() == IAPManager.removeAdsID.lowercased()){
                // Restore the user's purchases
                RemoveAdsManager.restoreRemoveAdsSuccess()
            }

            // finish the payment
            SKPaymentQueue.default().finishTransaction(transaction)
        }
    }
}

// This is called when the state of the IAP changes -- from purchasing to purchased, for example.
// This is where the magic happens :)
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]){
    for transaction in transactions{
        // get the producted ID from the transaction
        let productID = transaction.payment.productIdentifier

        // In this case, we have only one IAP, so we don't need to check
        // what IAP it is.
        // However, if you have multiple IAPs, you'll need to use productID
        // to check what functions you should run here!

        switch transaction.transactionState{
        case .purchasing:
            // if the user is currently purchasing the IAP,
            // we don't need to do anything.
            //
            // You could use this to show the user
            // an activity indicator, or something like that
            break
        case .purchased:
            // the user successfully purchased the IAP!
            RemoveAdsManager.removeAdsSuccess()
            SKPaymentQueue.default().finishTransaction(transaction)
        case .restored:
                // the user restored their IAP!
                IAPTestingHandler.restoreRemoveAdsSuccess()
                SKPaymentQueue.default().finishTransaction(transaction)
        case .failed:
                // The transaction failed!
                RemoveAdsManager.removeAdsFailure()
                // finish the transaction
                SKPaymentQueue.default().finishTransaction(transaction)
        case .deferred:
                // This happens when the IAP needs an external action
                // in order to proceeded, like Ask to Buy
                RemoveAdsManager.removeAdsDeferred()
                break
        }
    }
}

现在让我们添加一些可用于开始购买或恢复购买的功能:

// Call this when you want to begin a purchase
// for the productID you gave to the initializer
public func beginPurchase(){
    // If the user can make payments
    if SKPaymentQueue.canMakePayments(){
        // Create a new request
        let request = SKProductsRequest(productIdentifiers: [productID])
        // Set the request delegate to self, so we receive a response
        request.delegate = self
        // start the request
        request.start()
    }
    else{
        // Otherwise, tell the user that
        // they are not authorized to make payments,
        // due to parental controls, etc
    }
}

// Call this when you want to restore all purchases
// regardless of the productID you gave to the initializer
public func beginRestorePurchases(){
    // restore purchases, and give responses to self
    SKPaymentQueue.default().add(self)
    SKPaymentQueue.default().restoreCompletedTransactions()
}

接下来,让我们添加一个新的实用程序类来管理我们的 IAP。所有这些代码都可以在一个类中,但是拥有多个代码会使它更干净一些。我将创建一个名为RemoveAdsManager 的新类,并在其中放置一些函数

public class RemoveAdsManager{

    class func removeAds()
    class func restoreRemoveAds()

    class func areAdsRemoved() -> Bool

    class func removeAdsSuccess()
    class func restoreRemoveAdsSuccess()
    class func removeAdsDeferred()
    class func removeAdsFailure()
}

前三个函数removeAds、restoreRemoveAds 和areAdsRemoved 是您将调用以执行某些操作的函数。最后四个是 IAPManager 将调用的一个。

让我们在前两个函数removeAds和restoreRemoveAds中添加一些代码:

// Call this when the user wants
// to remove ads, like when they
// press a "remove ads" button
class func removeAds(){
    // Before starting the purchase, you could tell the
    // user that their purchase is happening, maybe with
    // an activity indicator

    let iap = IAPManager(productID: IAPManager.removeAdsID)
    iap.beginPurchase()
}

// Call this when the user wants
// to restore their IAP purchases,
// like when they press a "restore
// purchases" button.
class func restoreRemoveAds(){
    // Before starting the purchase, you could tell the
    // user that the restore action is happening, maybe with
    // an activity indicator

    let iap = IAPManager(productID: IAPManager.removeAdsID)
    iap.beginRestorePurchases()
}

最后,让我们为最后五个函数添加一些代码。

// Call this to check whether or not
// ads are removed. You can use the
// result of this to hide or show
// ads
class func areAdsRemoved() -> Bool{
    // This is the code that is run to check
    // if the user has the IAP.

    return UserDefaults.standard.bool(forKey: "RemoveAdsPurchased")
}

// This will be called by IAPManager
// when the user sucessfully purchases
// the IAP
class func removeAdsSuccess(){
    // This is the code that is run to actually
    // give the IAP to the user!
    //
    // I'm using UserDefaults in this example,
    // but you may want to use Keychain,
    // or some other method, as UserDefaults
    // can be modified by users using their
    // computer, if they know how to, more
    // easily than Keychain

    UserDefaults.standard.set(true, forKey: "RemoveAdsPurchased")
    UserDefaults.standard.synchronize()
}

// This will be called by IAPManager
// when the user sucessfully restores
//  their purchases
class func restoreRemoveAdsSuccess(){
    // Give the user their IAP back! Likely all you'll need to
    // do is call the same function you call when a user
    // sucessfully completes their purchase. In this case, removeAdsSuccess()

    removeAdsSuccess()
}

// This will be called by IAPManager
// when the IAP failed
class func removeAdsFailure(){
    // Send the user a message explaining that the IAP
    // failed for some reason, and to try again later
}

// This will be called by IAPManager
// when the IAP gets deferred.
class func removeAdsDeferred(){
    // Send the user a message explaining that the IAP
    // was deferred, and pending an external action, like
    // Ask to Buy.
}

把它们放在一起,我们会得到这样的结果:

import Foundation
import StoreKit

public class RemoveAdsManager{

    // Call this when the user wants
    // to remove ads, like when they
    // press a "remove ads" button
    class func removeAds(){
        // Before starting the purchase, you could tell the
        // user that their purchase is happening, maybe with
        // an activity indicator

        let iap = IAPManager(productID: IAPManager.removeAdsID)
        iap.beginPurchase()
    }

    // Call this when the user wants
    // to restore their IAP purchases,
    // like when they press a "restore
    // purchases" button.
    class func restoreRemoveAds(){
        // Before starting the purchase, you could tell the
        // user that the restore action is happening, maybe with
        // an activity indicator

        let iap = IAPManager(productID: IAPManager.removeAdsID)
        iap.beginRestorePurchases()
    }

    // Call this to check whether or not
    // ads are removed. You can use the
    // result of this to hide or show
    // ads
    class func areAdsRemoved() -> Bool{
        // This is the code that is run to check
        // if the user has the IAP.

        return UserDefaults.standard.bool(forKey: "RemoveAdsPurchased")
    }

    // This will be called by IAPManager
    // when the user sucessfully purchases
    // the IAP
    class func removeAdsSuccess(){
        // This is the code that is run to actually
        // give the IAP to the user!
        //
        // I'm using UserDefaults in this example,
        // but you may want to use Keychain,
        // or some other method, as UserDefaults
        // can be modified by users using their
        // computer, if they know how to, more
        // easily than Keychain

        UserDefaults.standard.set(true, forKey: "RemoveAdsPurchased")
        UserDefaults.standard.synchronize()
    }

    // This will be called by IAPManager
    // when the user sucessfully restores
    //  their purchases
    class func restoreRemoveAdsSuccess(){
        // Give the user their IAP back! Likely all you'll need to
        // do is call the same function you call when a user
        // sucessfully completes their purchase. In this case, removeAdsSuccess()
        removeAdsSuccess()
    }

    // This will be called by IAPManager
    // when the IAP failed
    class func removeAdsFailure(){
        // Send the user a message explaining that the IAP
        // failed for some reason, and to try again later
    }

    // This will be called by IAPManager
    // when the IAP gets deferred.
    class func removeAdsDeferred(){
        // Send the user a message explaining that the IAP
        // was deferred, and pending an external action, like
        // Ask to Buy.
    }

}

public class IAPManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver{

    // This should the ID of the in-app-purchase you made on AppStore Connect.
    // if you have multiple IAPs, you'll need to store their identifiers in
    // other variables, too (or, preferably in an enum).
    static let removeAdsID = "com.skiplit.removeAds"

    // This is the initializer for your IAPManager class
    //
    // An alternative, and more scaleable way of doing this
    // is to also accept a callback in the initializer, and call
    // that callback in places like the paymentQueue function, and
    // in all functions in this class, in place of calls to functions
    // in RemoveAdsManager.
    let productID: String
    init(productID: String){
        self.productID = productID
    }

    // Call this when you want to begin a purchase
    // for the productID you gave to the initializer
    public func beginPurchase(){
        // If the user can make payments
        if SKPaymentQueue.canMakePayments(){
            // Create a new request
            let request = SKProductsRequest(productIdentifiers: [productID])
            request.delegate = self
            request.start()
        }
        else{
            // Otherwise, tell the user that
            // they are not authorized to make payments,
            // due to parental controls, etc
        }
    }

    // Call this when you want to restore all purchases
    // regardless of the productID you gave to the initializer
    public func beginRestorePurchases(){
        SKPaymentQueue.default().add(self)
        SKPaymentQueue.default().restoreCompletedTransactions()
    }

    // This is called when a SKProductsRequest receives a response
    public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse){
        // Let's try to get the first product from the response
        // to the request
        if let product = response.products.first{
            // We were able to get the product! Make a new payment
            // using this product
            let payment = SKPayment(product: product)

            // add the new payment to the queue
            SKPaymentQueue.default().add(self)
            SKPaymentQueue.default().add(payment)
        }
        else{
            // Something went wrong! It is likely that either
            // the user doesn't have internet connection, or
            // your product ID is wrong!
            //
            // Tell the user in requestFailed() by sending an alert,
            // or something of the sort

            RemoveAdsManager.removeAdsFailure()
        }
    }

    // This is called when the user restores their IAP sucessfully
    private func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue){
        // For every transaction in the transaction queue...
        for transaction in queue.transactions{
            // If that transaction was restored
            if transaction.transactionState == .restored{
                // get the producted ID from the transaction
                let productID = transaction.payment.productIdentifier

                // In this case, we have only one IAP, so we don't need to check
                // what IAP it is. However, this is useful if you have multiple IAPs!
                // You'll need to figure out which one was restored
                if(productID.lowercased() == IAPManager.removeAdsID.lowercased()){
                    // Restore the user's purchases
                    RemoveAdsManager.restoreRemoveAdsSuccess()
                }

                // finish the payment
                SKPaymentQueue.default().finishTransaction(transaction)
            }
        }
    }

    // This is called when the state of the IAP changes -- from purchasing to purchased, for example.
    // This is where the magic happens :)
    public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]){
        for transaction in transactions{
            // get the producted ID from the transaction
            let productID = transaction.payment.productIdentifier

            // In this case, we have only one IAP, so we don't need to check
            // what IAP it is.
            // However, if you have multiple IAPs, you'll need to use productID
            // to check what functions you should run here!

            switch transaction.transactionState{
            case .purchasing:
                // if the user is currently purchasing the IAP,
                // we don't need to do anything.
                //
                // You could use this to show the user
                // an activity indicator, or something like that
                break
            case .purchased:
                // the user sucessfully purchased the IAP!
                RemoveAdsManager.removeAdsSuccess()
                SKPaymentQueue.default().finishTransaction(transaction)
            case .restored:
                // the user restored their IAP!
                RemoveAdsManager.restoreRemoveAdsSuccess()
                SKPaymentQueue.default().finishTransaction(transaction)
            case .failed:
                // The transaction failed!
                RemoveAdsManager.removeAdsFailure()
                // finish the transaction
                SKPaymentQueue.default().finishTransaction(transaction)
            case .deferred:
                // This happens when the IAP needs an external action
                // in order to proceeded, like Ask to Buy
                RemoveAdsManager.removeAdsDeferred()
                break
            }
        }
    }

}

最后,您需要添加一些方法让用户开始购买并调用RemoveAdsManager.removeAds() 并开始恢复并调用RemoveAdsManager.restoreRemoveAds(),就像某处的按钮一样!请记住,根据 App Store 指南,您确实需要提供一个按钮以在某处恢复购买。

提交审核

最后要做的就是在 App Store Connect 上提交您的 IAP 以供审核!有关执行此操作的详细说明,您可以按照 Submitting for review 标题下 my Objective-C answer 的最后部分。

【讨论】:

    【解决方案3】:

    只需将 Jojodmo 代码翻译成 Swift:

    class InAppPurchaseManager: NSObject , SKProductsRequestDelegate, SKPaymentTransactionObserver{
    
    
    
    
    
    //If you have more than one in-app purchase, you can define both of
    //of them here. So, for example, you could define both kRemoveAdsProductIdentifier
    //and kBuyCurrencyProductIdentifier with their respective product ids
    //
    //for this example, we will only use one product
    
    let kRemoveAdsProductIdentifier = "put your product id (the one that we just made in iTunesConnect) in here"
    
    @IBAction func tapsRemoveAds() {
    
        NSLog("User requests to remove ads")
    
        if SKPaymentQueue.canMakePayments() {
            NSLog("User can make payments")
    
            //If you have more than one in-app purchase, and would like
            //to have the user purchase a different product, simply define
            //another function and replace kRemoveAdsProductIdentifier with
            //the identifier for the other product
            let set : Set<String> = [kRemoveAdsProductIdentifier]
            let productsRequest = SKProductsRequest(productIdentifiers: set)
            productsRequest.delegate = self
            productsRequest.start()
    
        }
        else {
            NSLog("User cannot make payments due to parental controls")
            //this is called the user cannot make payments, most likely due to parental controls
        }
    }
    
    
    func purchase(product : SKProduct) {
    
        let payment = SKPayment(product: product)
        SKPaymentQueue.defaultQueue().addTransactionObserver(self)
        SKPaymentQueue.defaultQueue().addPayment(payment)
    }
    
    func restore() {
        //this is called when the user restores purchases, you should hook this up to a button
        SKPaymentQueue.defaultQueue().addTransactionObserver(self)
        SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
    }
    
    
    func doRemoveAds() {
        //TODO: implement
    }
    
    /////////////////////////////////////////////////
    //////////////// store delegate /////////////////
    /////////////////////////////////////////////////
    // MARK: - store delegate -
    
    
    func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
    
        if let validProduct = response.products.first {
            NSLog("Products Available!")
            self.purchase(validProduct)
        }
        else {
            NSLog("No products available")
            //this is called if your product id is not valid, this shouldn't be called unless that happens.
        }
    }
    
    func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
    
    
        NSLog("received restored transactions: \(queue.transactions.count)")
        for transaction in queue.transactions {
            if transaction.transactionState == .Restored {
                //called when the user successfully restores a purchase
                NSLog("Transaction state -> Restored")
    
                //if you have more than one in-app purchase product,
                //you restore the correct product for the identifier.
                //For example, you could use
                //if(productID == kRemoveAdsProductIdentifier)
                //to get the product identifier for the
                //restored purchases, you can use
                //
                //NSString *productID = transaction.payment.productIdentifier;
                self.doRemoveAds()
                SKPaymentQueue.defaultQueue().finishTransaction(transaction)
                break;
            }
        }
    }
    
    
    func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    
        for transaction in transactions {
            switch transaction.transactionState {
            case .Purchasing: NSLog("Transaction state -> Purchasing")
                //called when the user is in the process of purchasing, do not add any of your own code here.
            case .Purchased:
                //this is called when the user has successfully purchased the package (Cha-Ching!)
                self.doRemoveAds() //you can add your code for what you want to happen when the user buys the purchase here, for this tutorial we use removing ads
                SKPaymentQueue.defaultQueue().finishTransaction(transaction)
                NSLog("Transaction state -> Purchased")
            case .Restored:
                NSLog("Transaction state -> Restored")
                //add the same code as you did from SKPaymentTransactionStatePurchased here
                SKPaymentQueue.defaultQueue().finishTransaction(transaction)
            case .Failed:
                //called when the transaction does not finish
                if transaction.error?.code == SKErrorPaymentCancelled {
                    NSLog("Transaction state -> Cancelled")
                    //the user cancelled the payment ;(
                }
                SKPaymentQueue.defaultQueue().finishTransaction(transaction)
            case .Deferred:
                // The transaction is in the queue, but its final status is pending external action.
                NSLog("Transaction state -> Deferred")
    
            }
    
    
        }
    }
    } 
    

    【讨论】:

      【解决方案4】:

      RMStore 是一个用于应用内购买的轻量级 iOS 库。它包装了 StoreKit API,并为您提供了方便的异步请求块。购买产品就像调用一个方法一样简单。

      对于高级用户,该库还提供收据验证、内容下载和事务持久性。

      【讨论】:

        【解决方案5】:

        我知道我发布这篇文章已经很晚了,但是当我了解 IAP 模型的原理时,我分享了类似的经验。

        应用内购买是 iOS 中由 Storekit 框架实现的最全面的工作流程之一。 entire documentation 很清楚,如果您耐心阅读它,但在技术性方面有些先进。

        总结一下:

        1 - 请求产品 - 使用 SKProductRequest 和 SKProductRequestDelegate 类发出对产品 ID 的请求并从您自己的 itunesconnect 商店接收它们。

        这些 SKProducts 应该用于填充您的商店 UI,用户可以使用它来购买特定产品。

        2 - 发出付款请求 - 使用 SKPayment 和 SKPaymentQueue 将付款添加到交易队列。

        3 - 监控交易队列的状态更新 - 使用 SKPaymentTransactionObserver 协议的 updatedTransactions 方法来监控状态:

        SKPaymentTransactionStatePurchasing - don't do anything
        SKPaymentTransactionStatePurchased - unlock product, finish the transaction
        SKPaymentTransactionStateFailed - show error, finish the transaction
        SKPaymentTransactionStateRestored - unlock product, finish the transaction
        

        4 - 恢复按钮流程 - 使用 SKPaymentQueue 的 restoreCompletedTransactions 来完成此操作 - 第 3 步将处理其余的工作,以及 SKPaymentTransactionObserver 的以下方法:

        paymentQueueRestoreCompletedTransactionsFinished
        restoreCompletedTransactionsFailedWithError
        

        Here 是一个分步教程(由我自己编写,是我自己尝试理解它的结果),它解释了它。最后还提供了可以直接使用的代码示例。

        Here 是我为解释某些只有文字才能更好地描述的某些事情而创建的另一个。

        【讨论】:

        • StackOverflow 是一个帮助他人的网站,而不是试图从他们身上赚钱。您应该删除倒数第二个链接,或者只是在此处免费发布该教程中完成的内容。
        • @Jojodmo 你能用 SO 的任何指导方针证实你的主张吗?我看到很多人用免责声明推销自己的 SDK(甚至是付费的),我认为这里也很常见。
        • 没有针对它的指导方针,但如果您来这里是为了赚钱,那么您来这里可能是出于错误的原因。 IMO,您的回答似乎侧重于让人们注册您的视频教程,而不是帮助他人
        • 这只是烦恼而已。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-01-07
        • 1970-01-01
        • 2018-11-28
        • 2011-09-17
        • 2015-03-13
        相关资源
        最近更新 更多