├── LICENSE ├── README.md ├── StoreFrontExampleApp ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── header.imageset │ │ ├── Contents.json │ │ └── header.jpg ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Configuration.storekit ├── Constants.swift ├── Info.plist ├── SceneDelegate.swift └── ViewController.swift ├── StoreFrontKit.podspec ├── StoreFrontKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── StoreFrontExampleApp 1.xcscheme │ │ ├── StoreFrontExampleApp.xcscheme │ │ └── StoreFrontKit.xcscheme └── xcuserdata │ └── afrazsiddiqui.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── StoreFrontKit ├── Info.plist ├── Managers │ ├── SFKManager.swift │ └── SKFIAPManager.swift ├── Models │ ├── StoreFrontKitConfiguration.swift │ ├── StoreFrontProduct.swift │ └── StoreFrontProductViewModel.swift ├── StoreFrontKit.h ├── UserInterface │ ├── SFKMultiNonConsumableViewController.swift │ ├── SFKNonConsumableViewController.swift │ ├── SFKSubscriptionGroupViewController.swift │ ├── SFKSubscriptionTrialViewController.swift │ └── StoreFrontKitDisplayable.swift └── Views │ └── SFKProductTableViewCell.swift ├── StoreFrontKitTests ├── Info.plist └── StoreFrontKitTests.swift ├── header.png └── store_front_kit.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Afraz Siddiqui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StoreFrontKit 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/StoreFrontKit.svg?style=flat)](https://cocoapods.org/pods/StoreFrontKit) 4 | [![License](https://img.shields.io/cocoapods/l/StoreFrontKit.svg?style=flat)](https://cocoapods.org/pods/StoreFrontKit) 5 | [![Platform](https://img.shields.io/cocoapods/p/StoreFrontKit.svg?style=flat)](https://cocoapods.org/pods/StoreFrontKit) 6 | 7 | ![StoreFrontKit Logo](https://raw.githubusercontent.com/AfrazCodes/StoreFrontKit/master/store_front_kit.png) 8 | 9 | ![StoreFrontKit Examples](https://raw.githubusercontent.com/AfrazCodes/StoreFrontKit/master/header.png) 10 | 11 | ## Introduction 12 | 13 | Apple's StoreKit framework provides concise and clean APIs to interface with the App Store for in app purchases. However, a notable omission is a UI component to present in app purchases to users. The reasoning behind this is most likely that every app looks and feels different. 14 | 15 | However, often times, we find ourselves writing a "Store Front" type component over and over again. This UI encompasses information about the product, subscription, or service we are selling. Moreover, it gives the user a call to action to purchase, restore, or subscribe. 16 | 17 | StoreFrontKit is a fully managed and lightweight framework to solve this. The framework accomplishes the following: 18 | - Fully managed app store product fetching and caching 19 | - Transaction management and purchase restorations 20 | - Single item Store Front 21 | - Multi item store Front 22 | - Free trial subscription store front up sells 23 | - Subscription group store front 24 | 25 | ## Installation 26 | 27 | ### CocoaPods 28 | 29 | Include the following line in your `Podfile` 30 | 31 | `pod 'StoreFrontKit'` 32 | 33 | ### Manual 34 | 35 | You may download or clone this repo and use the source directly. 36 | 37 | ## Usage 38 | 39 | There are 2 steps to use `StoreFrontKit` 40 | 41 | ### Configure at App Launch 42 | 43 | First, you need to configure StoreFrontKit with the in app purchase product identifiers who want to manage in your app. The requirement is that this is done prior to using the storefront UIs. It is recommended you do this in your App Delegate as follows. 44 | 45 | ```swift 46 | @main 47 | class AppDelegate: UIResponder, UIApplicationDelegate { 48 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 49 | // Set Up Store Front 50 | SFKManager.shared.configure( 51 | with: StoreFrontKitConfiguration( 52 | products: [ 53 | .nonConsumable( 54 | productID: "com.example.item", viewModel: nil 55 | ), 56 | .subscription( 57 | productID: "com.example.subscription", viewModel: nil 58 | ), 59 | ] 60 | ) 61 | ) 62 | return true 63 | } 64 | } 65 | ``` 66 | 67 | ### Show Store Front 68 | 69 | Creating a Store Front is as simple as creating and pushing a view controller. 70 | 71 | ```swift 72 | let vc = SFKNonConsumableViewController( 73 | with: .nonConsumable( 74 | productID: Products.removeAds.rawValue, 75 | viewModel: StoreFrontProductViewModel( 76 | icon: UIImage(systemName: "x.square"), 77 | iconTintColor: .systemPink 78 | ) 79 | ) 80 | ) { result in 81 | switch result { 82 | case .success: break 83 | case .failure: break 84 | } 85 | } 86 | vc.title = "Remove Ads" 87 | vc.navigationItem.largeTitleDisplayMode = .always 88 | self?.navigationController?.pushViewController(vc, animated: true) 89 | ``` 90 | 91 | In the above example, we create a single item (non-consumable) store front VC and push it. You'll also note that it takes a callback completion block. This block relays successful in app purchase transactions. 92 | 93 | ### More information 94 | 95 | Check out the example app target included in this repo. 96 | 97 | ## Contributing 98 | 99 | Contribution to this framework is not just welcomed, it is encouraged! Feel free to open Pull Requests or issues for feature requests and bug fixes. The ask is to follow a few rules: 100 | - Build generic components that are not just specific to you 101 | - Follow clean architecture principles 102 | - Write testable code and related tests 103 | 104 | ## License 105 | 106 | Distributed under MIT License 107 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // StoreFrontExampleApp 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import StoreFrontKit 9 | import UIKit 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | // Set Up Store Front 15 | SFKManager.shared.configure( 16 | with: StoreFrontKitConfiguration( 17 | products: [ 18 | .nonConsumable( 19 | productID: Products.removeAds.rawValue, viewModel: nil 20 | ), 21 | .nonConsumable( 22 | productID: Products.coins.rawValue, viewModel: nil 23 | ), 24 | .nonConsumable( 25 | productID: Products.premium.rawValue, viewModel: nil 26 | ), 27 | .nonConsumable( 28 | productID: Products.stickets.rawValue, viewModel: nil 29 | ), 30 | .subscription( 31 | productID: Products.subscriptionQuarterly.rawValue, viewModel: nil 32 | ), 33 | .subscription( 34 | productID: Products.subscriptionMonthly.rawValue, viewModel: nil 35 | ), 36 | .subscription( 37 | productID: Products.subscriptionYearly.rawValue, viewModel: nil 38 | ) 39 | ] 40 | ) 41 | ) 42 | return true 43 | } 44 | 45 | // MARK: UISceneSession Lifecycle 46 | 47 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 48 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 49 | } 50 | 51 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {} 52 | } 53 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/Assets.xcassets/header.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "header.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/Assets.xcassets/header.imageset/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AfrazCodes/StoreFrontKit/9b7897289e264c15ddb7027d8b70e09c5e184ed0/StoreFrontExampleApp/Assets.xcassets/header.imageset/header.jpg -------------------------------------------------------------------------------- /StoreFrontExampleApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/Configuration.storekit: -------------------------------------------------------------------------------- 1 | { 2 | "products" : [ 3 | { 4 | "displayPrice" : "1.99", 5 | "familyShareable" : false, 6 | "internalID" : "A88589E8", 7 | "localizations" : [ 8 | { 9 | "description" : "Remove all banner and interstitial ads in app", 10 | "displayName" : "Remove Ads", 11 | "locale" : "en_US" 12 | } 13 | ], 14 | "productID" : "com.example.removeAds", 15 | "referenceName" : "Remove Ads", 16 | "type" : "NonConsumable" 17 | }, 18 | { 19 | "displayPrice" : "5.99", 20 | "familyShareable" : false, 21 | "internalID" : "D5B65743", 22 | "localizations" : [ 23 | { 24 | "description" : "Premium upgrade and unlock features.", 25 | "displayName" : "Premium", 26 | "locale" : "en_US" 27 | } 28 | ], 29 | "productID" : "com.example.premium", 30 | "referenceName" : "Premium", 31 | "type" : "NonConsumable" 32 | }, 33 | { 34 | "displayPrice" : "2.99", 35 | "familyShareable" : false, 36 | "internalID" : "85089459", 37 | "localizations" : [ 38 | { 39 | "description" : "Get all stickers", 40 | "displayName" : "Stickers", 41 | "locale" : "en_US" 42 | } 43 | ], 44 | "productID" : "com.example.stickers", 45 | "referenceName" : "Stickers", 46 | "type" : "NonConsumable" 47 | }, 48 | { 49 | "displayPrice" : "0.99", 50 | "familyShareable" : false, 51 | "internalID" : "6AC0EAB5", 52 | "localizations" : [ 53 | { 54 | "description" : "Get bag of 50 coins", 55 | "displayName" : "50 Coins", 56 | "locale" : "en_US" 57 | } 58 | ], 59 | "productID" : "com.example.coins", 60 | "referenceName" : "50 Coins", 61 | "type" : "NonConsumable" 62 | }, 63 | { 64 | "displayPrice" : "29.99", 65 | "familyShareable" : false, 66 | "internalID" : "C47FDB2F", 67 | "localizations" : [ 68 | { 69 | "description" : "Yearly subscription", 70 | "displayName" : "Yearly Plan", 71 | "locale" : "en_US" 72 | } 73 | ], 74 | "productID" : "com.example.subscription.annual", 75 | "referenceName" : "Yearly", 76 | "type" : "NonConsumable" 77 | } 78 | ], 79 | "settings" : { 80 | 81 | }, 82 | "subscriptionGroups" : [ 83 | { 84 | "id" : "5AB2AE7B", 85 | "localizations" : [ 86 | 87 | ], 88 | "name" : "Subscription Group", 89 | "subscriptions" : [ 90 | { 91 | "adHocOffers" : [ 92 | 93 | ], 94 | "displayPrice" : "4.99", 95 | "familyShareable" : false, 96 | "groupNumber" : 1, 97 | "internalID" : "BC10FBC6", 98 | "introductoryOffer" : null, 99 | "localizations" : [ 100 | { 101 | "description" : "Monthly subscription", 102 | "displayName" : "Monthly", 103 | "locale" : "en_US" 104 | } 105 | ], 106 | "productID" : "com.example.subscription.monthly", 107 | "recurringSubscriptionPeriod" : "P1M", 108 | "referenceName" : "Monthly", 109 | "subscriptionGroupID" : "5AB2AE7B", 110 | "type" : "RecurringSubscription" 111 | }, 112 | { 113 | "adHocOffers" : [ 114 | 115 | ], 116 | "displayPrice" : "9.99", 117 | "familyShareable" : false, 118 | "groupNumber" : 1, 119 | "internalID" : "3BE28464", 120 | "introductoryOffer" : null, 121 | "localizations" : [ 122 | { 123 | "description" : "3 month subscription", 124 | "displayName" : "Quarterly Subscription", 125 | "locale" : "en_US" 126 | } 127 | ], 128 | "productID" : "com.example.subscription.quarterly", 129 | "recurringSubscriptionPeriod" : "P1M", 130 | "referenceName" : "Quarterly", 131 | "subscriptionGroupID" : "5AB2AE7B", 132 | "type" : "RecurringSubscription" 133 | } 134 | ] 135 | } 136 | ], 137 | "version" : { 138 | "major" : 1, 139 | "minor" : 0 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // StoreFrontExampleApp 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Products: String, CaseIterable { 11 | case removeAds = "com.example.removeAds" 12 | case premium = "com.example.premium" 13 | case stickets = "com.example.stickers" 14 | case coins = "com.example.coins" 15 | case subscriptionMonthly = "com.example.subscription.monthly" 16 | case subscriptionQuarterly = "com.example.subscription.quarterly" 17 | case subscriptionYearly = "com.example.subscription.annual" 18 | } 19 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | SFK Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | UISceneStoryboardFile 39 | Main 40 | 41 | 42 | 43 | 44 | UIApplicationSupportsIndirectInputEvents 45 | 46 | UILaunchStoryboardName 47 | LaunchScreen 48 | UIMainStoryboardFile 49 | Main 50 | UIRequiredDeviceCapabilities 51 | 52 | armv7 53 | 54 | UISupportedInterfaceOrientations 55 | 56 | UIInterfaceOrientationPortrait 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // StoreFrontExampleApp 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 16 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 17 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 18 | guard let _ = (scene as? UIWindowScene) else { return } 19 | } 20 | 21 | func sceneDidDisconnect(_ scene: UIScene) { 22 | // Called as the scene is being released by the system. 23 | // This occurs shortly after the scene enters the background, or when its session is discarded. 24 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 25 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 26 | } 27 | 28 | func sceneDidBecomeActive(_ scene: UIScene) { 29 | // Called when the scene has moved from an inactive state to an active state. 30 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 31 | } 32 | 33 | func sceneWillResignActive(_ scene: UIScene) { 34 | // Called when the scene will move from an active state to an inactive state. 35 | // This may occur due to temporary interruptions (ex. an incoming phone call). 36 | } 37 | 38 | func sceneWillEnterForeground(_ scene: UIScene) { 39 | // Called as the scene transitions from the background to the foreground. 40 | // Use this method to undo the changes made on entering the background. 41 | } 42 | 43 | func sceneDidEnterBackground(_ scene: UIScene) { 44 | // Called as the scene transitions from the foreground to the background. 45 | // Use this method to save data, release shared resources, and store enough scene-specific state information 46 | // to restore the scene back to its current state. 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /StoreFrontExampleApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // StoreFrontExampleApp 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import StoreFrontKit 9 | import UIKit 10 | 11 | final class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 12 | /// Table view subview 13 | private let tableView: UITableView = { 14 | let table = UITableView(frame: .zero, style: .grouped) 15 | table.register( 16 | UITableViewCell.self, 17 | forCellReuseIdentifier: "cell" 18 | ) 19 | return table 20 | }() 21 | 22 | struct Section { 23 | let title: String 24 | let options: [Option] 25 | } 26 | 27 | struct Option { 28 | let title: String 29 | let handler: (() -> Void) 30 | } 31 | 32 | private var models = [Section]() 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | configureModels() 37 | title = "StoreFrontKit" 38 | view.backgroundColor = .systemBackground 39 | tableView.delegate = self 40 | tableView.dataSource = self 41 | view.addSubview(tableView) 42 | } 43 | 44 | override func viewDidLayoutSubviews() { 45 | super.viewDidLayoutSubviews() 46 | tableView.frame = view.bounds 47 | } 48 | 49 | private func configureModels() { 50 | models.append(Section(title: "Standard", options: [ 51 | Option(title: "Single Iteme", handler: { [weak self] in 52 | DispatchQueue.main.async { 53 | let vc = SFKNonConsumableViewController( 54 | with: .nonConsumable( 55 | productID: Products.removeAds.rawValue, 56 | viewModel: StoreFrontProductViewModel( 57 | icon: UIImage(systemName: "x.square"), 58 | iconTintColor: .systemPink 59 | ) 60 | ) 61 | ) { result in 62 | switch result { 63 | case .success: break 64 | case .failure: break 65 | } 66 | } 67 | vc.title = "Remove Ads" 68 | vc.navigationItem.largeTitleDisplayMode = .always 69 | self?.navigationController?.pushViewController(vc, animated: true) 70 | } 71 | }), 72 | Option(title: "Multiple Iteme", handler: { [weak self] in 73 | DispatchQueue.main.async { 74 | guard let strongSelf = self else { return } 75 | let header = UIView(frame: CGRect(x: 0, y: 0, width: strongSelf.view.frame.size.width, height: strongSelf.view.frame.size.width)) 76 | header.clipsToBounds = true 77 | let imageView = UIImageView(frame: header.bounds) 78 | header.addSubview(imageView) 79 | imageView.image = UIImage(named: "header") 80 | let vc = SFKMultiNonConsumableViewController( 81 | with: [ 82 | .nonConsumable( 83 | productID: Products.premium.rawValue, 84 | viewModel: StoreFrontProductViewModel( 85 | icon: UIImage(systemName: "crown"), 86 | iconTintColor: .systemPurple 87 | ) 88 | ), 89 | .nonConsumable( 90 | productID: Products.stickets.rawValue, 91 | viewModel: StoreFrontProductViewModel( 92 | icon: UIImage(systemName: "star"), 93 | iconTintColor: .systemGreen 94 | ) 95 | ), 96 | .nonConsumable( 97 | productID: Products.removeAds.rawValue, 98 | viewModel: StoreFrontProductViewModel( 99 | icon: UIImage(systemName: "x.square"), 100 | iconTintColor: .systemPink 101 | ) 102 | ), 103 | .nonConsumable( 104 | productID: Products.coins.rawValue, 105 | viewModel: StoreFrontProductViewModel( 106 | icon: UIImage(systemName: "bitcoinsign.circle.fill"), 107 | iconTintColor: .systemOrange 108 | ) 109 | ) 110 | ], 111 | header: header 112 | ) { result in 113 | switch result { 114 | case .success: break 115 | case .failure: break 116 | } 117 | } 118 | vc.title = "Upgrade" 119 | vc.navigationItem.largeTitleDisplayMode = .never 120 | self?.navigationController?.pushViewController(vc, animated: true) 121 | } 122 | }) 123 | ])) 124 | 125 | models.append(Section(title: "Subscriptions", options: [ 126 | Option(title: "Free Trial", handler: { [weak self] in 127 | DispatchQueue.main.async { 128 | let vc = SFKSubscriptionTrialViewController( 129 | with: .subscription( 130 | productID: Products.subscriptionMonthly.rawValue, 131 | viewModel: StoreFrontProductViewModel( 132 | icon: UIImage(systemName: "x.square"), 133 | iconTintColor: .systemPink 134 | ) 135 | ) 136 | ) { result in 137 | switch result { 138 | case .success: break 139 | case .failure: break 140 | } 141 | } 142 | vc.title = "Remove Ads" 143 | vc.navigationItem.largeTitleDisplayMode = .always 144 | self?.navigationController?.pushViewController(vc, animated: true) 145 | } 146 | }), 147 | Option(title: "Subscription Group", handler: { [weak self] in 148 | DispatchQueue.main.async { 149 | guard let strongSelf = self else { return } 150 | let header = UIView(frame: CGRect(x: 0, y: 0, width: strongSelf.view.frame.size.width, height: strongSelf.view.frame.size.width)) 151 | header.clipsToBounds = true 152 | let imageView = UIImageView(frame: header.bounds) 153 | header.addSubview(imageView) 154 | imageView.image = UIImage(named: "header") 155 | let vc = SFKMultiNonConsumableViewController( 156 | with: [ 157 | .subscription( 158 | productID: Products.subscriptionYearly.rawValue, 159 | viewModel: StoreFrontProductViewModel( 160 | icon: UIImage(systemName: "x.square"), 161 | iconTintColor: .systemPink 162 | ) 163 | ), 164 | .subscription( 165 | productID: Products.subscriptionMonthly.rawValue, 166 | viewModel: StoreFrontProductViewModel( 167 | icon: UIImage(systemName: "x.square"), 168 | iconTintColor: .systemPink 169 | ) 170 | ), 171 | .subscription( 172 | productID: Products.subscriptionQuarterly.rawValue, 173 | viewModel: StoreFrontProductViewModel( 174 | icon: UIImage(systemName: "x.square"), 175 | iconTintColor: .systemPink 176 | ) 177 | ) 178 | ], 179 | header: header 180 | ) { result in 181 | switch result { 182 | case .success: break 183 | case .failure: break 184 | } 185 | } 186 | vc.title = "Upgrade" 187 | vc.navigationItem.largeTitleDisplayMode = .never 188 | self?.navigationController?.pushViewController(vc, animated: true) 189 | } 190 | }) 191 | ])) 192 | } 193 | 194 | func numberOfSections(in tableView: UITableView) -> Int { 195 | return models.count 196 | } 197 | 198 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 199 | return models[section].options.count 200 | } 201 | 202 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 203 | let model = models[indexPath.section].options[indexPath.row] 204 | let cell = tableView.dequeueReusableCell( 205 | withIdentifier: "cell", 206 | for: indexPath 207 | ) 208 | cell.textLabel?.text = model.title 209 | cell.accessoryType = .disclosureIndicator 210 | return cell 211 | } 212 | 213 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 214 | tableView.deselectRow(at: indexPath, animated: true) 215 | let model = models[indexPath.section].options[indexPath.row] 216 | model.handler() 217 | } 218 | 219 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 220 | return models[section].title 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /StoreFrontKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'StoreFrontKit' 3 | s.version = '1.0.0' 4 | s.summary = 'Lightweight and flexible presentation kit for in app purchases.' 5 | s.description = 'Lightweight and customizable in app purchase presentation framework.' 6 | s.homepage = 'https://github.com/AfrazCodes/StoreFrontKit' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { 'AfrazCodes' => 'afraz9@gmail.com' } 9 | s.source = { :git => 'https://github.com/AfrazCodes/StoreFrontKit.git', :tag => s.version.to_s } 10 | s.ios.deployment_target = '13.0' 11 | s.source_files = 'StoreFrontKit/**/*.{swift}' 12 | s.swift_versions = '5.0' 13 | end 14 | -------------------------------------------------------------------------------- /StoreFrontKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8383A03725867AA3008188A2 /* StoreFrontKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8383A02D25867AA3008188A2 /* StoreFrontKit.framework */; }; 11 | 8383A03C25867AA3008188A2 /* StoreFrontKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A03B25867AA3008188A2 /* StoreFrontKitTests.swift */; }; 12 | 8383A03E25867AA3008188A2 /* StoreFrontKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 8383A03025867AA3008188A2 /* StoreFrontKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | 8383A05125867AD2008188A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A05025867AD2008188A2 /* AppDelegate.swift */; }; 14 | 8383A05325867AD2008188A2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A05225867AD2008188A2 /* SceneDelegate.swift */; }; 15 | 8383A05525867AD2008188A2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A05425867AD2008188A2 /* ViewController.swift */; }; 16 | 8383A05825867AD2008188A2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8383A05625867AD2008188A2 /* Main.storyboard */; }; 17 | 8383A05A25867AD3008188A2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8383A05925867AD3008188A2 /* Assets.xcassets */; }; 18 | 8383A05D25867AD3008188A2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8383A05B25867AD3008188A2 /* LaunchScreen.storyboard */; }; 19 | 8383A06C25867B1A008188A2 /* SFKManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A06B25867B1A008188A2 /* SFKManager.swift */; }; 20 | 8383A07725867C29008188A2 /* SFKNonConsumableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A07625867C29008188A2 /* SFKNonConsumableViewController.swift */; }; 21 | 8383A07F25867C39008188A2 /* SFKMultiNonConsumableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A07E25867C39008188A2 /* SFKMultiNonConsumableViewController.swift */; }; 22 | 8383A08425867C4E008188A2 /* SFKSubscriptionTrialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A08325867C4E008188A2 /* SFKSubscriptionTrialViewController.swift */; }; 23 | 8383A08925867C5D008188A2 /* SFKSubscriptionGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A08825867C5D008188A2 /* SFKSubscriptionGroupViewController.swift */; }; 24 | 8383A09425867C86008188A2 /* StoreFrontKitDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A09325867C86008188A2 /* StoreFrontKitDisplayable.swift */; }; 25 | 8383A09925867D59008188A2 /* StoreFrontProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A09825867D59008188A2 /* StoreFrontProduct.swift */; }; 26 | 8383A09E25867DC9008188A2 /* StoreFrontKitConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A09D25867DC9008188A2 /* StoreFrontKitConfiguration.swift */; }; 27 | 8383A0A325867EB5008188A2 /* SKFIAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A0A225867EB5008188A2 /* SKFIAPManager.swift */; }; 28 | 8383A0A825868566008188A2 /* StoreFrontProductViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A0A725868566008188A2 /* StoreFrontProductViewModel.swift */; }; 29 | 8383A0B1258688EA008188A2 /* SFKProductTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A0B0258688EA008188A2 /* SFKProductTableViewCell.swift */; }; 30 | 8383A0BD2586AA0C008188A2 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8383A0BC2586AA0C008188A2 /* Constants.swift */; }; 31 | 8383A0C52586AB89008188A2 /* StoreFrontKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8383A02D25867AA3008188A2 /* StoreFrontKit.framework */; }; 32 | 8383A0C62586AB89008188A2 /* StoreFrontKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8383A02D25867AA3008188A2 /* StoreFrontKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXContainerItemProxy section */ 36 | 8383A03825867AA3008188A2 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 8383A02425867AA3008188A2 /* Project object */; 39 | proxyType = 1; 40 | remoteGlobalIDString = 8383A02C25867AA3008188A2; 41 | remoteInfo = StoreFrontKit; 42 | }; 43 | 8383A0C72586AB89008188A2 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 8383A02425867AA3008188A2 /* Project object */; 46 | proxyType = 1; 47 | remoteGlobalIDString = 8383A02C25867AA3008188A2; 48 | remoteInfo = StoreFrontKit; 49 | }; 50 | /* End PBXContainerItemProxy section */ 51 | 52 | /* Begin PBXCopyFilesBuildPhase section */ 53 | 8383A0C92586AB8A008188A2 /* Embed Frameworks */ = { 54 | isa = PBXCopyFilesBuildPhase; 55 | buildActionMask = 2147483647; 56 | dstPath = ""; 57 | dstSubfolderSpec = 10; 58 | files = ( 59 | 8383A0C62586AB89008188A2 /* StoreFrontKit.framework in Embed Frameworks */, 60 | ); 61 | name = "Embed Frameworks"; 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXCopyFilesBuildPhase section */ 65 | 66 | /* Begin PBXFileReference section */ 67 | 8383A02D25867AA3008188A2 /* StoreFrontKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StoreFrontKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 8383A03025867AA3008188A2 /* StoreFrontKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StoreFrontKit.h; sourceTree = ""; }; 69 | 8383A03125867AA3008188A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70 | 8383A03625867AA3008188A2 /* StoreFrontKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StoreFrontKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 71 | 8383A03B25867AA3008188A2 /* StoreFrontKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreFrontKitTests.swift; sourceTree = ""; }; 72 | 8383A03D25867AA3008188A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | 8383A04E25867AD2008188A2 /* StoreFrontExampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StoreFrontExampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 74 | 8383A05025867AD2008188A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 75 | 8383A05225867AD2008188A2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 76 | 8383A05425867AD2008188A2 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 77 | 8383A05725867AD2008188A2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 78 | 8383A05925867AD3008188A2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 79 | 8383A05C25867AD3008188A2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 80 | 8383A05E25867AD3008188A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 81 | 8383A06B25867B1A008188A2 /* SFKManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFKManager.swift; sourceTree = ""; }; 82 | 8383A07625867C29008188A2 /* SFKNonConsumableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFKNonConsumableViewController.swift; sourceTree = ""; }; 83 | 8383A07E25867C39008188A2 /* SFKMultiNonConsumableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFKMultiNonConsumableViewController.swift; sourceTree = ""; }; 84 | 8383A08325867C4E008188A2 /* SFKSubscriptionTrialViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFKSubscriptionTrialViewController.swift; sourceTree = ""; }; 85 | 8383A08825867C5D008188A2 /* SFKSubscriptionGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFKSubscriptionGroupViewController.swift; sourceTree = ""; }; 86 | 8383A09325867C86008188A2 /* StoreFrontKitDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreFrontKitDisplayable.swift; sourceTree = ""; }; 87 | 8383A09825867D59008188A2 /* StoreFrontProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreFrontProduct.swift; sourceTree = ""; }; 88 | 8383A09D25867DC9008188A2 /* StoreFrontKitConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreFrontKitConfiguration.swift; sourceTree = ""; }; 89 | 8383A0A225867EB5008188A2 /* SKFIAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKFIAPManager.swift; sourceTree = ""; }; 90 | 8383A0A725868566008188A2 /* StoreFrontProductViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreFrontProductViewModel.swift; sourceTree = ""; }; 91 | 8383A0B0258688EA008188A2 /* SFKProductTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFKProductTableViewCell.swift; sourceTree = ""; }; 92 | 8383A0BB2586A9EA008188A2 /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = ""; }; 93 | 8383A0BC2586AA0C008188A2 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 94 | /* End PBXFileReference section */ 95 | 96 | /* Begin PBXFrameworksBuildPhase section */ 97 | 8383A02A25867AA3008188A2 /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | 8383A03325867AA3008188A2 /* Frameworks */ = { 105 | isa = PBXFrameworksBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | 8383A03725867AA3008188A2 /* StoreFrontKit.framework in Frameworks */, 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | 8383A04B25867AD2008188A2 /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | 8383A0C52586AB89008188A2 /* StoreFrontKit.framework in Frameworks */, 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | /* End PBXFrameworksBuildPhase section */ 121 | 122 | /* Begin PBXGroup section */ 123 | 8383A02325867AA3008188A2 = { 124 | isa = PBXGroup; 125 | children = ( 126 | 8383A02F25867AA3008188A2 /* StoreFrontKit */, 127 | 8383A03A25867AA3008188A2 /* StoreFrontKitTests */, 128 | 8383A04F25867AD2008188A2 /* StoreFrontExampleApp */, 129 | 8383A02E25867AA3008188A2 /* Products */, 130 | 8383A0C42586AB89008188A2 /* Frameworks */, 131 | ); 132 | sourceTree = ""; 133 | }; 134 | 8383A02E25867AA3008188A2 /* Products */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 8383A02D25867AA3008188A2 /* StoreFrontKit.framework */, 138 | 8383A03625867AA3008188A2 /* StoreFrontKitTests.xctest */, 139 | 8383A04E25867AD2008188A2 /* StoreFrontExampleApp.app */, 140 | ); 141 | name = Products; 142 | sourceTree = ""; 143 | }; 144 | 8383A02F25867AA3008188A2 /* StoreFrontKit */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 8383A03025867AA3008188A2 /* StoreFrontKit.h */, 148 | 8383A07125867BF2008188A2 /* Managers */, 149 | 8383A0AF258688DB008188A2 /* Views */, 150 | 8383A07025867BEC008188A2 /* Models */, 151 | 8383A07525867C03008188A2 /* UserInterface */, 152 | 8383A03125867AA3008188A2 /* Info.plist */, 153 | ); 154 | path = StoreFrontKit; 155 | sourceTree = ""; 156 | }; 157 | 8383A03A25867AA3008188A2 /* StoreFrontKitTests */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 8383A03B25867AA3008188A2 /* StoreFrontKitTests.swift */, 161 | 8383A03D25867AA3008188A2 /* Info.plist */, 162 | ); 163 | path = StoreFrontKitTests; 164 | sourceTree = ""; 165 | }; 166 | 8383A04F25867AD2008188A2 /* StoreFrontExampleApp */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 8383A05025867AD2008188A2 /* AppDelegate.swift */, 170 | 8383A05225867AD2008188A2 /* SceneDelegate.swift */, 171 | 8383A05425867AD2008188A2 /* ViewController.swift */, 172 | 8383A05625867AD2008188A2 /* Main.storyboard */, 173 | 8383A05925867AD3008188A2 /* Assets.xcassets */, 174 | 8383A05B25867AD3008188A2 /* LaunchScreen.storyboard */, 175 | 8383A05E25867AD3008188A2 /* Info.plist */, 176 | 8383A0BB2586A9EA008188A2 /* Configuration.storekit */, 177 | 8383A0BC2586AA0C008188A2 /* Constants.swift */, 178 | ); 179 | path = StoreFrontExampleApp; 180 | sourceTree = ""; 181 | }; 182 | 8383A07025867BEC008188A2 /* Models */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | 8383A09825867D59008188A2 /* StoreFrontProduct.swift */, 186 | 8383A09D25867DC9008188A2 /* StoreFrontKitConfiguration.swift */, 187 | 8383A0A725868566008188A2 /* StoreFrontProductViewModel.swift */, 188 | ); 189 | path = Models; 190 | sourceTree = ""; 191 | }; 192 | 8383A07125867BF2008188A2 /* Managers */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 8383A06B25867B1A008188A2 /* SFKManager.swift */, 196 | 8383A0A225867EB5008188A2 /* SKFIAPManager.swift */, 197 | ); 198 | path = Managers; 199 | sourceTree = ""; 200 | }; 201 | 8383A07525867C03008188A2 /* UserInterface */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 8383A07625867C29008188A2 /* SFKNonConsumableViewController.swift */, 205 | 8383A07E25867C39008188A2 /* SFKMultiNonConsumableViewController.swift */, 206 | 8383A08325867C4E008188A2 /* SFKSubscriptionTrialViewController.swift */, 207 | 8383A08825867C5D008188A2 /* SFKSubscriptionGroupViewController.swift */, 208 | 8383A09325867C86008188A2 /* StoreFrontKitDisplayable.swift */, 209 | ); 210 | path = UserInterface; 211 | sourceTree = ""; 212 | }; 213 | 8383A0AF258688DB008188A2 /* Views */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | 8383A0B0258688EA008188A2 /* SFKProductTableViewCell.swift */, 217 | ); 218 | path = Views; 219 | sourceTree = ""; 220 | }; 221 | 8383A0C42586AB89008188A2 /* Frameworks */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | ); 225 | name = Frameworks; 226 | sourceTree = ""; 227 | }; 228 | /* End PBXGroup section */ 229 | 230 | /* Begin PBXHeadersBuildPhase section */ 231 | 8383A02825867AA3008188A2 /* Headers */ = { 232 | isa = PBXHeadersBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | 8383A03E25867AA3008188A2 /* StoreFrontKit.h in Headers */, 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | /* End PBXHeadersBuildPhase section */ 240 | 241 | /* Begin PBXNativeTarget section */ 242 | 8383A02C25867AA3008188A2 /* StoreFrontKit */ = { 243 | isa = PBXNativeTarget; 244 | buildConfigurationList = 8383A04125867AA3008188A2 /* Build configuration list for PBXNativeTarget "StoreFrontKit" */; 245 | buildPhases = ( 246 | 8383A02825867AA3008188A2 /* Headers */, 247 | 8383A02925867AA3008188A2 /* Sources */, 248 | 8383A02A25867AA3008188A2 /* Frameworks */, 249 | 8383A02B25867AA3008188A2 /* Resources */, 250 | ); 251 | buildRules = ( 252 | ); 253 | dependencies = ( 254 | ); 255 | name = StoreFrontKit; 256 | productName = StoreFrontKit; 257 | productReference = 8383A02D25867AA3008188A2 /* StoreFrontKit.framework */; 258 | productType = "com.apple.product-type.framework"; 259 | }; 260 | 8383A03525867AA3008188A2 /* StoreFrontKitTests */ = { 261 | isa = PBXNativeTarget; 262 | buildConfigurationList = 8383A04425867AA3008188A2 /* Build configuration list for PBXNativeTarget "StoreFrontKitTests" */; 263 | buildPhases = ( 264 | 8383A03225867AA3008188A2 /* Sources */, 265 | 8383A03325867AA3008188A2 /* Frameworks */, 266 | 8383A03425867AA3008188A2 /* Resources */, 267 | ); 268 | buildRules = ( 269 | ); 270 | dependencies = ( 271 | 8383A03925867AA3008188A2 /* PBXTargetDependency */, 272 | ); 273 | name = StoreFrontKitTests; 274 | productName = StoreFrontKitTests; 275 | productReference = 8383A03625867AA3008188A2 /* StoreFrontKitTests.xctest */; 276 | productType = "com.apple.product-type.bundle.unit-test"; 277 | }; 278 | 8383A04D25867AD2008188A2 /* StoreFrontExampleApp */ = { 279 | isa = PBXNativeTarget; 280 | buildConfigurationList = 8383A05F25867AD3008188A2 /* Build configuration list for PBXNativeTarget "StoreFrontExampleApp" */; 281 | buildPhases = ( 282 | 8383A04A25867AD2008188A2 /* Sources */, 283 | 8383A04B25867AD2008188A2 /* Frameworks */, 284 | 8383A04C25867AD2008188A2 /* Resources */, 285 | 8383A0C92586AB8A008188A2 /* Embed Frameworks */, 286 | ); 287 | buildRules = ( 288 | ); 289 | dependencies = ( 290 | 8383A0C82586AB89008188A2 /* PBXTargetDependency */, 291 | ); 292 | name = StoreFrontExampleApp; 293 | productName = StoreFrontExampleApp; 294 | productReference = 8383A04E25867AD2008188A2 /* StoreFrontExampleApp.app */; 295 | productType = "com.apple.product-type.application"; 296 | }; 297 | /* End PBXNativeTarget section */ 298 | 299 | /* Begin PBXProject section */ 300 | 8383A02425867AA3008188A2 /* Project object */ = { 301 | isa = PBXProject; 302 | attributes = { 303 | LastSwiftUpdateCheck = 1220; 304 | LastUpgradeCheck = 1220; 305 | TargetAttributes = { 306 | 8383A02C25867AA3008188A2 = { 307 | CreatedOnToolsVersion = 12.2; 308 | LastSwiftMigration = 1220; 309 | }; 310 | 8383A03525867AA3008188A2 = { 311 | CreatedOnToolsVersion = 12.2; 312 | }; 313 | 8383A04D25867AD2008188A2 = { 314 | CreatedOnToolsVersion = 12.2; 315 | }; 316 | }; 317 | }; 318 | buildConfigurationList = 8383A02725867AA3008188A2 /* Build configuration list for PBXProject "StoreFrontKit" */; 319 | compatibilityVersion = "Xcode 9.3"; 320 | developmentRegion = en; 321 | hasScannedForEncodings = 0; 322 | knownRegions = ( 323 | en, 324 | Base, 325 | ); 326 | mainGroup = 8383A02325867AA3008188A2; 327 | productRefGroup = 8383A02E25867AA3008188A2 /* Products */; 328 | projectDirPath = ""; 329 | projectRoot = ""; 330 | targets = ( 331 | 8383A02C25867AA3008188A2 /* StoreFrontKit */, 332 | 8383A03525867AA3008188A2 /* StoreFrontKitTests */, 333 | 8383A04D25867AD2008188A2 /* StoreFrontExampleApp */, 334 | ); 335 | }; 336 | /* End PBXProject section */ 337 | 338 | /* Begin PBXResourcesBuildPhase section */ 339 | 8383A02B25867AA3008188A2 /* Resources */ = { 340 | isa = PBXResourcesBuildPhase; 341 | buildActionMask = 2147483647; 342 | files = ( 343 | ); 344 | runOnlyForDeploymentPostprocessing = 0; 345 | }; 346 | 8383A03425867AA3008188A2 /* Resources */ = { 347 | isa = PBXResourcesBuildPhase; 348 | buildActionMask = 2147483647; 349 | files = ( 350 | ); 351 | runOnlyForDeploymentPostprocessing = 0; 352 | }; 353 | 8383A04C25867AD2008188A2 /* Resources */ = { 354 | isa = PBXResourcesBuildPhase; 355 | buildActionMask = 2147483647; 356 | files = ( 357 | 8383A05D25867AD3008188A2 /* LaunchScreen.storyboard in Resources */, 358 | 8383A05A25867AD3008188A2 /* Assets.xcassets in Resources */, 359 | 8383A05825867AD2008188A2 /* Main.storyboard in Resources */, 360 | ); 361 | runOnlyForDeploymentPostprocessing = 0; 362 | }; 363 | /* End PBXResourcesBuildPhase section */ 364 | 365 | /* Begin PBXSourcesBuildPhase section */ 366 | 8383A02925867AA3008188A2 /* Sources */ = { 367 | isa = PBXSourcesBuildPhase; 368 | buildActionMask = 2147483647; 369 | files = ( 370 | 8383A0A325867EB5008188A2 /* SKFIAPManager.swift in Sources */, 371 | 8383A07725867C29008188A2 /* SFKNonConsumableViewController.swift in Sources */, 372 | 8383A0B1258688EA008188A2 /* SFKProductTableViewCell.swift in Sources */, 373 | 8383A09425867C86008188A2 /* StoreFrontKitDisplayable.swift in Sources */, 374 | 8383A09E25867DC9008188A2 /* StoreFrontKitConfiguration.swift in Sources */, 375 | 8383A09925867D59008188A2 /* StoreFrontProduct.swift in Sources */, 376 | 8383A07F25867C39008188A2 /* SFKMultiNonConsumableViewController.swift in Sources */, 377 | 8383A0A825868566008188A2 /* StoreFrontProductViewModel.swift in Sources */, 378 | 8383A06C25867B1A008188A2 /* SFKManager.swift in Sources */, 379 | 8383A08925867C5D008188A2 /* SFKSubscriptionGroupViewController.swift in Sources */, 380 | 8383A08425867C4E008188A2 /* SFKSubscriptionTrialViewController.swift in Sources */, 381 | ); 382 | runOnlyForDeploymentPostprocessing = 0; 383 | }; 384 | 8383A03225867AA3008188A2 /* Sources */ = { 385 | isa = PBXSourcesBuildPhase; 386 | buildActionMask = 2147483647; 387 | files = ( 388 | 8383A03C25867AA3008188A2 /* StoreFrontKitTests.swift in Sources */, 389 | ); 390 | runOnlyForDeploymentPostprocessing = 0; 391 | }; 392 | 8383A04A25867AD2008188A2 /* Sources */ = { 393 | isa = PBXSourcesBuildPhase; 394 | buildActionMask = 2147483647; 395 | files = ( 396 | 8383A05525867AD2008188A2 /* ViewController.swift in Sources */, 397 | 8383A05125867AD2008188A2 /* AppDelegate.swift in Sources */, 398 | 8383A05325867AD2008188A2 /* SceneDelegate.swift in Sources */, 399 | 8383A0BD2586AA0C008188A2 /* Constants.swift in Sources */, 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | /* End PBXSourcesBuildPhase section */ 404 | 405 | /* Begin PBXTargetDependency section */ 406 | 8383A03925867AA3008188A2 /* PBXTargetDependency */ = { 407 | isa = PBXTargetDependency; 408 | target = 8383A02C25867AA3008188A2 /* StoreFrontKit */; 409 | targetProxy = 8383A03825867AA3008188A2 /* PBXContainerItemProxy */; 410 | }; 411 | 8383A0C82586AB89008188A2 /* PBXTargetDependency */ = { 412 | isa = PBXTargetDependency; 413 | target = 8383A02C25867AA3008188A2 /* StoreFrontKit */; 414 | targetProxy = 8383A0C72586AB89008188A2 /* PBXContainerItemProxy */; 415 | }; 416 | /* End PBXTargetDependency section */ 417 | 418 | /* Begin PBXVariantGroup section */ 419 | 8383A05625867AD2008188A2 /* Main.storyboard */ = { 420 | isa = PBXVariantGroup; 421 | children = ( 422 | 8383A05725867AD2008188A2 /* Base */, 423 | ); 424 | name = Main.storyboard; 425 | sourceTree = ""; 426 | }; 427 | 8383A05B25867AD3008188A2 /* LaunchScreen.storyboard */ = { 428 | isa = PBXVariantGroup; 429 | children = ( 430 | 8383A05C25867AD3008188A2 /* Base */, 431 | ); 432 | name = LaunchScreen.storyboard; 433 | sourceTree = ""; 434 | }; 435 | /* End PBXVariantGroup section */ 436 | 437 | /* Begin XCBuildConfiguration section */ 438 | 8383A03F25867AA3008188A2 /* Debug */ = { 439 | isa = XCBuildConfiguration; 440 | buildSettings = { 441 | ALWAYS_SEARCH_USER_PATHS = NO; 442 | CLANG_ANALYZER_NONNULL = YES; 443 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 444 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 445 | CLANG_CXX_LIBRARY = "libc++"; 446 | CLANG_ENABLE_MODULES = YES; 447 | CLANG_ENABLE_OBJC_ARC = YES; 448 | CLANG_ENABLE_OBJC_WEAK = YES; 449 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 450 | CLANG_WARN_BOOL_CONVERSION = YES; 451 | CLANG_WARN_COMMA = YES; 452 | CLANG_WARN_CONSTANT_CONVERSION = YES; 453 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 454 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 455 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 456 | CLANG_WARN_EMPTY_BODY = YES; 457 | CLANG_WARN_ENUM_CONVERSION = YES; 458 | CLANG_WARN_INFINITE_RECURSION = YES; 459 | CLANG_WARN_INT_CONVERSION = YES; 460 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 461 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 462 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 463 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 464 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 465 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 466 | CLANG_WARN_STRICT_PROTOTYPES = YES; 467 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 468 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 469 | CLANG_WARN_UNREACHABLE_CODE = YES; 470 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 471 | COPY_PHASE_STRIP = NO; 472 | CURRENT_PROJECT_VERSION = 1; 473 | DEBUG_INFORMATION_FORMAT = dwarf; 474 | ENABLE_STRICT_OBJC_MSGSEND = YES; 475 | ENABLE_TESTABILITY = YES; 476 | GCC_C_LANGUAGE_STANDARD = gnu11; 477 | GCC_DYNAMIC_NO_PIC = NO; 478 | GCC_NO_COMMON_BLOCKS = YES; 479 | GCC_OPTIMIZATION_LEVEL = 0; 480 | GCC_PREPROCESSOR_DEFINITIONS = ( 481 | "DEBUG=1", 482 | "$(inherited)", 483 | ); 484 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 485 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 486 | GCC_WARN_UNDECLARED_SELECTOR = YES; 487 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 488 | GCC_WARN_UNUSED_FUNCTION = YES; 489 | GCC_WARN_UNUSED_VARIABLE = YES; 490 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 491 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 492 | MTL_FAST_MATH = YES; 493 | ONLY_ACTIVE_ARCH = YES; 494 | SDKROOT = iphoneos; 495 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 496 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 497 | VERSIONING_SYSTEM = "apple-generic"; 498 | VERSION_INFO_PREFIX = ""; 499 | }; 500 | name = Debug; 501 | }; 502 | 8383A04025867AA3008188A2 /* Release */ = { 503 | isa = XCBuildConfiguration; 504 | buildSettings = { 505 | ALWAYS_SEARCH_USER_PATHS = NO; 506 | CLANG_ANALYZER_NONNULL = YES; 507 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 508 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 509 | CLANG_CXX_LIBRARY = "libc++"; 510 | CLANG_ENABLE_MODULES = YES; 511 | CLANG_ENABLE_OBJC_ARC = YES; 512 | CLANG_ENABLE_OBJC_WEAK = YES; 513 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 514 | CLANG_WARN_BOOL_CONVERSION = YES; 515 | CLANG_WARN_COMMA = YES; 516 | CLANG_WARN_CONSTANT_CONVERSION = YES; 517 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 518 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 519 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 520 | CLANG_WARN_EMPTY_BODY = YES; 521 | CLANG_WARN_ENUM_CONVERSION = YES; 522 | CLANG_WARN_INFINITE_RECURSION = YES; 523 | CLANG_WARN_INT_CONVERSION = YES; 524 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 525 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 526 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 527 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 528 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 529 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 530 | CLANG_WARN_STRICT_PROTOTYPES = YES; 531 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 532 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 533 | CLANG_WARN_UNREACHABLE_CODE = YES; 534 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 535 | COPY_PHASE_STRIP = NO; 536 | CURRENT_PROJECT_VERSION = 1; 537 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 538 | ENABLE_NS_ASSERTIONS = NO; 539 | ENABLE_STRICT_OBJC_MSGSEND = YES; 540 | GCC_C_LANGUAGE_STANDARD = gnu11; 541 | GCC_NO_COMMON_BLOCKS = YES; 542 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 543 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 544 | GCC_WARN_UNDECLARED_SELECTOR = YES; 545 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 546 | GCC_WARN_UNUSED_FUNCTION = YES; 547 | GCC_WARN_UNUSED_VARIABLE = YES; 548 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 549 | MTL_ENABLE_DEBUG_INFO = NO; 550 | MTL_FAST_MATH = YES; 551 | SDKROOT = iphoneos; 552 | SWIFT_COMPILATION_MODE = wholemodule; 553 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 554 | VALIDATE_PRODUCT = YES; 555 | VERSIONING_SYSTEM = "apple-generic"; 556 | VERSION_INFO_PREFIX = ""; 557 | }; 558 | name = Release; 559 | }; 560 | 8383A04225867AA3008188A2 /* Debug */ = { 561 | isa = XCBuildConfiguration; 562 | buildSettings = { 563 | CLANG_ENABLE_MODULES = YES; 564 | CODE_SIGN_STYLE = Automatic; 565 | DEFINES_MODULE = YES; 566 | DEVELOPMENT_TEAM = 3798Z36XLV; 567 | DYLIB_COMPATIBILITY_VERSION = 1; 568 | DYLIB_CURRENT_VERSION = 1; 569 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 570 | INFOPLIST_FILE = StoreFrontKit/Info.plist; 571 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 572 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 573 | LD_RUNPATH_SEARCH_PATHS = ( 574 | "$(inherited)", 575 | "@executable_path/Frameworks", 576 | "@loader_path/Frameworks", 577 | ); 578 | PRODUCT_BUNDLE_IDENTIFIER = com.asndigital.StoreFrontKit; 579 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 580 | SKIP_INSTALL = YES; 581 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 582 | SWIFT_VERSION = 5.0; 583 | TARGETED_DEVICE_FAMILY = "1,2"; 584 | }; 585 | name = Debug; 586 | }; 587 | 8383A04325867AA3008188A2 /* Release */ = { 588 | isa = XCBuildConfiguration; 589 | buildSettings = { 590 | CLANG_ENABLE_MODULES = YES; 591 | CODE_SIGN_STYLE = Automatic; 592 | DEFINES_MODULE = YES; 593 | DEVELOPMENT_TEAM = 3798Z36XLV; 594 | DYLIB_COMPATIBILITY_VERSION = 1; 595 | DYLIB_CURRENT_VERSION = 1; 596 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 597 | INFOPLIST_FILE = StoreFrontKit/Info.plist; 598 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 599 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 600 | LD_RUNPATH_SEARCH_PATHS = ( 601 | "$(inherited)", 602 | "@executable_path/Frameworks", 603 | "@loader_path/Frameworks", 604 | ); 605 | PRODUCT_BUNDLE_IDENTIFIER = com.asndigital.StoreFrontKit; 606 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 607 | SKIP_INSTALL = YES; 608 | SWIFT_VERSION = 5.0; 609 | TARGETED_DEVICE_FAMILY = "1,2"; 610 | }; 611 | name = Release; 612 | }; 613 | 8383A04525867AA3008188A2 /* Debug */ = { 614 | isa = XCBuildConfiguration; 615 | buildSettings = { 616 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 617 | CODE_SIGN_STYLE = Automatic; 618 | DEVELOPMENT_TEAM = 3798Z36XLV; 619 | INFOPLIST_FILE = StoreFrontKitTests/Info.plist; 620 | LD_RUNPATH_SEARCH_PATHS = ( 621 | "$(inherited)", 622 | "@executable_path/Frameworks", 623 | "@loader_path/Frameworks", 624 | ); 625 | PRODUCT_BUNDLE_IDENTIFIER = com.asndigital.StoreFrontKitTests; 626 | PRODUCT_NAME = "$(TARGET_NAME)"; 627 | SWIFT_VERSION = 5.0; 628 | TARGETED_DEVICE_FAMILY = "1,2"; 629 | }; 630 | name = Debug; 631 | }; 632 | 8383A04625867AA3008188A2 /* Release */ = { 633 | isa = XCBuildConfiguration; 634 | buildSettings = { 635 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 636 | CODE_SIGN_STYLE = Automatic; 637 | DEVELOPMENT_TEAM = 3798Z36XLV; 638 | INFOPLIST_FILE = StoreFrontKitTests/Info.plist; 639 | LD_RUNPATH_SEARCH_PATHS = ( 640 | "$(inherited)", 641 | "@executable_path/Frameworks", 642 | "@loader_path/Frameworks", 643 | ); 644 | PRODUCT_BUNDLE_IDENTIFIER = com.asndigital.StoreFrontKitTests; 645 | PRODUCT_NAME = "$(TARGET_NAME)"; 646 | SWIFT_VERSION = 5.0; 647 | TARGETED_DEVICE_FAMILY = "1,2"; 648 | }; 649 | name = Release; 650 | }; 651 | 8383A06025867AD3008188A2 /* Debug */ = { 652 | isa = XCBuildConfiguration; 653 | buildSettings = { 654 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 655 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 656 | CODE_SIGN_STYLE = Automatic; 657 | DEVELOPMENT_TEAM = 3798Z36XLV; 658 | INFOPLIST_FILE = StoreFrontExampleApp/Info.plist; 659 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 660 | LD_RUNPATH_SEARCH_PATHS = ( 661 | "$(inherited)", 662 | "@executable_path/Frameworks", 663 | ); 664 | PRODUCT_BUNDLE_IDENTIFIER = com.asndigital.StoreFrontExampleApp; 665 | PRODUCT_NAME = "$(TARGET_NAME)"; 666 | SWIFT_VERSION = 5.0; 667 | TARGETED_DEVICE_FAMILY = "1,2"; 668 | }; 669 | name = Debug; 670 | }; 671 | 8383A06125867AD3008188A2 /* Release */ = { 672 | isa = XCBuildConfiguration; 673 | buildSettings = { 674 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 675 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 676 | CODE_SIGN_STYLE = Automatic; 677 | DEVELOPMENT_TEAM = 3798Z36XLV; 678 | INFOPLIST_FILE = StoreFrontExampleApp/Info.plist; 679 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 680 | LD_RUNPATH_SEARCH_PATHS = ( 681 | "$(inherited)", 682 | "@executable_path/Frameworks", 683 | ); 684 | PRODUCT_BUNDLE_IDENTIFIER = com.asndigital.StoreFrontExampleApp; 685 | PRODUCT_NAME = "$(TARGET_NAME)"; 686 | SWIFT_VERSION = 5.0; 687 | TARGETED_DEVICE_FAMILY = "1,2"; 688 | }; 689 | name = Release; 690 | }; 691 | /* End XCBuildConfiguration section */ 692 | 693 | /* Begin XCConfigurationList section */ 694 | 8383A02725867AA3008188A2 /* Build configuration list for PBXProject "StoreFrontKit" */ = { 695 | isa = XCConfigurationList; 696 | buildConfigurations = ( 697 | 8383A03F25867AA3008188A2 /* Debug */, 698 | 8383A04025867AA3008188A2 /* Release */, 699 | ); 700 | defaultConfigurationIsVisible = 0; 701 | defaultConfigurationName = Release; 702 | }; 703 | 8383A04125867AA3008188A2 /* Build configuration list for PBXNativeTarget "StoreFrontKit" */ = { 704 | isa = XCConfigurationList; 705 | buildConfigurations = ( 706 | 8383A04225867AA3008188A2 /* Debug */, 707 | 8383A04325867AA3008188A2 /* Release */, 708 | ); 709 | defaultConfigurationIsVisible = 0; 710 | defaultConfigurationName = Release; 711 | }; 712 | 8383A04425867AA3008188A2 /* Build configuration list for PBXNativeTarget "StoreFrontKitTests" */ = { 713 | isa = XCConfigurationList; 714 | buildConfigurations = ( 715 | 8383A04525867AA3008188A2 /* Debug */, 716 | 8383A04625867AA3008188A2 /* Release */, 717 | ); 718 | defaultConfigurationIsVisible = 0; 719 | defaultConfigurationName = Release; 720 | }; 721 | 8383A05F25867AD3008188A2 /* Build configuration list for PBXNativeTarget "StoreFrontExampleApp" */ = { 722 | isa = XCConfigurationList; 723 | buildConfigurations = ( 724 | 8383A06025867AD3008188A2 /* Debug */, 725 | 8383A06125867AD3008188A2 /* Release */, 726 | ); 727 | defaultConfigurationIsVisible = 0; 728 | defaultConfigurationName = Release; 729 | }; 730 | /* End XCConfigurationList section */ 731 | }; 732 | rootObject = 8383A02425867AA3008188A2 /* Project object */; 733 | } 734 | -------------------------------------------------------------------------------- /StoreFrontKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /StoreFrontKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /StoreFrontKit.xcodeproj/xcshareddata/xcschemes/StoreFrontExampleApp 1.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 55 | 56 | 57 | 63 | 65 | 71 | 72 | 73 | 74 | 76 | 77 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /StoreFrontKit.xcodeproj/xcshareddata/xcschemes/StoreFrontExampleApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /StoreFrontKit.xcodeproj/xcshareddata/xcschemes/StoreFrontKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /StoreFrontKit.xcodeproj/xcuserdata/afrazsiddiqui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /StoreFrontKit.xcodeproj/xcuserdata/afrazsiddiqui.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | StoreFrontExampleApp 1.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | StoreFrontExampleApp.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | StoreFrontKit.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 0 21 | 22 | 23 | SuppressBuildableAutocreation 24 | 25 | 8383A02C25867AA3008188A2 26 | 27 | primary 28 | 29 | 30 | 8383A03525867AA3008188A2 31 | 32 | primary 33 | 34 | 35 | 8383A04D25867AD2008188A2 36 | 37 | primary 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /StoreFrontKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | StoreFrontKit 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | 24 | 25 | -------------------------------------------------------------------------------- /StoreFrontKit/Managers/SFKManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SFKManager.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// StoreFrontKit Manager responsible for global set up and management 11 | public final class SFKManager { 12 | /// Shared instance 13 | public static let shared = SFKManager() 14 | 15 | /// Represents current configuraiton 16 | var configuration: StoreFrontKitConfiguration = StoreFrontKitConfiguration(products: []) 17 | 18 | /// Privatized constructor 19 | private init() {} 20 | 21 | /// Configure store front at app launch 22 | /// - Parameter configuration: Store front configuration 23 | public func configure(with configuration: StoreFrontKitConfiguration) { 24 | self.configuration = configuration 25 | fetchProductsFromAppStore() 26 | } 27 | 28 | // MARK: - Private 29 | 30 | /// Fetch store kit products from Apple 31 | private func fetchProductsFromAppStore() { 32 | SFKIAPManager.shared.fetchProductsFromAppStore() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /StoreFrontKit/Managers/SKFIAPManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKFIAPManager.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import Foundation 9 | import StoreKit 10 | 11 | /// Represents In App Purchase Manager 12 | public final class SFKIAPManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver { 13 | /// Shared Instance 14 | static let shared = SFKIAPManager() 15 | 16 | /// Represents fetched products from Apple 17 | var appStoreProducts = Set() 18 | 19 | /// Represents closure to be executed once app store products are ready 20 | private var whenReadyHandlers = [(() -> Void)]() 21 | 22 | /// Represents type for purchase completion callback handler 23 | public typealias SFKPurchaseCompletion = ((Result) -> Void) 24 | 25 | /// Represents purchase completion handler 26 | private var completion: SFKPurchaseCompletion? 27 | 28 | /// Represents current product being transacted 29 | private var transactingProduct: StoreFrontProduct? 30 | 31 | /// Represents StoreFront Kti transaction errors 32 | public enum SFKTransactionError: Error { 33 | /// Represents product resolution error 34 | case missingProduct 35 | /// Represents transaction failures in queue 36 | case transactionFailed 37 | /// Represents error for users who do not have a valid apple id set up 38 | case cannotMakePurchases 39 | /// Represents unknown error 40 | case unknown 41 | } 42 | 43 | // MARK: - Init 44 | 45 | /// Constructor override 46 | override init() { 47 | super.init() 48 | // Add transaction observer 49 | SKPaymentQueue.default().add(self) 50 | } 51 | 52 | /// Fetch store kit products from Apple 53 | /// Note: In dev environment please set up local store kit configuration 54 | func fetchProductsFromAppStore() { 55 | let request = SKProductsRequest(productIdentifiers: Set(SFKManager.shared.configuration.products.compactMap({ $0.identifier }))) 56 | request.delegate = self 57 | request.start() 58 | } 59 | 60 | func purchase(product: StoreFrontProduct, completion: @escaping SFKPurchaseCompletion) { 61 | guard SKPaymentQueue.canMakePayments() else { 62 | completion(.failure(SFKTransactionError.cannotMakePurchases)) 63 | return 64 | } 65 | 66 | guard let appStoreProduct = appStoreProducts.first(where: { $0.productIdentifier == product.identifier }) else { 67 | completion(.failure(SFKTransactionError.missingProduct)) 68 | return 69 | } 70 | 71 | self.transactingProduct = product 72 | self.completion = completion 73 | 74 | whenAppStoreProductsReady { 75 | DispatchQueue.main.async { 76 | let payment = SKPayment(product: appStoreProduct) 77 | SKPaymentQueue.default().add(payment) 78 | } 79 | } 80 | } 81 | 82 | func restorePurchases() { 83 | SKPaymentQueue.default().restoreCompletedTransactions() 84 | } 85 | 86 | // MARK: - Private 87 | 88 | private func whenAppStoreProductsReady(handler: @escaping (() -> Void)) { 89 | guard appStoreProducts.isEmpty else { 90 | handler() 91 | return 92 | } 93 | whenReadyHandlers.append(handler) 94 | } 95 | 96 | // MARK: - Store Kit Interface 97 | 98 | public func request(_ request: SKRequest, didFailWithError error: Error) { 99 | guard request is SKProductsRequest else { 100 | return 101 | } 102 | appStoreProducts = [] 103 | } 104 | 105 | public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { 106 | appStoreProducts = Set(response.products) 107 | } 108 | 109 | public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { 110 | transactions.forEach { 111 | switch $0.transactionState { 112 | case .purchasing: 113 | break 114 | case .purchased: 115 | if let currentTransactingProduct = transactingProduct, 116 | currentTransactingProduct.identifier == $0.payment.productIdentifier { 117 | completion?(.success($0.payment.productIdentifier)) 118 | } 119 | SKPaymentQueue.default().finishTransaction($0) 120 | case .failed: 121 | completion?(.failure(SFKTransactionError.transactionFailed)) 122 | SKPaymentQueue.default().finishTransaction($0) 123 | case .restored: 124 | SKPaymentQueue.default().finishTransaction($0) 125 | case .deferred: 126 | SKPaymentQueue.default().finishTransaction($0) 127 | @unknown default: 128 | break 129 | } 130 | } 131 | } 132 | 133 | public func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool { 134 | return true 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /StoreFrontKit/Models/StoreFrontKitConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreFrontKitConfiguration.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents store front configuration 11 | public struct StoreFrontKitConfiguration { 12 | /// Represents products to register with store front 13 | public let products: Set 14 | 15 | public init(products: Set) { 16 | self.products = products 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /StoreFrontKit/Models/StoreFrontProduct.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreFrontProduct.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents different types of Store Kit Product 11 | public enum StoreFrontProduct: Hashable { 12 | /// Represents subscription 13 | case subscription(productID: String, viewModel: StoreFrontProductViewModel?) 14 | /// Represents single time purchase item 15 | case nonConsumable(productID: String, viewModel: StoreFrontProductViewModel?) 16 | /// Represents item that can be purchased multiple times; Ex: Digital coins in game 17 | case consumable(productID: String, viewModel: StoreFrontProductViewModel?) 18 | 19 | /// Hasher for set usage 20 | /// - Parameter hasher: Hasher object to compute item hash value 21 | public func hash(into hasher: inout Hasher) { 22 | hasher.combine(identifier) 23 | } 24 | 25 | /// Equatability of products 26 | public static func == (lhs: StoreFrontProduct, rhs: StoreFrontProduct) -> Bool { 27 | return lhs.identifier == rhs.identifier 28 | } 29 | 30 | /// StoreKit product identifier 31 | var identifier: String { 32 | switch self { 33 | case .subscription(let productID, _): return productID 34 | case .consumable(let productID, _): return productID 35 | case .nonConsumable(let productID, _): return productID 36 | } 37 | } 38 | 39 | /// Represents optional product viewModel 40 | var viewModel: StoreFrontProductViewModel? { 41 | switch self { 42 | case .subscription(_, let viewModel): return viewModel 43 | case .consumable(_, let viewModel): return viewModel 44 | case .nonConsumable(_, let viewModel): return viewModel 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /StoreFrontKit/Models/StoreFrontProductViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreFrontProductViewModel.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | /// Represents view model for given product 12 | public struct StoreFrontProductViewModel { 13 | let icon: UIImage? 14 | let iconTintColor: UIColor 15 | 16 | /// Constructor 17 | /// - Parameters: 18 | /// - icon: icon for product 19 | /// - iconTintColor: icon tint 20 | public init(icon: UIImage?, iconTintColor: UIColor) { 21 | self.icon = icon 22 | self.iconTintColor = iconTintColor 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /StoreFrontKit/StoreFrontKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // StoreFrontKit.h 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | #import 9 | 10 | FOUNDATION_EXPORT double StoreFrontKitVersionNumber; 11 | FOUNDATION_EXPORT const unsigned char StoreFrontKitVersionString[]; 12 | -------------------------------------------------------------------------------- /StoreFrontKit/UserInterface/SFKMultiNonConsumableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SFKMultiConsumableViewController.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Store front for multiple products 11 | public final class SFKMultiNonConsumableViewController: UIViewController, StoreFrontKitDisplayable, UITableViewDelegate, UITableViewDataSource { 12 | /// Represents products 13 | public var sfkProduct: [StoreFrontProduct] 14 | 15 | /// Purchase completion 16 | public var completion: SFKIAPManager.SFKPurchaseCompletion 17 | 18 | /// Represents list table subview 19 | private let tableView: UITableView = { 20 | let tableView = UITableView() 21 | tableView.register( 22 | SFKProductTableViewCell.self, 23 | forCellReuseIdentifier: SFKProductTableViewCell.identifier 24 | ) 25 | tableView.register( 26 | UITableViewCell.self, 27 | forCellReuseIdentifier: "cell" 28 | ) 29 | return tableView 30 | }() 31 | 32 | // MARK: - Init 33 | 34 | public required init(with product: StoreFrontProduct, completion: @escaping SFKIAPManager.SFKPurchaseCompletion) { 35 | sfkProduct = [product] 36 | self.completion = completion 37 | super.init(nibName: nil, bundle: nil) 38 | } 39 | 40 | public init(with products: [StoreFrontProduct], header: UIView? = nil, footer: UIView? = nil, completion: @escaping SFKIAPManager.SFKPurchaseCompletion) { 41 | self.sfkProduct = products 42 | self.completion = completion 43 | super.init(nibName: nil, bundle: nil) 44 | tableView.tableHeaderView = header 45 | tableView.tableFooterView = footer 46 | } 47 | 48 | required init?(coder: NSCoder) { 49 | fatalError() 50 | } 51 | 52 | // MARK: - Lifecycle 53 | 54 | public override func viewDidLoad() { 55 | super.viewDidLoad() 56 | view.backgroundColor = .systemBackground 57 | view.addSubview(tableView) 58 | tableView.delegate = self 59 | tableView.dataSource = self 60 | } 61 | 62 | public override func viewDidLayoutSubviews() { 63 | super.viewDidLayoutSubviews() 64 | tableView.frame = view.bounds 65 | } 66 | 67 | // MARK: - Table 68 | 69 | public func numberOfSections(in tableView: UITableView) -> Int { 70 | return 1 71 | } 72 | 73 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 74 | // Products + restore purchases cell 75 | return sfkProduct.count + 1 76 | } 77 | 78 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 79 | if indexPath.row == sfkProduct.count { 80 | // Restore cell 81 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 82 | cell.textLabel?.text = "Restore Purchases" 83 | cell.textLabel?.textAlignment = .center 84 | cell.textLabel?.textColor = .link 85 | return cell 86 | } 87 | let model = sfkProduct[indexPath.row] 88 | 89 | guard let appStoreProduct = SFKIAPManager.shared.appStoreProducts.first(where: { 90 | $0.productIdentifier == model.identifier 91 | }) else { 92 | fatalError("Failed to find app store product; Double check that you have registered this with SFK Configuration") 93 | } 94 | 95 | guard let cell = tableView.dequeueReusableCell( 96 | withIdentifier: SFKProductTableViewCell.identifier, 97 | for: indexPath 98 | ) as? SFKProductTableViewCell else { 99 | fatalError() 100 | } 101 | 102 | cell.configure( 103 | with: SFKProductTableViewCell.ViewModel( 104 | title: appStoreProduct.localizedTitle, 105 | price: "\(appStoreProduct.priceLocale.currencySymbol ?? "$")\(appStoreProduct.price)", 106 | description: appStoreProduct.localizedDescription, 107 | icon: model.viewModel?.icon, 108 | iconTint: model.viewModel?.iconTintColor 109 | ), 110 | index: indexPath.row 111 | ) 112 | cell.delegate = self 113 | return cell 114 | } 115 | 116 | // MARK: - StoreFront 117 | 118 | public func purchase(_ product: StoreFrontProduct) { 119 | SFKIAPManager.shared.purchase(product: product, completion: completion) 120 | } 121 | 122 | public func restorePurchases() { 123 | SFKIAPManager.shared.restorePurchases() 124 | } 125 | 126 | public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 127 | return 120.0 128 | } 129 | } 130 | 131 | extension SFKMultiNonConsumableViewController: SFKProductTableViewCellDelegate { 132 | func sfkProductTableViewCell(_ cell: SFKProductTableViewCell, didTapGetWith model: SFKProductTableViewCell.ViewModel, index: Int) { 133 | purchase(sfkProduct[index]) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /StoreFrontKit/UserInterface/SFKNonConsumableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SFKConsumableViewController.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Represents display for single non-consumabel item 11 | public final class SFKNonConsumableViewController: UIViewController, StoreFrontKitDisplayable { 12 | /// Represents display associated products 13 | public var sfkProduct: [StoreFrontProduct] 14 | 15 | /// Represents completion handler 16 | public var completion: SFKIAPManager.SFKPurchaseCompletion 17 | 18 | private let iconImageView: UIImageView = { 19 | let imageView = UIImageView() 20 | imageView.contentMode = .scaleAspectFit 21 | return imageView 22 | }() 23 | 24 | private let descriptionLabel: UILabel = { 25 | let label = UILabel() 26 | label.numberOfLines = 0 27 | label.textColor = .label 28 | label.textAlignment = .center 29 | label.font = .systemFont(ofSize: 23, weight: .semibold) 30 | return label 31 | }() 32 | 33 | private let priceLabel: UILabel = { 34 | let label = UILabel() 35 | label.font = .systemFont(ofSize: 21, weight: .regular) 36 | label.numberOfLines = 1 37 | label.textColor = .secondaryLabel 38 | label.textAlignment = .center 39 | return label 40 | }() 41 | 42 | private let buyButton: UIButton = { 43 | let button = UIButton() 44 | button.backgroundColor = .systemGreen 45 | button.setTitle("Buy Now", for: .normal) 46 | button.setTitleColor(.white, for: .normal) 47 | button.layer.masksToBounds = true 48 | button.layer.cornerRadius = 8 49 | return button 50 | }() 51 | 52 | private let restoreButton: UIButton = { 53 | let button = UIButton() 54 | button.setTitle("Restore Purchases", for: .normal) 55 | button.setTitleColor(.systemBlue, for: .normal) 56 | return button 57 | }() 58 | 59 | // MARK: - Init 60 | 61 | public required init(with product: StoreFrontProduct, completion: @escaping SFKIAPManager.SFKPurchaseCompletion) { 62 | sfkProduct = [product] 63 | self.completion = completion 64 | super.init(nibName: nil, bundle: nil) 65 | } 66 | 67 | required init?(coder: NSCoder) { 68 | fatalError() 69 | } 70 | 71 | // MARK: - Lifecycle 72 | 73 | public override func viewDidLoad() { 74 | super.viewDidLoad() 75 | view.backgroundColor = .systemBackground 76 | addSubviews() 77 | configureUI() 78 | configureButtons() 79 | } 80 | 81 | public override func viewDidLayoutSubviews() { 82 | super.viewDidLayoutSubviews() 83 | 84 | buyButton.frame = CGRect( 85 | x: 15, 86 | y: view.frame.size.height-100-view.safeAreaInsets.bottom, 87 | width: view.frame.size.width-30, 88 | height: 50 89 | ).integral 90 | 91 | restoreButton.frame = CGRect( 92 | x: 15, 93 | y: view.frame.size.height-50-view.safeAreaInsets.bottom, 94 | width: view.frame.size.width-30, 95 | height: 50 96 | ).integral 97 | 98 | iconImageView.frame = CGRect( 99 | x: (view.frame.size.width - (view.frame.size.width/1.5))/2, 100 | y: view.safeAreaInsets.top + 15, 101 | width: view.frame.size.width/1.5, 102 | height: view.frame.size.width/1.5 103 | ).integral 104 | 105 | let remainingHeight: CGFloat = view.frame.size.height - 100 - iconImageView.frame.maxY - view.safeAreaInsets.bottom 106 | descriptionLabel.frame = CGRect( 107 | x: 20, 108 | y: iconImageView.frame.maxY, 109 | width: view.frame.size.width - 40, 110 | height: remainingHeight/2 111 | ).integral 112 | 113 | priceLabel.sizeToFit() 114 | priceLabel.frame = CGRect( 115 | x: 10, 116 | y: descriptionLabel.frame.maxY + 5, 117 | width: view.frame.size.width - 20, 118 | height: remainingHeight/2 119 | ).integral 120 | } 121 | 122 | private func configureUI() { 123 | iconImageView.image = sfkProduct.first?.viewModel?.icon 124 | iconImageView.tintColor = sfkProduct.first?.viewModel?.iconTintColor 125 | guard let product = sfkProduct.first else { 126 | return 127 | } 128 | let storeKitProduct = SFKIAPManager.shared.appStoreProducts.first(where: { $0.productIdentifier == product.identifier }) 129 | descriptionLabel.text = storeKitProduct?.localizedDescription 130 | priceLabel.text = "\(storeKitProduct?.priceLocale.currencySymbol ?? "$")" + "\(storeKitProduct?.price ?? NSDecimalNumber(value: 0))" 131 | } 132 | 133 | private func addSubviews() { 134 | view.addSubview(iconImageView) 135 | view.addSubview(descriptionLabel) 136 | view.addSubview(priceLabel) 137 | view.addSubview(buyButton) 138 | view.addSubview(restoreButton) 139 | } 140 | 141 | private func configureButtons() { 142 | buyButton.addTarget(self, action: #selector(didTapBuy), for: .touchUpInside) 143 | restoreButton.addTarget(self, action: #selector(didTapRestore), for: .touchUpInside) 144 | } 145 | 146 | @objc private func didTapBuy() { 147 | guard let product = sfkProduct.first else { 148 | return 149 | } 150 | purchase(product) 151 | } 152 | 153 | @objc private func didTapRestore() { 154 | restorePurchases() 155 | } 156 | 157 | // MARK: - StoreFront 158 | 159 | public func purchase(_ product: StoreFrontProduct) { 160 | SFKIAPManager.shared.purchase(product: product, completion: completion) 161 | } 162 | 163 | public func restorePurchases() { 164 | SFKIAPManager.shared.restorePurchases() 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /StoreFrontKit/UserInterface/SFKSubscriptionGroupViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SFKSubscriptionGroupViewController.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import UIKit 9 | 10 | // TODO: Update UI 11 | 12 | /// Represents display for subscription group 13 | public final class SFKSubscriptionGroupViewController: UIViewController, StoreFrontKitDisplayable { 14 | /// Represents display associated products 15 | public var sfkProduct: [StoreFrontProduct] 16 | 17 | /// Represents completion handler 18 | public var completion: SFKIAPManager.SFKPurchaseCompletion 19 | 20 | // MARK: - Init 21 | 22 | public required init(with product: StoreFrontProduct, completion: @escaping SFKIAPManager.SFKPurchaseCompletion) { 23 | sfkProduct = [product] 24 | self.completion = completion 25 | super.init(nibName: nil, bundle: nil) 26 | } 27 | 28 | required init?(coder: NSCoder) { 29 | fatalError() 30 | } 31 | 32 | // MARK: - Lifecycle 33 | 34 | public override func viewDidLoad() { 35 | super.viewDidLoad() 36 | view.backgroundColor = .systemBackground 37 | } 38 | 39 | public override func viewDidLayoutSubviews() { 40 | super.viewDidLayoutSubviews() 41 | } 42 | 43 | // MARK: - StoreFront 44 | 45 | public func purchase(_ product: StoreFrontProduct) { 46 | SFKIAPManager.shared.purchase(product: product, completion: completion) 47 | } 48 | 49 | public func restorePurchases() { 50 | SFKIAPManager.shared.restorePurchases() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /StoreFrontKit/UserInterface/SFKSubscriptionTrialViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SFKSubscriptionTrialViewController.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Represents display for subscription trial 11 | public final class SFKSubscriptionTrialViewController: UIViewController, StoreFrontKitDisplayable { 12 | /// Represents display associated products 13 | public var sfkProduct: [StoreFrontProduct] 14 | 15 | /// Represents completion handler 16 | public var completion: SFKIAPManager.SFKPurchaseCompletion 17 | 18 | // MARK: - Init 19 | 20 | public required init(with product: StoreFrontProduct, completion: @escaping SFKIAPManager.SFKPurchaseCompletion) { 21 | sfkProduct = [product] 22 | self.completion = completion 23 | super.init(nibName: nil, bundle: nil) 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | fatalError() 28 | } 29 | 30 | // MARK: - Lifecycle 31 | 32 | public override func viewDidLoad() { 33 | super.viewDidLoad() 34 | view.backgroundColor = .systemBackground 35 | } 36 | 37 | public override func viewDidLayoutSubviews() { 38 | super.viewDidLayoutSubviews() 39 | } 40 | 41 | // MARK: - StoreFront 42 | 43 | public func purchase(_ product: StoreFrontProduct) { 44 | SFKIAPManager.shared.purchase(product: product, completion: completion) 45 | } 46 | 47 | public func restorePurchases() { 48 | SFKIAPManager.shared.restorePurchases() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /StoreFrontKit/UserInterface/StoreFrontKitDisplayable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreFrontKitDisplayable.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import Foundation 9 | 10 | // TODO: Update UI 11 | 12 | /// Represents a store front 13 | public protocol StoreFrontKitDisplayable { 14 | /// Represents the product(s) to be displayed 15 | var sfkProduct: [StoreFrontProduct] { get set } 16 | 17 | /// Represents purchase completion callback 18 | var completion: SFKIAPManager.SFKPurchaseCompletion { get set } 19 | 20 | /// Represents display constructor 21 | /// - Parameters: 22 | /// - product: Represents product to display 23 | /// - completion: Completion callback for purchases 24 | init(with product: StoreFrontProduct, completion: @escaping SFKIAPManager.SFKPurchaseCompletion) 25 | 26 | /// Initiate product purchase 27 | /// - Parameter product: Product to be purchased 28 | func purchase(_ product: StoreFrontProduct) 29 | 30 | /// Initiate product restoration 31 | func restorePurchases() 32 | } 33 | -------------------------------------------------------------------------------- /StoreFrontKit/Views/SFKProductTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SFKProductTableViewCell.swift 3 | // StoreFrontKit 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol SFKProductTableViewCellDelegate: AnyObject { 11 | func sfkProductTableViewCell(_ cell: SFKProductTableViewCell, didTapGetWith model: SFKProductTableViewCell.ViewModel, index: Int) 12 | } 13 | 14 | /// Represents table view cell for product item 15 | final class SFKProductTableViewCell: UITableViewCell { 16 | /// Cell identifier 17 | static let identifier = "SFKProductTableViewCell" 18 | 19 | private var index = 0 20 | 21 | /// Delegate to notify of events 22 | weak var delegate: SFKProductTableViewCellDelegate? 23 | 24 | /// Represents cell viewModel 25 | struct ViewModel { 26 | let title: String 27 | let price: String 28 | let description: String 29 | let icon: UIImage? 30 | let iconTint: UIColor? 31 | } 32 | 33 | private var model: ViewModel? 34 | 35 | // MARK: - Subviews 36 | 37 | private let productTitleLabel: UILabel = { 38 | let label = UILabel() 39 | label.textColor = .label 40 | label.numberOfLines = 1 41 | label.font = .systemFont(ofSize: 20, weight: .semibold) 42 | label.textAlignment = .left 43 | return label 44 | }() 45 | 46 | private let productDescriptionLabel: UILabel = { 47 | let label = UILabel() 48 | label.numberOfLines = 0 49 | label.textColor = .secondaryLabel 50 | label.font = .systemFont(ofSize: 18, weight: .regular) 51 | label.textAlignment = .left 52 | return label 53 | }() 54 | 55 | private let productPriceLabel: UILabel = { 56 | let label = UILabel() 57 | label.numberOfLines = 0 58 | label.textColor = .label 59 | label.font = .systemFont(ofSize: 18, weight: .regular) 60 | label.textAlignment = .left 61 | return label 62 | }() 63 | 64 | private let getButton: UIButton = { 65 | let button = UIButton() 66 | button.backgroundColor = .systemBlue 67 | button.setTitle("GET", for: .normal) 68 | button.setTitleColor(.white, for: .normal) 69 | button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) 70 | button.layer.masksToBounds = true 71 | button.layer.cornerRadius = 6 72 | return button 73 | }() 74 | 75 | private let iconImageView: UIImageView = { 76 | let imageView = UIImageView() 77 | imageView.tintColor = .black 78 | imageView.contentMode = .scaleAspectFit 79 | return imageView 80 | }() 81 | 82 | // MARK: - Init 83 | 84 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 85 | super.init(style: style, reuseIdentifier: reuseIdentifier) 86 | contentView.clipsToBounds = true 87 | selectionStyle = .none 88 | accessoryType = .none 89 | getButton.addTarget(self, action: #selector(didTapGet), for: .touchUpInside) 90 | contentView.addSubview(iconImageView) 91 | contentView.addSubview(productTitleLabel) 92 | contentView.addSubview(productDescriptionLabel) 93 | contentView.addSubview(productPriceLabel) 94 | contentView.addSubview(getButton) 95 | } 96 | 97 | required init?(coder: NSCoder) { 98 | fatalError() 99 | } 100 | 101 | @objc private func didTapGet() { 102 | guard let model = model else { return } 103 | delegate?.sfkProductTableViewCell(self, didTapGetWith: model, index: index) 104 | } 105 | 106 | override func layoutSubviews() { 107 | super.layoutSubviews() 108 | iconImageView.frame = CGRect(x: 15, y: (contentView.frame.size.height-50)/2, width: 50, height: 50).integral 109 | getButton.frame = CGRect(x: contentView.frame.size.width-80, y: (contentView.frame.size.height-34)/2, width: 70, height: 34).integral 110 | 111 | productTitleLabel.sizeToFit() 112 | productTitleLabel.frame = CGRect(x: iconImageView.frame.maxX+5, y: 0, width: contentView.frame.size.width-80-iconImageView.frame.maxX, height: 35).integral 113 | productDescriptionLabel.frame = CGRect(x: iconImageView.frame.maxX+5, y: 35, width: contentView.frame.size.width-80-iconImageView.frame.maxX, height: 50).integral 114 | productPriceLabel.frame = CGRect(x: iconImageView.frame.maxX+5, y: 85, width: contentView.frame.size.width-80-iconImageView.frame.maxX, height: 35).integral 115 | } 116 | 117 | override func prepareForReuse() { 118 | super.prepareForReuse() 119 | productTitleLabel.text = nil 120 | productDescriptionLabel.text = nil 121 | productPriceLabel.text = nil 122 | iconImageView.tintColor = .clear 123 | } 124 | 125 | /// Configure cell with viewModel 126 | /// - Parameter viewModel: ViewModel to configure with 127 | func configure(with viewModel: ViewModel, index: Int) { 128 | self.index = index 129 | self.model = viewModel 130 | productTitleLabel.text = viewModel.title 131 | productDescriptionLabel.text = viewModel.description 132 | productPriceLabel.text = viewModel.price 133 | iconImageView.image = viewModel.icon 134 | iconImageView.tintColor = viewModel.iconTint 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /StoreFrontKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /StoreFrontKitTests/StoreFrontKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreFrontKitTests.swift 3 | // StoreFrontKitTests 4 | // 5 | // Created by Afraz Siddiqui on 12/13/20. 6 | // 7 | 8 | import XCTest 9 | @testable import StoreFrontKit 10 | 11 | class StoreFrontKitTests: XCTestCase {} 12 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AfrazCodes/StoreFrontKit/9b7897289e264c15ddb7027d8b70e09c5e184ed0/header.png -------------------------------------------------------------------------------- /store_front_kit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AfrazCodes/StoreFrontKit/9b7897289e264c15ddb7027d8b70e09c5e184ed0/store_front_kit.png --------------------------------------------------------------------------------