Andrea Bizzotto 10 жил өмнө
parent
commit
3f15d3a515

+ 0 - 0
SwiftyStoreKit/AppDelegate.swift → SwiftyStoreDemo/AppDelegate.swift


+ 0 - 0
SwiftyStoreKit/Assets.xcassets/AppIcon.appiconset/Contents.json → SwiftyStoreDemo/Assets.xcassets/AppIcon.appiconset/Contents.json


+ 0 - 0
SwiftyStoreKit/Base.lproj/LaunchScreen.storyboard → SwiftyStoreDemo/Base.lproj/LaunchScreen.storyboard


+ 71 - 0
SwiftyStoreDemo/Base.lproj/Main.storyboard

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8187.4" systemVersion="14E11f" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8151.3"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="SwiftyStoreKit" customModuleProvider="target" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="L02-jg-tgd">
+                                <rect key="frame" x="251" y="76" width="98" height="36"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="20"/>
+                                <state key="normal" title="Purchase 1"/>
+                                <connections>
+                                    <action selector="purchase1" destination="BYZ-38-t0r" eventType="touchUpInside" id="omU-Ey-qsT"/>
+                                </connections>
+                            </button>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="HlX-Me-2Je">
+                                <rect key="frame" x="249.5" y="132" width="101" height="36"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="20"/>
+                                <state key="normal" title="Purchase 2"/>
+                                <connections>
+                                    <action selector="purchase2" destination="BYZ-38-t0r" eventType="touchUpInside" id="e9U-1E-Exo"/>
+                                </connections>
+                            </button>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jJz-4L-d9P">
+                                <rect key="frame" x="249.5" y="188" width="101" height="36"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="20"/>
+                                <state key="normal" title="Purchase 3"/>
+                                <connections>
+                                    <action selector="purchase3" destination="BYZ-38-t0r" eventType="touchUpInside" id="dro-p6-v4G"/>
+                                </connections>
+                            </button>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CG8-Ue-vcg">
+                                <rect key="frame" x="216.5" y="274" width="167" height="36"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="20"/>
+                                <state key="normal" title="Restore Purchases"/>
+                                <connections>
+                                    <action selector="restorePurchases" destination="BYZ-38-t0r" eventType="touchUpInside" id="ulP-6V-3dz"/>
+                                </connections>
+                            </button>
+                        </subviews>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                        <constraints>
+                            <constraint firstItem="jJz-4L-d9P" firstAttribute="top" secondItem="HlX-Me-2Je" secondAttribute="bottom" constant="20" id="4ak-PX-vGn"/>
+                            <constraint firstItem="CG8-Ue-vcg" firstAttribute="top" secondItem="jJz-4L-d9P" secondAttribute="bottom" constant="50" id="Ehw-fT-p1e"/>
+                            <constraint firstItem="CG8-Ue-vcg" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="U3y-1n-HiO"/>
+                            <constraint firstItem="L02-jg-tgd" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="56" id="X3N-i3-4U6"/>
+                            <constraint firstItem="HlX-Me-2Je" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="ZOf-So-6Cx"/>
+                            <constraint firstItem="jJz-4L-d9P" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="aZx-lg-43B"/>
+                            <constraint firstItem="HlX-Me-2Je" firstAttribute="top" secondItem="L02-jg-tgd" secondAttribute="bottom" constant="20" id="ggr-bG-p2t"/>
+                            <constraint firstItem="L02-jg-tgd" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="xyS-Po-vk7"/>
+                        </constraints>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="308" y="374"/>
+        </scene>
+    </scenes>
+</document>

+ 0 - 0
SwiftyStoreKit/Info.plist → SwiftyStoreDemo/Info.plist


+ 79 - 0
SwiftyStoreDemo/ViewController.swift

