<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>IAP &#8211; 隨心所欲</title>
	<atom:link href="https://doeverythingiwant.com/tag/iap/feed/" rel="self" type="application/rss+xml" />
	<link>https://doeverythingiwant.com</link>
	<description>iOS Developer 的隨筆記錄</description>
	<lastBuildDate>Mon, 20 Nov 2023 15:11:06 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://doeverythingiwant.com/wp-content/uploads/2025/08/cropped-0802.png</url>
	<title>IAP &#8211; 隨心所欲</title>
	<link>https://doeverythingiwant.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>實作 IAP (In-App Purchase)</title>
		<link>https://doeverythingiwant.com/implement-storekit/</link>
					<comments>https://doeverythingiwant.com/implement-storekit/#respond</comments>
		
		<dc:creator><![CDATA[艾普利]]></dc:creator>
		<pubDate>Mon, 26 Sep 2022 14:47:00 +0000</pubDate>
				<category><![CDATA[寫程式]]></category>
		<category><![CDATA[IAP]]></category>
		<category><![CDATA[In-App Purchase]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[Storekit]]></category>
		<guid isPermaLink="false">https://doeverythingiwant.com/?p=191</guid>

					<description><![CDATA[Photo by Kelly Sikkema on Unsp&#8230;]]></description>
										<content:encoded><![CDATA[
<p class="has-text-align-center">Photo by <a href="https://unsplash.com/@kellysikkema?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" target="_blank" rel="noopener">Kelly Sikkema</a> on <a href="https://unsplash.com/s/photos/buy?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" target="_blank" rel="noopener">Unsplash</a></p>



<p>去年WWDC針對IAP 用的StoreKit 做了大更新，這個SDK 已經用了快10年，中間雖然也是有做過一些些調整，本次可是整個SDK流程都改變了，不過可惜的事，必需要iOS15之後才可以使用新流程，若App支援iOS 14以下的話，還是需要使用過去的那套流程</p>



<span id="more-191"></span>



<p>有一個無論是新舊流程都可以使用的功能，在過去若要測試IAP 的時候一定要去 App store connect 中設定好商品、沙箱測試人員…等等之後才可以進行測試。</p>



<p>現在可以直接使用Xcode 就可以測試了!! 可以測試的範圍可以到購買完成，當然如果要想得到Receipt 還是要在App store connect 設定好且用實機進行測試</p>



<p><a href="https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode#see-also" target="_blank" rel="noopener"><strong>Apple Developer Documentation</strong><br>&nbsp;<em>Setting Up StoreKit Testing in Xcode </em>developer.apple.com</a><a href="https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode#see-also" target="_blank" rel="noopener"></a></p>



<p>此文章會先介紹StoreKit 的程式寫法，StoreKit 2 會撰寫另一篇文章</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>在開始之前，簡單說明一下對於App 而言的IAP 流程是怎麼樣的</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="468" src="https://doeverythingiwant.com/wp-content/uploads/2022/09/6666-1024x468.png" alt="" class="wp-image-193" srcset="https://doeverythingiwant.com/wp-content/uploads/2022/09/6666-1024x468.png 1024w, https://doeverythingiwant.com/wp-content/uploads/2022/09/6666-300x137.png 300w, https://doeverythingiwant.com/wp-content/uploads/2022/09/6666-768x351.png 768w, https://doeverythingiwant.com/wp-content/uploads/2022/09/6666-1536x702.png 1536w, https://doeverythingiwant.com/wp-content/uploads/2022/09/6666-2048x936.png 2048w, https://doeverythingiwant.com/wp-content/uploads/2022/09/6666-600x274.png 600w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>App 裡要做的事情大致上有四件事</p>



<p><strong>1、取得商品資訊</strong></p>



<p><strong>2、購買商品</strong></p>



<p><strong>3、交易狀態處理</strong></p>



<p><strong>4、取得收據</strong></p>



<p>一般大多數都是由後端Server 去處理Verify Receipt，且Apple也不希望是由App 端去進行，若有需要相關資訊的可以參考官網說明</p>



<p><a href="https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/validating_receipts_with_the_app_store" target="_blank" rel="noopener"><strong>Apple Developer Documentation</strong><br><em>Validating Receipts with App Store</em>developer.apple.com</a><a href="https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/validating_receipts_with_the_app_store" target="_blank" rel="noopener"></a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>終於要開始寫程式了!</p>



<p>可至官網下載 Demo Code</p>



<p><a href="https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/offering_completing_and_restoring_in-app_purchases" target="_blank" rel="noopener"><strong>Apple Developer Documentation</strong><br><em>Offering, Completing and Restoring In-App Purchases</em>developer.apple.com</a><a href="https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/offering_completing_and_restoring_in-app_purchases" target="_blank" rel="noopener"></a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading"><strong>取得商品資訊</strong></h2>



<p>要取得商品資訊之後，必需要先知道Product ID ，用Product ID 去向Apple 要取商品資料</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-swift" data-lang="Swift"><code>private(set) var products: [SKProduct] = []
private var productRequest: SKProductsRequest!
    
// MARK: - get production
func getProductInfo() {
    let productIdentifiers = Set([&quot;Item01&quot;, &quot;Item02&quot;, &quot;Item03&quot;])

    productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
    productRequest.delegate = self

    productRequest.start()
}

----------

extension IAPManager: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        if !response.products.isEmpty {
            self.products = response.products
        }
    
        if response.invalidProductIdentifiers.count &gt; 0 {
            print(&quot;invalidProductionIdentifiers: \(response.invalidProductIdentifiers)&quot;)
        }
    }
}</code></pre></div>



<p>取得商品使用 StoreKit 中的 SKProductsRequest，initial 的時候把ProductID 帶入，有一點要留意一下，因為是使用Set 所以取得商品後需要自己去做排序喔</p>



<pre class="wp-block-preformatted">productRequest.start()</pre>



<p>呼叫start() 之後就可以從SKProductsRequestDelegate中的productsRequest裡取得商品，invalidProductIdentifiers 裡的Product ID 是在App Store 找不到相對應的商品</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">購買商品</h2>



<p>從App Store上取得的商品物件是 SKProduct ，在購買的時候也要使用SKProduct 進行</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-swift" data-lang="Swift"><code>func buyProduct(product: SKProduct) {
    let payment = SKPayment(product: product)
    SKPaymentQueue.default().add(payment)
}</code></pre></div>



<p>購買商品的時候需要建立 SKPayment 物件，並把要購買的SKProduct 放進去，接著就是將它放入 SKPaymentQueue裡就行了</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">交易狀態處理</h2>



<p>要得知交易的狀態，需要成為 SKPaymentQueue的observer</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-swift" data-lang="Swift"><code>func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -&gt; Bool {
  ...
  SKPaymentQueue.default().add(IAPManager.shared)
  ...
  return true
}</code></pre></div>



