Skip to content

Commit d4579d9

Browse files
authored
Merge pull request #107 from bizz84/feature/non-atomic-purchases
Feature/non atomic purchases
2 parents 3180fca + c936b12 commit d4579d9

File tree

12 files changed

+428
-236
lines changed

12 files changed

+428
-236
lines changed

README.md

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ SwiftyStoreKit is a lightweight In App Purchases framework for iOS 8.0+, tvOS 9.
2323
<img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview.png" width="320">
2424
<img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview2.png" width="320">
2525

26-
### Setup + Complete Transactions
26+
## App startup
27+
28+
### Complete Transactions
2729

2830
Apple recommends to register a transaction observer [as soon as the app starts](https://developer.apple.com/library/ios/technotes/tn2387/_index.html):
2931
> Adding your app's observer at launch ensures that it will persist during all launches of your app, thus allowing your app to receive all the payment queue notifications.
@@ -33,13 +35,17 @@ SwiftyStoreKit supports this by calling `completeTransactions()` when the app st
3335
```swift
3436
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
3537

36-
SwiftyStoreKit.completeTransactions() { completedTransactions in
38+
SwiftyStoreKit.completeTransactions(atomically: true) { products in
3739

38-
for completedTransaction in completedTransactions {
40+
for product in products {
3941

40-
if completedTransaction.transactionState == .purchased || completedTransaction.transactionState == .restored {
42+
if product.transaction.transactionState == .purchased || product.transaction.transactionState == .restored {
4143

42-
print("purchased: \(completedTransaction.productId)")
44+
if product.needsFinishTransaction {
45+
// Deliver content from server, then:
46+
SwiftyStoreKit.finishTransaction(product.transaction)
47+
}
48+
print("purchased: \(product)")
4349
}
4450
}
4551
}
@@ -49,6 +55,8 @@ func application(application: UIApplication, didFinishLaunchingWithOptions launc
4955

5056
If there are any pending transactions at this point, these will be reported by the completion block so that the app state and UI can be updated.
5157

58+
## Purchases
59+
5260
### Retrieve products info
5361
```swift
5462
SwiftyStoreKit.retrieveProductsInfo(["com.musevisions.SwiftyStoreKit.Purchase1"]) { result in
@@ -64,13 +72,33 @@ SwiftyStoreKit.retrieveProductsInfo(["com.musevisions.SwiftyStoreKit.Purchase1"]
6472
}
6573
}
6674
```
75+
6776
### Purchase a product
6877

78+
* **Atomic**: to be used when the content is delivered immediately.
79+
6980
```swift
70-
SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1") { result in
81+
SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", atomically: true) { result in
7182
switch result {
72-
case .success(let productId):
73-
print("Purchase Success: \(productId)")
83+
case .success(let product):
84+
print("Purchase Success: \(product.productId)")
85+
case .error(let error):
86+
print("Purchase Failed: \(error)")
87+
}
88+
}
89+
```
90+
91+
* **Non-Atomic**: to be used when the content is delivered by the server.
92+
93+
```swift
94+
SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", atomically: false) { result in
95+
switch result {
96+
case .success(let product):
97+
// fetch content from your server, then:
98+
if product.needsFinishTransaction {
99+
SwiftyStoreKit.finishTransaction(product.transaction)
100+
}
101+
print("Purchase Success: \(product.productId)")
74102
case .error(let error):
75103
print("Purchase Failed: \(error)")
76104
}
@@ -79,20 +107,69 @@ SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1") { res
79107

80108
### Restore previous purchases
81109

110+
* **Atomic**: to be used when the content is delivered immediately.
111+
112+
```swift
113+
SwiftyStoreKit.restorePurchases(atomically: true) { results in
114+
if results.restoreFailedProducts.count > 0 {
115+
print("Restore Failed: \(results.restoreFailedProducts)")
116+
}
117+
else if results.restoredProducts.count > 0 {
118+
print("Restore Success: \(results.restoredProducts)")
119+
}
120+
else {
121+
print("Nothing to Restore")
122+
}
123+
}
124+
```
125+
126+
* **Non-Atomic**: to be used when the content is delivered by the server.
127+
82128
```swift
83-
SwiftyStoreKit.restorePurchases() { results in
129+
SwiftyStoreKit.restorePurchases(atomically: false) { results in
84130
if results.restoreFailedProducts.count > 0 {
85131
print("Restore Failed: \(results.restoreFailedProducts)")
86132
}
87-
else if results.restoredProductIds.count > 0 {
88-
print("Restore Success: \(results.restoredProductIds)")
133+
else if results.restoredProducts.count > 0 {
134+
for product in results.restoredProducts {
135+
// fetch content from your server, then:
136+
if product.needsFinishTransaction {
137+
SwiftyStoreKit.finishTransaction(product.transaction)
138+
}
139+
}
140+
print("Restore Success: \(results.restoredProducts)")
89141
}
90142
else {
91143
print("Nothing to Restore")
92144
}
93145
}
94146
```
95147