@@ -0,0 +1,79 @@
+//
+//  ViewController.swift
+//  SwiftyStoreKit
+//
+//  Created by Andrea Bizzotto on 03/09/2015.
+//  Copyright © 2015 musevisions. All rights reserved.
+//
+
+import UIKit
+
+class ViewController: UIViewController {
+
+    let AppBundleId = "com.musevisions.iOS.SwiftyStoreKit"
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+    }
+    
+    func showMessage(title: String, message: String) {
+        
+        guard let _ = self.presentedViewController else {
+            let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert)
+            alert.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: nil))
+            self.presentViewController(alert, animated: true, completion: nil)
+            return
+        }
+    }
+    
+    @IBAction func purchase1() {
+        purchase("1")
+    }
+    @IBAction func purchase2() {
+        purchase("2")
+    }
+    @IBAction func purchase3() {
+        purchase("3")
+    }
+    func purchase(no: String) {
+        
+        SwiftyStore.sharedInstance.purchaseProduct(AppBundleId + ".purchase" + no) { result in
+            
+            switch result {
+            case .Success(let productId):
+                self.showMessage("Thank You", message: "Purchase completed")
+                print("Purchase Success: \(productId)")
+                break
+            case .Error(let error):
+                if case ResponseError.RequestFailed(let internalError) = error where internalError.code == 0 {
+                    self.showMessage("Purchase failed", message: "Please check your Internet connection or try again later")
+                }
+                else {
+                    self.showMessage("Purchase failed", message: "Unknown error. Please contact support")
+                }
+                print("Purchase Failed: \(error)")
+                break
+            }
+        }
+    }
+    @IBAction func restorePurchases() {
+        
+        SwiftyStore.sharedInstance.restorePurchases() { result in
+            switch result {
+            case .Success(let productId):
+                self.showMessage("Purchases Restored", message: "All purchases have been restored")
+                print("Restore Success: \(productId)")
+                break
+            case .NothingToRestore:
+                self.showMessage("Nothing to restore", message: "No previous purchases were found")
+                print("Nothing to Restore")
+                break
+            case .Error(let error):
+                print("Restore Failed: \(error)")
+                break
+            }
+        }
+    }
+
+}
+

+ 54 - 30
SwiftyStoreKit.xcodeproj/project.pbxproj

@@ -7,21 +7,28 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
-		6502F6021B985833004E342D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6011B985833004E342D /* AppDelegate.swift */; };
-		6502F6041B985833004E342D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6031B985833004E342D /* ViewController.swift */; };
-		6502F6071B985833004E342D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6502F6051B985833004E342D /* Main.storyboard */; };
-		6502F6091B985833004E342D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6502F6081B985833004E342D /* Assets.xcassets */; };
-		6502F60C1B985833004E342D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6502F60A1B985833004E342D /* LaunchScreen.storyboard */; };
+		6502F61C1B985858004E342D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6141B985858004E342D /* AppDelegate.swift */; settings = {ASSET_TAGS = (); }; };
+		6502F61D1B985858004E342D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6502F6151B985858004E342D /* Assets.xcassets */; settings = {ASSET_TAGS = (); }; };
+		6502F61E1B985858004E342D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6502F6161B985858004E342D /* LaunchScreen.storyboard */; settings = {ASSET_TAGS = (); }; };
+		6502F61F1B985858004E342D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6502F6181B985858004E342D /* Main.storyboard */; settings = {ASSET_TAGS = (); }; };
+		6502F6201B985858004E342D /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6502F61A1B985858004E342D /* Info.plist */; settings = {ASSET_TAGS = (); }; };
+		6502F6211B985858004E342D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F61B1B985858004E342D /* ViewController.swift */; settings = {ASSET_TAGS = (); }; };
+		6502F6251B98586A004E342D /* InAppProductPurchaseRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6221B98586A004E342D /* InAppProductPurchaseRequest.swift */; settings = {ASSET_TAGS = (); }; };
+		6502F6261B98586A004E342D /* InAppProductQueryRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6231B98586A004E342D /* InAppProductQueryRequest.swift */; settings = {ASSET_TAGS = (); }; };
+		6502F6271B98586A004E342D /* SwiftyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6502F6241B98586A004E342D /* SwiftyStore.swift */; settings = {ASSET_TAGS = (); }; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
 		6502F5FE1B985833004E342D /* SwiftyStoreKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyStoreKit.app; sourceTree = BUILT_PRODUCTS_DIR; };
-		6502F6011B985833004E342D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
-		6502F6031B985833004E342D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
-		6502F6061B985833004E342D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
-		6502F6081B985833004E342D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
-		6502F60B1B985833004E342D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
-		6502F60D1B985833004E342D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		6502F6141B985858004E342D /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		6502F6151B985858004E342D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		6502F6171B985858004E342D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		6502F6191B985858004E342D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		6502F61A1B985858004E342D /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		6502F61B1B985858004E342D /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
+		6502F6221B98586A004E342D /* InAppProductPurchaseRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppProductPurchaseRequest.swift; sourceTree = "<group>"; };
+		6502F6231B98586A004E342D /* InAppProductQueryRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppProductQueryRequest.swift; sourceTree = "<group>"; };
+		6502F6241B98586A004E342D /* SwiftyStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyStore.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -38,6 +45,7 @@
 		6502F5F51B985833004E342D = {
 			isa = PBXGroup;
 			children = (
+				6502F6131B985858004E342D /* SwiftyStoreDemo */,
 				6502F6001B985833004E342D /* SwiftyStoreKit */,
 				6502F5FF1B985833004E342D /* Products */,
 			);