<p>之後就可以在 <strong>func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) </strong>中取得交易狀態</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-swift" data-lang="Swift"><code>extension IAPManager: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            
            case .purchasing:
                print(&quot;[IAPurchase] purchasing&quot;)
            case .purchased:
                print(&quot;[IAPurchase] purchased&quot;)
                SKPaymentQueue.default().finishTransaction(transaction)
            case .failed:
                print(&quot;[IAPurchase] failed error \(String(describing: transaction.error?.localizedDescription))&quot;)
                SKPaymentQueue.default().finishTransaction(transaction)
            case .restored:
                print(&quot;[IAPurchase] restored&quot;)
                SKPaymentQueue.default().finishTransaction(transaction)
            case .deferred:
                print(&quot;[IAPurchase] deferred&quot;)
            @unknown default:
                print(&quot;[IAPurchase] Unexpected transaction state \(transaction.transactionState)&quot;)
            }
        }
    }
}</code></pre></div>



<p>交易狀態共有五種</p>



<p><strong>.purchasing</strong>，正在交易中</p>



<p><strong>.purchased</strong>，交易成功，記得寫上 finishTransaction 結束交易 ，不然下次開啟App 的時候還會再收到一次</p>



<p><strong>.failed</strong>，交易失敗，就看是否有需要處理交易失敗的情況，記得也要寫上 finishTransaction 結束交易</p>



<p><strong>.restored</strong>，若有實作Restore 功能，Restored 就是完成後的狀態，一樣記得要finishTransaction</p>



<p><strong>.deferred</strong>，交易正在 Queue 正等待被處理，基本上和&nbsp;.purchasing 差不多是過渡情況</p>



<p></p>



<p>就看是否要針對其狀態進行處理，一般&nbsp;.purchasing 與&nbsp;.deferred 不需要做什麼特別處理，不過UI上需要顯示 Indicator 讓使用者知道現在正在處理中</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">取得收據</h2>



<p>當收到交易成功的狀態時，就可以在本機上找到收據，一般流程需要就收據丟回給Server 去做後續驗証處理</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-swift" data-lang="Swift"><code>func fetchReceipt() -&gt; String {
        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
           FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
            do {
                let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
                let receiptString = receiptData.base64EncodedString(options: [])
                
                return receiptString
            } catch {
                print(&quot;[IAPurchase] Couldn&#39;t read receipt data with error: &quot; + error.localizedDescription)
                return &quot;&quot;
            }
        }
        return &quot;&quot;
}</code></pre></div>



<p>需要使用 <strong>FileManager</strong> 去取得收據，再把它轉成Base64的字串，因為若要拿這個收據到Apple 上去驗証的話，就需要把收據轉成加密過的 Base64字串，若想知道如何驗証收據可以參考官方文件</p>



<p><a href="https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/validating_receipts_with_the_app_store" target="_blank" rel="noopener"><strong>Apple Developer Documentation</strong><br><em>Validating receipts with the app store</em>developer.apple.com</a><a href="https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/validating_receipts_with_the_app_store" target="_blank" rel="noopener"></a></p>



<p>大多數的情況下都是由Server 去進行收據驗証工作，除非是訂閱型商品，不然App 可以利用交易狀態來得知是否購買成功。</p>



<p>以上是就基本的IAP 實作，基本上官方Demo Code 都寫的很清楚，只要了解官方程式實作起來就不會太難。</p>



<p>那最後就祝大家寫Code 愉快!!</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://doeverythingiwant.com/implement-storekit/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