148+
#### What does atomic / non-atomic mean?
149+
150+
When you purchase a product the following things happen:
151+
152+
* A payment is added to the payment queue for your IAP.
153+
* When the payment has been processed with Apple, the payment queue is updated so that the appropriate transaction can be handled.
154+
* If the transaction state is **purchased** or **restored**, the app can unlock the functionality purchased by the user.
155+
* The app should call `finishTransaction()` to complete the purchase.
156+
157+
This is what is [recommended by Apple](https://developer.apple.com/reference/storekit/skpaymentqueue/1506003-finishtransaction):
158+
159+
> Your application should call finishTransaction(_:) only after it has successfully processed the transaction and unlocked the functionality purchased by the user.
160+
161+
* A purchase is **atomic** when the app unlocks the functionality purchased by the user immediately and call `finishTransaction()` at the same time. This is desirable if you're unlocking functionality that is already inside the app.
162+
163+
* In cases when you need to make a request to your own server in order to unlock the functionality, you can use a **non-atomic** purchase instead.
164+
165+
SwiftyStoreKit provides three operations that can be performed **atomically** or **non-atomically**:
166+
167+
* Making a purchase
168+
* Restoring purchases
169+
* Completing transactions on app launch
170+
171+
## Receipt verification
172+
96173
### Retrieve local receipt
97174

98175
```swift

SwiftyStoreKit-iOS-Demo/AppDelegate.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
5555

5656
func completeIAPTransactions() {
5757

58-
SwiftyStoreKit.completeTransactions() { completedTransactions in
58+
SwiftyStoreKit.completeTransactions(atomically: true) { products in
5959

60-
for completedTransaction in completedTransactions {
60+
for product in products {
6161

62-
if completedTransaction.transactionState == .purchased || completedTransaction.transactionState == .restored {
62+
if product.transaction.transactionState == .purchased || product.transaction.transactionState == .restored {
6363

64-
print("purchased: \(completedTransaction.productId)")
64+
if product.needsFinishTransaction {
65+
// Deliver content from server, then:
66+
SwiftyStoreKit.finishTransaction(product.transaction)
67+
}
68+
print("purchased: \(product.productId)")
6569
}
6670
}
6771
}

SwiftyStoreKit-iOS-Demo/ViewController.swift

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,31 @@ class ViewController: UIViewController {
7777
func purchase(_ purchase: RegisteredPurchase) {
7878

7979
NetworkActivityIndicatorManager.networkOperationStarted()
80-
SwiftyStoreKit.purchaseProduct(AppBundleId + "." + purchase.rawValue) { result in
80+
SwiftyStoreKit.purchaseProduct(AppBundleId + "." + purchase.rawValue, atomically: true) { result in
8181
NetworkActivityIndicatorManager.networkOperationFinished()
8282

83+
if case .success(let product) = result {
84+
// Deliver content from server, then:
85+
if product.needsFinishTransaction {
86+
SwiftyStoreKit.finishTransaction(product.transaction)
87+
}
88+
}
8389
self.showAlert(self.alertForPurchaseResult(result))
8490
}
8591
}
92+
8693
@IBAction func restorePurchases() {
8794

8895
NetworkActivityIndicatorManager.networkOperationStarted()
89-
SwiftyStoreKit.restorePurchases() { results in
96+
SwiftyStoreKit.restorePurchases(atomically: true) { results in
9097
NetworkActivityIndicatorManager.networkOperationFinished()
9198

99+
for product in results.restoredProducts {
100+
// Deliver content from server, then:
101+
if product.needsFinishTransaction {
102+
SwiftyStoreKit.finishTransaction(product.transaction)
103+
}
104+
}
92105
self.showAlert(self.alertForRestorePurchases(results))
93106
}
94107
}
@@ -176,7 +189,7 @@ extension ViewController {
176189
}
177190
}
178191

179-
func alertForProductRetrievalInfo(_ result: SwiftyStoreKit.RetrieveResults) -> UIAlertController {
192+
func alertForProductRetrievalInfo(_ result: RetrieveResults) -> UIAlertController {
180193

181194
if let product = result.retrievedProducts.first {
182195
let priceString = product.localizedPrice!
@@ -191,10 +204,10 @@ extension ViewController {
191204
}
192205
}
193206

194-
func alertForPurchaseResult(_ result: SwiftyStoreKit.PurchaseResult) -> UIAlertController {
207+
func alertForPurchaseResult(_ result: PurchaseResult) -> UIAlertController {
195208
switch result {
196-
case .success(let productId):
197-
print("Purchase Success: \(productId)")
209+
case .success(let product):
210+
print("Purchase Success: \(product.productId)")
198211
return alertWithTitle("Thank You", message: "Purchase completed")
199212
case .error(let error):
200213
print("Purchase Failed: \(error)")
@@ -214,14 +227,14 @@ extension ViewController {
214227
}
215228
}
216229

217-
func alertForRestorePurchases(_ results: SwiftyStoreKit.RestoreResults) -> UIAlertController {
230+
func alertForRestorePurchases(_ results: RestoreResults) -> UIAlertController {
218231

219232
if results.restoreFailedProducts.count > 0 {
220233
print("Restore Failed: \(results.restoreFailedProducts)")
221234
return alertWithTitle("Restore failed", message: "Unknown error. Please contact support")
222235
}
223-
else if results.restoredProductIds.count > 0 {
224-
print("Restore Success: \(results.restoredProductIds)")
236+
else if results.restoredProducts.count > 0 {
237+
print("Restore Success: \(results.restoredProducts)")
225238
return alertWithTitle("Purchases Restored", message: "All purchases have been restored")
226239
}
227240
else {
@@ -231,7 +244,7 @@ extension ViewController {
231244
}
232245

233246

234-
func alertForVerifyReceipt(_ result: SwiftyStoreKit.VerifyReceiptResult) -> UIAlertController {
247+
func alertForVerifyReceipt(_ result: VerifyReceiptResult) -> UIAlertController {
235248

236249
switch result {
237250
case .success(let receipt):
@@ -248,7 +261,7 @@ extension ViewController {
248261
}
249262
}
250263

251-
func alertForVerifySubscription(_ result: SwiftyStoreKit.VerifySubscriptionResult) -> UIAlertController {
264+
func alertForVerifySubscription(_ result: VerifySubscriptionResult) -> UIAlertController {
252265

253266
switch result {
254267
case .purchased(let expiresDate):
@@ -263,7 +276,7 @@ extension ViewController {
263276
}
264277
}
265278

266-
func alertForVerifyPurchase(_ result: SwiftyStoreKit.VerifyPurchaseResult) -> UIAlertController {
279+
func alertForVerifyPurchase(_ result: VerifyPurchaseResult) -> UIAlertController {
267280

268281
switch result {
269282
case .purchased:
@@ -275,7 +288,7 @@ extension ViewController {
275288
}
276289
}
277290

278-
func alertForRefreshReceipt(_ result: SwiftyStoreKit.RefreshReceiptResult) -> UIAlertController {
291+
func alertForRefreshReceipt(_ result: RefreshReceiptResult) -> UIAlertController {
279292
switch result {
280293
case .success(let receiptData):
281294
print("Receipt refresh Success: \(receiptData.base64EncodedString)")

SwiftyStoreKit-macOS-Demo/AppDelegate.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3535

3636
func completeIAPTransactions() {
3737

38-
SwiftyStoreKit.completeTransactions() { completedTransactions in
38+
SwiftyStoreKit.completeTransactions(atomically: true) { products in
3939

40-
for completedTransaction in completedTransactions {
40+
for product in products {
4141

42-
if completedTransaction.transactionState == .purchased || completedTransaction.transactionState == .restored {
42+
if product.transaction.transactionState == .purchased || product.transaction.transactionState == .restored {
4343

44-
print("purchased: \(completedTransaction.productId)")
44+
if product.needsFinishTransaction {
45+
// Deliver content from server, then:
46+
SwiftyStoreKit.finishTransaction(product.transaction)
47+
}
48+
print("purchased: \(product.productId)")
4549
}
4650
}
4751
}

0 commit comments

Comments
 (0)