@@ -54,16 +62,26 @@
 		6502F6001B985833004E342D /* SwiftyStoreKit */ = {
 			isa = PBXGroup;
 			children = (
-				6502F6011B985833004E342D /* AppDelegate.swift */,
-				6502F6031B985833004E342D /* ViewController.swift */,
-				6502F6051B985833004E342D /* Main.storyboard */,
-				6502F6081B985833004E342D /* Assets.xcassets */,
-				6502F60A1B985833004E342D /* LaunchScreen.storyboard */,
-				6502F60D1B985833004E342D /* Info.plist */,
+				6502F6221B98586A004E342D /* InAppProductPurchaseRequest.swift */,
+				6502F6231B98586A004E342D /* InAppProductQueryRequest.swift */,
+				6502F6241B98586A004E342D /* SwiftyStore.swift */,
 			);
 			path = SwiftyStoreKit;
 			sourceTree = "<group>";
 		};
+		6502F6131B985858004E342D /* SwiftyStoreDemo */ = {
+			isa = PBXGroup;
+			children = (
+				6502F6141B985858004E342D /* AppDelegate.swift */,
+				6502F6151B985858004E342D /* Assets.xcassets */,
+				6502F6161B985858004E342D /* LaunchScreen.storyboard */,
+				6502F6181B985858004E342D /* Main.storyboard */,
+				6502F61A1B985858004E342D /* Info.plist */,
+				6502F61B1B985858004E342D /* ViewController.swift */,
+			);
+			path = SwiftyStoreDemo;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -121,9 +139,10 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				6502F60C1B985833004E342D /* LaunchScreen.storyboard in Resources */,
-				6502F6091B985833004E342D /* Assets.xcassets in Resources */,
-				6502F6071B985833004E342D /* Main.storyboard in Resources */,
+				6502F6201B985858004E342D /* Info.plist in Resources */,
+				6502F61F1B985858004E342D /* Main.storyboard in Resources */,
+				6502F61D1B985858004E342D /* Assets.xcassets in Resources */,
+				6502F61E1B985858004E342D /* LaunchScreen.storyboard in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -134,28 +153,31 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				6502F6041B985833004E342D /* ViewController.swift in Sources */,
-				6502F6021B985833004E342D /* AppDelegate.swift in Sources */,
+				6502F6211B985858004E342D /* ViewController.swift in Sources */,
+				6502F6261B98586A004E342D /* InAppProductQueryRequest.swift in Sources */,
+				6502F6251B98586A004E342D /* InAppProductPurchaseRequest.swift in Sources */,
+				6502F61C1B985858004E342D /* AppDelegate.swift in Sources */,
+				6502F6271B98586A004E342D /* SwiftyStore.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXVariantGroup section */
-		6502F6051B985833004E342D /* Main.storyboard */ = {
+		6502F6161B985858004E342D /* LaunchScreen.storyboard */ = {
 			isa = PBXVariantGroup;
 			children = (
-				6502F6061B985833004E342D /* Base */,
+				6502F6171B985858004E342D /* Base */,
 			);
-			name = Main.storyboard;
+			name = LaunchScreen.storyboard;
 			sourceTree = "<group>";
 		};
-		6502F60A1B985833004E342D /* LaunchScreen.storyboard */ = {
+		6502F6181B985858004E342D /* Main.storyboard */ = {
 			isa = PBXVariantGroup;
 			children = (
-				6502F60B1B985833004E342D /* Base */,
+				6502F6191B985858004E342D /* Base */,
 			);
-			name = LaunchScreen.storyboard;
+			name = Main.storyboard;
 			sourceTree = "<group>";
 		};
 /* End PBXVariantGroup section */
@@ -246,7 +268,8 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				INFOPLIST_FILE = SwiftyStoreKit/Info.plist;
+				INFOPLIST_FILE = SwiftyStoreDemo/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKit;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -257,7 +280,8 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				INFOPLIST_FILE = SwiftyStoreKit/Info.plist;
+				INFOPLIST_FILE = SwiftyStoreDemo/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.musevisions.iOS.SwiftyStoreKit;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 0 - 25
SwiftyStoreKit/Base.lproj/Main.storyboard

@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6211" systemVersion="14A298i" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
-    <dependencies>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6204"/>
-    </dependencies>
-    <scenes>
-        <!--View Controller-->
-        <scene sceneID="tne-QT-ifu">
-            <objects>
-                <viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
-                    <layoutGuides>
-                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
-                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
-                    </layoutGuides>
-                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
-                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
-                    </view>
-                </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
-            </objects>
-        </scene>
-    </scenes>
-</document>

+ 142 - 0
SwiftyStoreKit/InAppProductPurchaseRequest.swift

@@ -0,0 +1,142 @@
+//
+//  InAppProductPurchaseRequest.swift
+//  WordShooter
+//
+//  Created by Andrea Bizzotto on 01/09/2015.
+//  Copyright © 2015 musevisions. All rights reserved.
+//
+
+import UIKit
+import StoreKit
+
+public class InAppProductPurchaseRequest: NSObject, SKPaymentTransactionObserver {
+
+    enum ResultType {
+        case Purchased(productId: String)
+        case Restored(productId: String)
+        case NothingToRestore
+        case Failed(error: NSError)
+    }
+    
+    typealias RequestCallback = (result: ResultType) -> ()
+    private let callback: RequestCallback
+    private var purchases : [SKPaymentTransactionState: [String]] = [:]
+
+    var paymentQueue: SKPaymentQueue {
+        get {
+            return  SKPaymentQueue.defaultQueue()
+        }
+    }
+    
+    let product : SKProduct?
+    
+    deinit {
+        paymentQueue.removeTransactionObserver(self)
+    }
+    // Initialiser for product purchase
+    private init(product: SKProduct?, callback: RequestCallback) {
+
+        self.product = product
+        self.callback = callback
+        super.init()
+        paymentQueue.addTransactionObserver(self)
+    }
+    // MARK: Public methods
+    class func startPayment(product: SKProduct, callback: RequestCallback) -> InAppProductPurchaseRequest {
+        let request = InAppProductPurchaseRequest(product: product, callback: callback)
+        request.startPayment(product)
+        return request
+    }
+    class func restorePurchases(callback: RequestCallback) -> InAppProductPurchaseRequest {
+        let request = InAppProductPurchaseRequest(product: nil, callback: callback)
+        request.startRestorePurchases()
+        return request
+    }
+    
+    // MARK: Private methods
+    private func startPayment(product: SKProduct) {
+        let payment = SKMutablePayment(product: product)
+        dispatch_async(dispatch_get_global_queue(0, 0), {
+            self.paymentQueue.addPayment(payment)
+        })
+    }
+    private func startRestorePurchases() {
+        
+        dispatch_async(dispatch_get_global_queue(0, 0), {
+            self.paymentQueue.restoreCompletedTransactions()
+        })
+    }
+    
+    // MARK: SKPaymentTransactionObserver
+    public func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
+        
+        for transaction in transactions {
+            switch transaction.transactionState {
+            case .Purchased:
+                dispatch_async(dispatch_get_main_queue(), {
+                    self.callback(result: ResultType.Purchased(productId: transaction.payment.productIdentifier))
+                })
+                paymentQueue.finishTransaction(transaction)
+                break
+            case .Failed:
+                dispatch_async(dispatch_get_main_queue(), {
+                    // It appears that in some edge cases transaction.error is nil here. Since returning an associated error is
+                    // mandatory, return a default one if needed
+                    let altError = NSError(domain: SKErrorDomain, code: 0, userInfo: [ NSLocalizedDescriptionKey: "Unknown error" ])
+                    self.callback(result: ResultType.Failed(error: transaction.error ?? altError))
+                })
+                paymentQueue.finishTransaction(transaction)
+                break
+            case .Restored:
+                dispatch_async(dispatch_get_main_queue(), {
+                    self.callback(result: ResultType.Restored(productId: transaction.payment.productIdentifier))
+                })
+                paymentQueue.finishTransaction(transaction)
+                break
+            case .Purchasing:
+                // In progress: do nothing
+                break
+            case .Deferred:
+                break
+            }
+            // Keep track of payments
+            if let _ = purchases[transaction.transactionState] {
+                purchases[transaction.transactionState]?.append(transaction.payment.productIdentifier)
+            }
+            else {
+                purchases[transaction.transactionState] = [ transaction.payment.productIdentifier ]
+            }
+        }
+    }
+    
+    public func paymentQueue(queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
+        
+    }
+    
+    public func paymentQueue(queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: NSError) {
+        
+        dispatch_async(dispatch_get_main_queue(), {
+            self.callback(result: ResultType.Failed(error: error))
+        })
+    }
+
+    public func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
+        
+        if let product = self.product {
+            self.callback(result: ResultType.Restored(productId: product.productIdentifier))
+            return
+        }
+        // This method will be called after all purchases have been restored (includes the case of no purchases)
+        guard let restored = purchases[.Restored] where restored.count > 0 else {
+            
+            self.callback(result: ResultType.NothingToRestore)
+            return
+        }
+        //print("\(restored)")
+    }
+    
+    public func paymentQueue(queue: SKPaymentQueue, updatedDownloads downloads: [SKDownload]) {
+        
+    }
+}
+

+ 86 - 0
SwiftyStoreKit/InAppProductQueryRequest.swift

@@ -0,0 +1,86 @@
+//
+//  InAppPurchaseProductRequest.swift
+//  WordShooter
+//
+//  Created by Andrea Bizzotto on 01/09/2015.
+//  Copyright © 2015 musevisions. All rights reserved.
+//
+
+import UIKit
+import StoreKit
+
+public enum ResponseError : ErrorType {
+    case InvalidProducts(invalidProductIdentifiers: [String])
+    case NoProducts
+    case RequestFailed(error: NSError)
+}
+public class InAppProductQueryRequest: NSObject, SKProductsRequestDelegate {
+
+    enum ResultType {
+        case Success(products: [SKProduct])
+        case Error(e: ResponseError)
+    }
+
+    typealias RequestCallback = (result: ResultType) -> ()
+    private let callback: RequestCallback
+    private let request: SKProductsRequest
+    // http://stackoverflow.com/questions/24011575/what-is-the-difference-between-a-weak-reference-and-an-unowned-reference
+    deinit {
+        request.delegate = nil
+    }
+    private init(productIds: Set<String>, callback: RequestCallback) {
+        
+        self.callback = callback
+        request = SKProductsRequest(productIdentifiers: productIds)
+        super.init()
+        request.delegate = self
+    }
+    
+    class func startQuery(productIds: Set<String>, callback: RequestCallback) -> InAppProductQueryRequest {
+        let request = InAppProductQueryRequest(productIds: productIds, callback: callback)
+        request.start()
+        return request
+    }
+
+    public func start() {
+        dispatch_async(dispatch_get_global_queue(0, 0), {
+            self.request.start()
+        })
+    }
+    public func cancel() {
+        dispatch_async(dispatch_get_global_queue(0, 0), {
+            self.request.cancel()
+        })
+    }
+    
+    // MARK: SKProductsRequestDelegate
+    public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
+        
+        if response.invalidProductIdentifiers.count > 0 {
+            let error = ResponseError.InvalidProducts(invalidProductIdentifiers: response.invalidProductIdentifiers)
+            dispatch_async(dispatch_get_main_queue(), {
+                self.callback(result: .Error(e: error))
+            })
+            return
+        }
+        if response.products.count == 0 {
+            let error = ResponseError.NoProducts
+            dispatch_async(dispatch_get_main_queue(), {
+                self.callback(result: .Error(e: error))
+            })
+            return
+        }
+        callback(result: .Success(products: response.products))
+    }
+    
+    public func requestDidFinish(request: SKRequest) {
+        
+    }
+    public func request(request: SKRequest, didFailWithError error: NSError) {
+        
+        let error = ResponseError.RequestFailed(error: error)
+        dispatch_async(dispatch_get_main_queue(), {
+            self.callback(result: .Error(e: error))
+        })
+    }
+}

+ 140 - 0
SwiftyStoreKit/SwiftyStore.swift

@@ -0,0 +1,140 @@
+//
+//  InAppPurchasesHelper.swift
+//  WordShooter
+//
+//  Created by Andrea Bizzotto on 01/09/2015.
+//  Copyright © 2015 musevisions. All rights reserved.
+//
+
+import UIKit
+import Foundation
+import StoreKit
+
+
+public class SwiftyStore {
+
+    // MARK: Private declarations
+    private class InAppPurchaseStore {
+        var products: [String: SKProduct] = [:]
+        func addProduct(product: SKProduct) {
+            products[product.productIdentifier] = product
+        }
+    }
+    private var store: InAppPurchaseStore = InAppPurchaseStore()
+
+    // As we can have multiple inflight queries and purchases, we store them in a dictionary by product id
+    private var inflightQueries: [String: InAppProductQueryRequest] = [:]
+    private var inflightPurchases: [String: InAppProductPurchaseRequest] = [:]
+    private var restoreRequest: InAppProductPurchaseRequest?
+
+    // MARK: Enums
+    public enum PurchaseResultType {
+        case Success(productId: String)
+        case Error(error: ErrorType)
+    }
+    public enum RestoreResultType {
+        case Success(productId: String)
+        case Error(error: ErrorType)
+        case NothingToRestore
+    }
+
+    // MARK: Singleton
+    static let sharedInstance = SwiftyStore()
+    
+    var canMakePayments: Bool {
+        return SKPaymentQueue.canMakePayments()
+    }
+    
+    // MARK: Public methods
+    public func purchaseProduct(productId: String, completion: (result: PurchaseResultType) -> ()) {
+        
+        guard let product = store.products[productId] else {
+
+            requestProduct(productId) { (inner: () throws -> SKProduct) -> () in
+                do {
+                    let product = try inner()
+                    self.purchase(product: product, completion: completion)
+                }
+                catch let error {
+                    completion(result: .Error(error: error))
+                }
+            }
+            return
+        }
+        purchase(product: product, completion: completion)
+    }
+    
+    public func restorePurchases(completion: (result: RestoreResultType) -> ()) {
+
+        restoreRequest = InAppProductPurchaseRequest.restorePurchases() { result in
+        
+            self.restoreRequest = nil
+            let returnValue = self.processRestoreResult(result)
+            completion(result: returnValue)
+        }
+    }
+
+    // MARK: private methods
+    private func purchase(product product: SKProduct, completion: (result: PurchaseResultType) -> ()) {
+    
+        inflightPurchases[product.productIdentifier] = InAppProductPurchaseRequest.startPayment(product) { result in
+
+            self.inflightPurchases[product.productIdentifier] = nil
+            let returnValue = self.processPurchaseResult(result)
+            completion(result: returnValue)
+        }
+    }
+
+    private func processPurchaseResult(result: InAppProductPurchaseRequest.ResultType) -> PurchaseResultType {
+        switch result {
+        case .Purchased(let productId):
+            // TODO: Need a way to match with current product?
+            return .Success(productId: productId)
+        case .Failed(let error):
+            return .Error(error: error)
+        case .Restored(_):
+            fatalError("case Restored is not allowed for purchase flow")
+        case .NothingToRestore:
+            fatalError("case NothingToRestore is not allowed for purchase flow")
+        }
+    }
+    
+    private func processRestoreResult(result: InAppProductPurchaseRequest.ResultType) -> RestoreResultType {
+        switch result {
+        case .Purchased(_):
+            fatalError("case Purchased is not allowed for restore flow")
+        case .Failed(let error):
+            return .Error(error: error)
+        case .Restored(let productId):
+            return .Success(productId: productId)
+        case .NothingToRestore:
+            return .NothingToRestore
+        }
+    }
+    
+    // http://appventure.me/2015/06/19/swift-try-catch-asynchronous-closures/
+    private func requestProduct(productId: String, completion: (result: (() throws -> SKProduct)) -> ()) -> () {
+        
+        inflightQueries[productId] = InAppProductQueryRequest.startQuery([productId]) { result in
+        
+            self.inflightQueries[productId] = nil
+            if case .Success(let products) = result {
+                
+                // Add to Store
+                for product in products {
+                    //print("Received product with ID: \(product.productIdentifier)")
+                    self.store.addProduct(product)
+                }
+                guard let product = self.store.products[productId] else {
+                    completion(result: { throw ResponseError.NoProducts })
+                    return
+                }
+                completion(result: { return product })
+            }
+            else if case .Error(let error) = result {
+                
+                completion(result: { throw error })
+            }
+        }
+    }
+}

+ 0 - 25
SwiftyStoreKit/ViewController.swift

@@ -1,25 +0,0 @@
-//
-//  ViewController.swift
-//  SwiftyStoreKit
-//
-//  Created by Andrea Bizzotto on 03/09/2015.
-//  Copyright © 2015 musevisions. All rights reserved.
-//
-
-import UIKit
-
-class ViewController: UIViewController {
-
-    override func viewDidLoad() {
-        super.viewDidLoad()
-        // Do any additional setup after loading the view, typically from a nib.
-    }
-
-    override func didReceiveMemoryWarning() {
-        super.didReceiveMemoryWarning()
-        // Dispose of any resources that can be recreated.
-    }
-
-
-}
-