├── .gitignore ├── lucid.png ├── Auto-renewing-subscriptions ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Auto-renewing-subscriptions-Bridging-Header.h ├── ViewController.swift ├── In-app Purchases │ ├── Reachability.swift │ ├── DemoTransactionHandler.swift │ ├── KSReachability.h │ ├── InAppPurchaseManager.swift │ └── KSReachability.m ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── AppDelegate.swift ├── Auto-renewing-subscriptions.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── joseph.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── joseph.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /lucid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidsoftware/auto-renewing-subscription-demo/HEAD/lucid.png -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/Auto-renewing-subscriptions-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "KSReachability.h" 6 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions.xcodeproj/project.xcworkspace/xcuserdata/joseph.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidsoftware/auto-renewing-subscription-demo/HEAD/Auto-renewing-subscriptions.xcodeproj/project.xcworkspace/xcuserdata/joseph.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Auto-renewing-subscriptions.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions.xcodeproj/xcuserdata/joseph.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Auto-renewing-subscriptions.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Auto-renewing-subscriptions 4 | // 5 | // Created by Joseph Slinker on 4/16/19. 6 | // Copyright © 2019 Lucid Software. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | @IBAction func purchaseTapped(sender: UIButton) { 19 | guard let product = InAppPurchaseManager.sharedManager.availableProducts()?.first else { 20 | return 21 | } 22 | InAppPurchaseManager.sharedManager.purchaseProduct(product) { (receipts, error) in 23 | if let error = error { 24 | print(String(describing: error)) 25 | } else if let receipts = receipts { 26 | print("Congrats! Here are you receipts for your purchases: \(receipts)") 27 | } 28 | } 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/In-app Purchases/Reachability.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reachability.swift 3 | // Lucidchart 4 | // 5 | // Created by Parker Wightman on 1/5/15. 6 | // Copyright (c) 2015 Lucid Software. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Reachability { 12 | 13 | var reachable: Bool { 14 | return self.reach.reachable 15 | } 16 | 17 | var wifiReachable: Bool { 18 | return self.reach.reachable && !self.reach.wwanOnly 19 | } 20 | 21 | fileprivate let reach = KSReachability.toInternet()! 22 | fileprivate var changeBlocks = [ReachabilityBlock]() 23 | fileprivate var inititializedBlocks = [ReachabilityBlock]() 24 | fileprivate var view: UIView? 25 | 26 | typealias ReachabilityBlock = (_ reachable: Bool) -> Void 27 | 28 | init() { 29 | reach.onReachabilityChanged = { reach in 30 | for block in self.changeBlocks { 31 | block((reach?.reachable)!) 32 | } 33 | } 34 | 35 | reach.onInitializationComplete = { reach in 36 | for block in self.inititializedBlocks { 37 | block((reach?.reachable)!) 38 | } 39 | } 40 | } 41 | 42 | static let sharedInstance = Reachability() 43 | 44 | func onChange(_ block: @escaping ReachabilityBlock) { 45 | changeBlocks.append(block) 46 | } 47 | 48 | func onInitialize(_ block: @escaping ReachabilityBlock) { 49 | inititializedBlocks.append(block) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/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 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Auto-renewing-subscriptions 4 | // 5 | // Created by Joseph Slinker on 4/16/19. 6 | // Copyright © 2019 Lucid Software. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | 20 | InAppPurchaseManager.sharedManager.start(withHandler: DemoTransactionHandler()) 21 | 22 | return true 23 | } 24 | 25 | func applicationWillResignActive(_ application: UIApplication) { 26 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 27 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 28 | } 29 | 30 | func applicationDidEnterBackground(_ application: UIApplication) { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | func applicationWillEnterForeground(_ application: UIApplication) { 36 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 37 | } 38 | 39 | func applicationDidBecomeActive(_ application: UIApplication) { 40 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 41 | } 42 | 43 | func applicationWillTerminate(_ application: UIApplication) { 44 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 45 | } 46 | 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/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 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/In-app Purchases/DemoTransactionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoTransactionHandler.swift 3 | // Auto-renewing-subscriptions 4 | // 5 | // Created by Joseph Slinker on 4/16/19. 6 | // Copyright © 2019 Lucid Software. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StoreKit 11 | 12 | class DemoTransactionHandler: InAppPurchaseTransactionHandler { 13 | 14 | func availableProductIdentifiers() -> [String] { 15 | return ["identifier_from_itunes_connect_monthly", "identifier_from_itunes_connect_annual", "other_product_identifiers"] 16 | } 17 | 18 | func validateReceipt(receipt: Data, completion: @escaping InAppPurchaseCompletion) { 19 | /* 20 | YOUR CODE GOES HERE! 21 | Validate your receipt with your server and call `completion` with the result. 22 | 23 | Uncomment the below code if you're trying to test purchases and want to see the raw receipts in app. 24 | Do NOT use this method to validate receipts in production apps. Validating receipts in-app are vulnerable 25 | to man-in-the-middle attacks. 26 | */ 27 | 28 | // let data = Bundle.main.appStoreReceiptURL.flatMap { try? Data(contentsOf: $0) }! 29 | // let dict = ["receipt-data": data.base64EncodedString(), "password": <#shared secret from iTunes Connect or VerifyReceiptRequestPayload#>] 30 | // let requestData = try! JSONSerialization.data(withJSONObject: dict) 31 | // 32 | // let appleUrl = URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")! 33 | // var request = URLRequest(url: appleUrl) 34 | // request.httpMethod = "POST" 35 | // request.httpBody = requestData 36 | // 37 | // URLSession.shared.dataTask(with: request) { (data, response, error) in 38 | // if let data = data, let string = String(data: data, encoding: .utf8) { 39 | // print("Receipt data received: \(string)") 40 | // } else if let error = error { 41 | // print("Failed to fetch and parse receipt data. \(String(describing: error))") 42 | // } else { 43 | // print("Failed to fetch and parse receipt data, but no error was returned.") 44 | // } 45 | // 46 | // // Based on the data you have, call the completion 47 | // completion([], nil) 48 | // }.resume() 49 | } 50 | 51 | func transactionChangedToPurchasingState(transaction: SKPaymentTransaction) { 52 | print("Transaction changed to 'purchasing': \(String(describing: transaction))") 53 | } 54 | 55 | func transactionChangedToDeferredState(transaction: SKPaymentTransaction) { 56 | print("Transaction changed to 'deferred': \(String(describing: transaction))") 57 | } 58 | 59 | func failedToFetchProducts() { 60 | print("Failed to fetch products. Typically this means that not valid products are available in App Store Connect. Have you made it through approval yet?") 61 | } 62 | 63 | func purchaseFailedForProduct(product: SKProduct) { 64 | print("Something went wrong while processing the purchase. This does not include user cancellation. \(String(describing: product))") 65 | } 66 | 67 | func productPurchaseFinalized(product: SKProduct) { 68 | print("The purchase of the product was finalized. That means that the transaction has been closed and is considered complete. \(String(describing: product))") 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/In-app Purchases/KSReachability.h: -------------------------------------------------------------------------------- 1 | // 2 | // KSReachability.h 3 | // 4 | // Created by Karl Stenerud on 5/5/12. 5 | // 6 | // Copyright (c) 2012 Karl Stenerud. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall remain in place 16 | // in this source code. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import 28 | #import 29 | 30 | 31 | /** This is the notification name used in the Apple reachability example. 32 | * It is not used internally, and is merely a suggested notification name. 33 | */ 34 | #define kDefaultNetworkReachabilityChangedNotification @"kNetworkReachabilityChangedNotification" 35 | 36 | 37 | @class KSReachability; 38 | 39 | typedef void(^KSReachabilityCallback)(KSReachability* reachability); 40 | 41 | 42 | /** Monitors network connectivity. 43 | * 44 | * You can elect to be notified via blocks (onReachabilityChanged), 45 | * notifications (notificationName), or KVO (flags, reachable, and WWANOnly). 46 | * 47 | * All notification methods are disabled by default. 48 | * 49 | * Note: Upon construction, this object will fetch its initial reachability 50 | * state in the background. This means that the reachability status will ALWAYS 51 | * be "unreachable" until some time after object construction (possibly up to 10 52 | * seconds, depending on how long the DNS lookup takes). Use the "initialized" 53 | * property to monitor initialization, or set the callback "onInitializationComplete". 54 | */ 55 | @interface KSReachability : NSObject 56 | 57 | #pragma mark Constructors 58 | 59 | /** Reachability to a specific host. Returns nil if an initialization error occurs. 60 | * 61 | * @param hostname The name or IP address of the host to monitor. If nil or 62 | * empty string, check reachability to the internet in general. 63 | */ 64 | + (KSReachability*) reachabilityToHost:(NSString*) hostname; 65 | 66 | /** Reachability to the local (wired or wifi) network. Returns nil if an initialization error occurs. 67 | */ 68 | + (KSReachability*) reachabilityToLocalNetwork; 69 | 70 | /** Reachability to the internet. Returns nil if an initialization error occurs. 71 | */ 72 | + (KSReachability*) reachabilityToInternet; 73 | 74 | 75 | #pragma mark General Information 76 | 77 | /** The host we are monitoring reachability to, if any. */ 78 | @property(nonatomic,readonly,retain) NSString* hostname; 79 | 80 | 81 | #pragma mark Notifications and Callbacks 82 | 83 | /** If non-nil, called when the KSReachability object has finished initializing. 84 | * If initialization has already completed, calls on the next main thread run loop. 85 | * This block will only be called once, and then discarded (released). 86 | * Block will be invoked on the main thread. 87 | */ 88 | @property(atomic,readwrite,copy) KSReachabilityCallback onInitializationComplete; 89 | 90 | /** If non-nil, called whenever reachability flags change. 91 | * Block will be invoked on the main thread. 92 | */ 93 | @property(atomic,readwrite,copy) KSReachabilityCallback onReachabilityChanged; 94 | 95 | /** The notification to send when reachability changes (nil = don't send). 96 | * Default = nil 97 | */ 98 | @property(nonatomic,readwrite,retain) NSString* notificationName; 99 | 100 | 101 | #pragma mark KVO Compliant Status Properties 102 | 103 | /** The current reachability flags. 104 | * This property will always report 0 while "initialized" property = NO. 105 | */ 106 | @property(nonatomic,readonly,assign) SCNetworkReachabilityFlags flags; 107 | 108 | /** Whether the host is reachable or not. 109 | * This property will always report NO while "initialized" property = NO. 110 | */ 111 | @property(nonatomic,readonly,assign) BOOL reachable; 112 | 113 | /* If YES, the host is only reachable by WWAN (iOS only). 114 | * This property will always report NO while "initialized" property = NO. 115 | */ 116 | @property(nonatomic,readonly,assign) BOOL WWANOnly; 117 | 118 | /** If YES, this object's status properties are valid. */ 119 | @property(atomic,readonly,assign) BOOL initialized; 120 | 121 | @end 122 | 123 | 124 | 125 | /** A one-time operation to perform as soon as a host is deemed reachable. 126 | * The operation will only be performed once, regardless of how many times a 127 | * host becomes reachable. 128 | */ 129 | @interface KSReachableOperation: NSObject 130 | 131 | /** Constructor. Returns nil if an initialization error occurs. 132 | * 133 | * @param hostname The name or IP address of the host to monitor. If nil or 134 | * empty string, check reachability to the internet in general. 135 | * If hostname is a URL string, it will use the host portion. 136 | * 137 | * @param allowWWAN If NO, a WWAN-only connection is not enough to trigger 138 | * this operation. 139 | * 140 | * @param onReachabilityAchieved Invoke when the host becomes reachable. 141 | * This will be invoked ONE TIME ONLY, no matter 142 | * how many times reachability changes. 143 | * Block will be invoked on the main thread. 144 | */ 145 | + (KSReachableOperation*) operationWithHost:(NSString*) hostname 146 | allowWWAN:(BOOL) allowWWAN 147 | onReachabilityAchieved:(dispatch_block_t) onReachabilityAchieved; 148 | 149 | /** Constructor. Returns nil if an initialization error occurs. 150 | * 151 | * @param reachability A reachability instance. Note: This object will overwrite 152 | * the onReachabilityChanged property. 153 | * 154 | * @param allowWWAN If NO, a WWAN-only connection is not enough to trigger 155 | * this operation. 156 | * 157 | * @param onReachabilityAchieved Invoke when the host becomes reachable. 158 | * This will be invoked ONE TIME ONLY, no matter 159 | * how many times reachability changes. 160 | * Block will be invoked on the main thread. 161 | */ 162 | + (KSReachableOperation*) operationWithReachability:(KSReachability*) reachability 163 | allowWWAN:(BOOL) allowWWAN 164 | onReachabilityAchieved:(dispatch_block_t) onReachabilityAchieved; 165 | 166 | /** Initializer. Returns nil if an initialization error occurs. 167 | * 168 | * @param hostname The name or IP address of the host to monitor. If nil or 169 | * empty string, check reachability to the internet in general. 170 | * If hostname is a URL string, it will use the host portion. 171 | * 172 | * @param allowWWAN If NO, a WWAN-only connection is not enough to trigger 173 | * this operation. 174 | * 175 | * @param onReachabilityAchieved Invoke when the host becomes reachable. 176 | * This will be invoked ONE TIME ONLY, no matter 177 | * how many times reachability changes. 178 | * Block will be invoked on the main thread. 179 | */ 180 | - (id) initWithHost:(NSString*) hostname 181 | allowWWAN:(BOOL) allowWWAN 182 | onReachabilityAchieved:(dispatch_block_t) onReachabilityAchieved; 183 | 184 | /** Initializer. Returns nil if an initialization error occurs. 185 | * 186 | * @param reachability A reachability instance. Note: This object will overwrite 187 | * the onReachabilityChanged property. 188 | * 189 | * @param allowWWAN If NO, a WWAN-only connection is not enough to trigger 190 | * this operation. 191 | * 192 | * @param onReachabilityAchieved Invoke when the host becomes reachable. 193 | * This will be invoked ONE TIME ONLY, no matter 194 | * how many times reachability changes. 195 | * Block will be invoked on the main thread. 196 | */ 197 | - (id) initWithReachability:(KSReachability*) reachability 198 | allowWWAN:(BOOL) allowWWAN 199 | onReachabilityAchieved:(dispatch_block_t) onReachabilityAchieved; 200 | 201 | /** Access to internal reachability instance. Use this to monitor for errors. */ 202 | @property(nonatomic,readonly,retain) KSReachability* reachability; 203 | 204 | @end 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Lucid Software + Mobile](https://github.com/lucidsoftware/auto-renewing-subscription-demo/blob/master/lucid.png "Lucid Software + Mobile") 2 | 3 | 4 | 5 | # Tutorial: Auto-Renewable Subscriptions 6 | 7 | Adding In-app Purchases to an iOS app is hard to get right. This short demo will show how the Lucidchart iOS app handles in-app purchases as well as shares the majority of the code. 8 | 9 | The Swift classes shared in this demo are taken directly from the Lucidchart iOS app. This code has been used to offer multiple levels of auto-renewable in-app purchases for years now. In that time, the code has grown and evolved into a really reliable module for handling any kind of in-app purchase. 10 | 11 | ### How to Use 12 | 13 | The provided Xcode project will build and run as is, but will not attempt to process any in-app purchases without some work on your part. The following short tutorial will walk you through adding the necessary files to your project and how to implement the parts that are specific to your app. 14 | 15 | If you're not interested in reading the full tutorial provided, it can be summarized in 3 steps. 16 | 1. Add the files from the "In-App Purchases" directory to your project 17 | 2. Create in-app purchase products in App Store Connect 18 | 3. Call `start(withHandler:)` and `purchaseProduct(_ completion:)` 19 | 20 | ### Before You Get Started 21 | 22 | The only third party dependency this project has is [KSReachability](https://github.com/kstenerud/KSReachability). A copy of KSReachability and a wrapper to simplify using it is provided. 23 | 24 | This demo assumes that you have created your in-app purchases in App Store Connect. If you haven't done that, Ray Wenderlich has a great [tutorial](https://www.raywenderlich.com/659-in-app-purchases-auto-renewable-subscriptions-tutorial) on how to get that setup. 25 | 26 | TL;DR for creating in-app purchases: App Store Connect > My Apps > App Name > In-App Purchases > + button. You'll also need the Shared Secret available on the same page. 27 | 28 | ## Tutorial 29 | 30 | ### 1. Copy Project Files 31 | There are 5 files that you will need to copy to your project. They are all contained in the "In-app Purchases" directory. If your project doesn't already have a bridging header, you should be prompted to create one now. 32 | 33 | When finished copying, these 5 files should be visible within your project: 34 | 1. DemoTransactionHandler.swift 35 | 2. InAppPurchaseManager.swift 36 | 3. Reachability.swift 37 | 4. KSReachability.h 38 | 5. KSReachability.m 39 | 40 | With the exception of InAppPurchaseManager.swift, not all of these files are strictly necessary you should feel free to leave out the files that aren't relevant to you. 41 | 42 | DemoTransactionHandler.swift is a basic implementation of the only class you will need to provide. You can either skip this file entirely and implement your own, or you can add this file and build on top of it. The rest of the tutorial assumes you are building on top of it. 43 | 44 | KSReachability and Reachability.swift are for handling network connectivity. If you already have something that does this for your app, feel free to skip these. You'll see the one major place where they're used and you can replace our code with whatever equivalent you have in your project. 45 | 46 | ### 2. Bridging Header 47 | At this point you either had an existing bridging header or you were prompted to create one when you added KSReachability. Either way, make sure that you add KSReachability to your bridging header. 48 | ```objective-c 49 | #import "KSReachability.h" 50 | ``` 51 | 52 | ### 3. Observe Purchases 53 | With your project properly setup it's time to write some code. 54 | 55 | In-app purchase transactions are not always initialized by a user. In the case of auto-renewable subscriptions a new transaction will occur every time a subscription is up for renewal. Adding a purchase handler as soon as your app launches will ensure that non-user-initiated transactions are handled immediately. 56 | 57 | The Lucidchart iOS begins observing in the App Delegate, right after the app launches. You can add the following line of code to your App Delegate in `application(didFinishLaunchingWithOptions:)`. The provided demo project has this for you. 58 | ```swift 59 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 60 | InAppPurchaseManager.sharedManager.start(withHandler: DemoTransactionHandler()) 61 | return true 62 | } 63 | ``` 64 | 65 | ### 4. Validate Transactions 66 | The provided `DemoTransactionHandler` is a class that implements the `InAppPurchaseTransactionHandler` protocol. That protocol has 2 major responsibilities: 67 | 1. Provide the product identifiers found in App Store Connect 68 | 2. Validate purchase receipts 69 | 70 | The protocol additionally provides an interface for updating progress UI or sending analytics messages. 71 | 72 | ```swift 73 | func availableProductIdentifiers() -> [String] 74 | 75 | func validateReceipt(receipt: Data, completion: @escaping InAppPurchaseCompletion) 76 | ``` 77 | 78 | For your app to function you need to implement two functions. The provided implementation of `DemoTransactionHandler` has a commented out example of how a receipt could be validated. While the provided example does work for a demo, it is not acceptable for a production app. 79 | 80 | For most apps the correct way to validate a receipt is on the server. In fact, the Lucidchart app simply forwards the receipt data and validates receipts on the server in a way that is very similar to the provided code. If you prefer to validate receipts on device, Apple provides [documentation](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW2) on how to cryptographically verify the receipt data. 81 | 82 | Regardless of how you decide to validate your receipts you will need the shared secret from App Store Connect. The provided implementation of `DemoTransactionHandler` has a clearly marked place for you to put your shared secret. 83 | 84 | ### 5. Purchase a Product 85 | With your receipt validation logic implemented it's time to make a purchase. Simply choose a product from the list of available products to request that one be purchased. 86 | ```swift 87 | let product = InAppPurchaseManager.sharedManager.availableProducts().first! 88 | InAppPurchaseManager.sharedManager.purchaseProduct(product) { ... } 89 | ``` 90 | In the provided project this is done in the `ViewController` class. If you have correctly setup `DemoTransactionHandler`, then the product identifiers will be successfully converted to `SKProduct`'s and the purchase process will begin. 91 | 92 | Note: Because you are likely in a development environment the actual purchase flow provided by Apple's StoreKit will appear sligthly different than in production. This shouldn't affect your logic in any way. 93 | 94 | ### 6. Paid Application Agreement 95 | If your In-app Purchase is an auto-renewable subscription, then your app needs to disply certain information at the time of purchase. No exceptions. Twitter is littered with developers who were rejected by App Store review for omitting or changing the wording of the following bullet points. 96 | 97 | 1. Title of publication or service 98 | 2. Length of subscription (time period and/or content/services provided during each subscription 99 | period) 100 | 3. Price of subscription, and price per unit if appropriate 101 | 4. Payment will be charged to iTunes Account at confirmation of purchase 102 | 5. Subscription automatically renews unless auto-renew is turned off at least 24-hours before the end 103 | of the current period 104 | 6. Account will be charged for renewal within 24-hours prior to the end of the current period, and 105 | identify the cost of the renewal 106 | 7. Subscriptions may be managed by the user and auto-renewal may be turned off by going to the 107 | user’s Account Settings after purchase 108 | 8. Links to Your Privacy Policy and Terms of Use 109 | 110 | As a concrete example the Lucidchart app shows the following text at the time of purchase: 111 | ``` 112 | Lucidchart Basic: $5.99/month 113 | Payment will be charged to your iTunes account upon confirmation of purchase. 114 | Subscriptions automatically renew on a monthly basis from the date of original purchase. 115 | Subscriptions automatically renew unless auto-renew is turned off at least 24-hours before the end of the current period. 116 | Any unused portion of a free trial period will be forfeited when a subscription is purchased. 117 | To manage auto-renewal or cancel your subscription, please go to the iTunes Account Settings on your device. 118 | For more information, refer to our Terms and Conditions and Privacy Policy. 119 | ``` 120 | 121 | In addition to providing these disclosures at the time of purchase you will need to provide the same information in your App Store description. The following is taken directly from the Lucidchart App Store listing: 122 | 123 | ``` 124 | UPGRADE FOR FULL FUNCTIONALITY: 125 | * Lucidchart Basic gives you unlimited documents and unlimited shapes per document 126 | * Lucidchart Pro gives you all that plus Visio and Omnigraffle import, Visio export, and access to every shape library 127 | * With Lucidchart, you only need to upgrade once to get premium access on your iPhone, iPad, the web, and any other device 128 | * After a 7-day free trial, Free accounts are limited to 5 active documents and 60 objects per document 129 | 130 | Both Basic ($5.99 USD) and Pro ($8.99 USD) upgrades are available as monthly subscriptions. 131 | Subscriptions automatically renew on a monthly basis from the date of original purchase (unless auto-renewal is turned off at least 24 hours before the end of the current period). 132 | Subscriptions may be managed within iTunes Account Settings. 133 | Any unused portion of a free trial period will be forfeited when a subscription is purchased. 134 | ``` 135 | 136 | ### 7. Submit to the App Store 137 | You're done! If everything was done correctly you're now ready to submit your app to the App Store and watch the auto-renewing revenue pour in. 138 | 139 | Hopefully you found this demo helpful. If you have any issues, you can reach the team by emailing us at `ios at lucidchart.com`. You can also find the author on [Twitter](https://twitter.com/theslinker) if that's more your thing. 140 | 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/In-app Purchases/InAppPurchaseManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InAppPurchaseManager.swift 3 | // Lucidchart 4 | // 5 | // Created by Chloe Sumsion on 7/27/15. 6 | // Copyright (c) 2015 Lucid Software. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import StoreKit 11 | 12 | typealias InAppPurchaseCompletion = ((_ receipts:[InAppPurchaseReceipt]?, _ error:InAppPurchaseError?) -> Void) 13 | 14 | enum InAppPurchaseError: Equatable { 15 | case appBundleUnavailable 16 | case userCancelledTransaction 17 | case incorrectlyFormattedRequest 18 | case badResponse 19 | case internalError 20 | case unknown(error: Error) 21 | 22 | func description() -> String { 23 | switch self { 24 | case .appBundleUnavailable: 25 | return "app bundle unavailable" 26 | case .userCancelledTransaction: 27 | return "user cancelled transaction" 28 | case .incorrectlyFormattedRequest: 29 | return "incorrectly formatted request" 30 | case .badResponse: 31 | return "bad response" 32 | case .internalError: 33 | return "internal server error occured" 34 | case .unknown: 35 | return "unknown" 36 | } 37 | } 38 | } 39 | 40 | func ==(lhs: InAppPurchaseError, rhs: InAppPurchaseError) -> Bool { 41 | switch (lhs, rhs) { 42 | case (.appBundleUnavailable, .appBundleUnavailable): 43 | return true 44 | case (.userCancelledTransaction, .userCancelledTransaction): 45 | return true 46 | case (.incorrectlyFormattedRequest, .incorrectlyFormattedRequest): 47 | return true 48 | case (.badResponse, .badResponse): 49 | return true 50 | case (.internalError, .internalError): 51 | return true 52 | case (.unknown, .unknown): 53 | return true 54 | default: 55 | return false 56 | } 57 | } 58 | 59 | /// Represents the data necessary for processing a receipt. 60 | class InAppPurchaseReceipt { 61 | 62 | let transactionID: String 63 | let purchaseDate: Date 64 | let belongsToCurrentAccount: Bool 65 | 66 | init(transactionID: String, purchaseDate: Date, belongsToCurrentAccount: Bool) { 67 | self.transactionID = transactionID 68 | self.purchaseDate = purchaseDate 69 | self.belongsToCurrentAccount = belongsToCurrentAccount 70 | } 71 | 72 | } 73 | 74 | protocol InAppPurchaseTransactionHandler { 75 | // Absolutely necessary for the purchase manager to function 76 | func availableProductIdentifiers() -> [String] 77 | func validateReceipt(receipt: Data, completion: @escaping InAppPurchaseCompletion) 78 | 79 | // Not strictly necessary, but may be useful for analytics etc. 80 | func transactionChangedToPurchasingState(transaction: SKPaymentTransaction) 81 | func transactionChangedToDeferredState(transaction: SKPaymentTransaction) 82 | func failedToFetchProducts() 83 | func purchaseFailedForProduct(product: SKProduct) 84 | func productPurchaseFinalized(product: SKProduct) 85 | } 86 | 87 | class InAppPurchaseManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver, SKRequestDelegate { 88 | 89 | static let sharedManager = InAppPurchaseManager() 90 | fileprivate var productsResponse: SKProductsResponse? 91 | fileprivate var purchaseCompletion:InAppPurchaseCompletion? 92 | fileprivate(set) var purchasingProduct: SKProduct? 93 | fileprivate var transactionHandler: InAppPurchaseTransactionHandler! 94 | fileprivate var transactionsToFinalize = [SKPaymentTransaction]() 95 | 96 | 97 | /// This should **never** be used in a production version of the app. Setting setting to true will cause all incoming transactions to skipping processing and immediately resolve. This is useful for clearing out the queue of iTunes popups that can happen from testing purchases. 98 | private var skipTransactionProcessingDebug: Bool = false 99 | 100 | public func setSkipTransactionProcessingDebug(_ value: Bool) { 101 | #if DEBUG 102 | self.skipTransactionProcessingDebug = value 103 | #endif 104 | } 105 | 106 | 107 | /// Begins observing changes to the In-app purchase queue. An implementation of the `InAppPurchaseTransactionHandler` protocol should be implemented by your app. 108 | /// 109 | /// - Parameter handler: Handles transaction state changes. Will be retained by the receiver and is not typically used outside of the receiver. 110 | func start(withHandler handler: InAppPurchaseTransactionHandler) { 111 | assert(self.transactionHandler == nil, "startWithHandler: may only be called once") 112 | self.transactionHandler = handler 113 | 114 | SKPaymentQueue.default().add(self) 115 | self.fetchProducts() 116 | 117 | // If start listening is called offline, it will fail. We need to be able to recover. 118 | Reachability.sharedInstance.onChange { [weak self] reachable in 119 | if reachable && self?.productsResponse?.products == nil { 120 | self?.fetchProducts() 121 | } 122 | } 123 | } 124 | 125 | private func fetchProducts() { 126 | if SKPaymentQueue.canMakePayments() { 127 | let identifiers = self.transactionHandler.availableProductIdentifiers() 128 | let request = SKProductsRequest(productIdentifiers: Set(identifiers)) 129 | request.delegate = self 130 | request.start() 131 | } 132 | else { 133 | self.transactionHandler.failedToFetchProducts() 134 | } 135 | } 136 | 137 | /// If products is nil, it's because in-app purchases are not available or there was an error in fetching from the app store 138 | /// 139 | /// - Returns: A list of products available to be purchased or `nil` if an error occurred while fetching products from App Store Connect 140 | func availableProducts() -> [SKProduct]? { 141 | return self.productsResponse?.products 142 | } 143 | 144 | 145 | /// Begin the purchasing process for a specific product. 146 | /// 147 | /// - Parameters: 148 | /// - product: The product to be purchased. Should be one of the products listed in `availableProducts` 149 | /// - completion: Called when the purchase has either been completed or failed 150 | func purchaseProduct(_ product: SKProduct, completion:InAppPurchaseCompletion?) { 151 | self.purchaseCompletion = { (receipts, error) in 152 | if let error = error, error != .userCancelledTransaction { 153 | self.transactionHandler.purchaseFailedForProduct(product: product) 154 | } 155 | completion?(receipts, error) 156 | } 157 | self.purchasingProduct = product 158 | let payment = SKPayment(product: product) 159 | SKPaymentQueue.default().add(payment) 160 | } 161 | 162 | 163 | /// Checks to see if the user has any history of In-app purchases. This is typically exposed to the user as a "Check For Purchases" or "Restore Purchases" button 164 | /// 165 | /// - Parameter completion: Called when the purchase has either been completed or failed 166 | func revalidateAllPurchases(_ completion:InAppPurchaseCompletion?) { 167 | self.purchaseCompletion = completion 168 | self.refreshReceipt() 169 | } 170 | 171 | // MARK: - SKProductsRequestDelegate - 172 | 173 | func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { 174 | self.productsResponse = response 175 | } 176 | 177 | // MARK: - SKPaymentTransactionObserver - 178 | 179 | func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { 180 | for transaction in transactions { 181 | switch transaction.transactionState { 182 | case .purchasing: 183 | self.handlePurchasingState(transaction) 184 | case .purchased: 185 | self.handlePurchasedState(transaction) 186 | case .failed: 187 | self.handleFailedState(transaction) 188 | case .restored: 189 | self.handleRestoredState(transaction) 190 | case .deferred: 191 | self.handleDeferredState(transaction) 192 | @unknown default: 193 | break 194 | } 195 | } 196 | } 197 | 198 | private func handlePurchasingState(_ transaction: SKPaymentTransaction) { 199 | // This `if` block is only for aiding debug, no production code should ever be in it 200 | #if DEBUG 201 | if self.skipTransactionProcessingDebug { 202 | SKPaymentQueue.default().finishTransaction(transaction) 203 | return 204 | } 205 | #endif 206 | 207 | self.transactionHandler.transactionChangedToPurchasingState(transaction: transaction) 208 | } 209 | 210 | private func handlePurchasedState(_ transaction: SKPaymentTransaction) { 211 | // This `if` block is only for aiding debug, no production code should ever be in it 212 | #if DEBUG 213 | if self.skipTransactionProcessingDebug { 214 | SKPaymentQueue.default().finishTransaction(transaction) 215 | return 216 | } 217 | #endif 218 | 219 | self.transactionsToFinalize.append(transaction) 220 | self.validateReceipt() 221 | } 222 | 223 | // Transaction should only fail on simulator. This prevents those test transaction from getting stuck permanently on device 224 | private func handleFailedState(_ transaction: SKPaymentTransaction) { 225 | SKPaymentQueue.default().finishTransaction(transaction) 226 | self.purchaseCompletion?(nil, .userCancelledTransaction) 227 | } 228 | 229 | private func handleRestoredState(_ transaction: SKPaymentTransaction) { 230 | #if DEBUG 231 | if self.skipTransactionProcessingDebug { 232 | SKPaymentQueue.default().finishTransaction(transaction) 233 | return 234 | } 235 | #endif 236 | 237 | self.transactionsToFinalize.append(transaction) 238 | self.validateReceipt() 239 | } 240 | 241 | private func handleDeferredState(_ transaction: SKPaymentTransaction) { 242 | self.transactionHandler.transactionChangedToDeferredState(transaction: transaction) 243 | } 244 | 245 | // MARK: - Receipt Validation - 246 | 247 | private func validateReceipt() { 248 | let url = Bundle.main.appStoreReceiptURL! 249 | guard let receipt = try? Data(contentsOf: url) else { 250 | self.refreshReceipt() 251 | return 252 | } 253 | 254 | self.transactionHandler.validateReceipt(receipt: receipt) { receipts, error in 255 | self.purchaseCompletion?(receipts, error) 256 | 257 | let closeTransactions = { 258 | while let transaction = self.transactionsToFinalize.popLast() { 259 | SKPaymentQueue.default().finishTransaction(transaction) 260 | } 261 | } 262 | 263 | guard let receipts = receipts, self.transactionsToFinalize.count > 0 else { 264 | if error == nil { 265 | closeTransactions() 266 | } 267 | return 268 | } 269 | 270 | // finalize the purchases that we are given receipts for 271 | for receipt in receipts { 272 | let tuple = self.transactionsToFinalize.enumerated().first(where: { 273 | return $0.element.transactionIdentifier == receipt.transactionID 274 | }) 275 | if let tuple = tuple { 276 | self.transactionsToFinalize.remove(at: tuple.offset) 277 | SKPaymentQueue.default().finishTransaction(tuple.element) 278 | 279 | let product = self.productsResponse?.products.first(where: { 280 | $0.productIdentifier == tuple.element.payment.productIdentifier 281 | }) 282 | 283 | if let product = product { 284 | self.transactionHandler.productPurchaseFinalized(product: product) 285 | } 286 | } 287 | } 288 | 289 | closeTransactions() 290 | } 291 | } 292 | 293 | private func refreshReceipt() { 294 | let request = SKReceiptRefreshRequest(receiptProperties: nil) 295 | request.delegate = self 296 | request.start() 297 | } 298 | 299 | // MARK: - SKRequestDelegate 300 | 301 | func requestDidFinish(_ request: SKRequest) { 302 | if request is SKReceiptRefreshRequest { 303 | if let url = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: url.path) { 304 | self.validateReceipt() 305 | } 306 | else { 307 | self.purchaseCompletion?(nil, InAppPurchaseError.appBundleUnavailable) 308 | } 309 | } 310 | } 311 | 312 | func request(_ request: SKRequest, didFailWithError error: Error) { 313 | if request is SKReceiptRefreshRequest { 314 | self.purchaseCompletion?(nil, InAppPurchaseError.appBundleUnavailable) 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions/In-app Purchases/KSReachability.m: -------------------------------------------------------------------------------- 1 | // 2 | // KSReachability.m 3 | // 4 | // Created by Karl Stenerud on 5/5/12. 5 | // 6 | // Copyright (c) 2012 Karl Stenerud. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall remain in place 16 | // in this source code. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import "KSReachability.h" 28 | #import 29 | #import 30 | 31 | 32 | // ---------------------------------------------------------------------- 33 | #pragma mark - ARC-Safe Memory Management - 34 | // ---------------------------------------------------------------------- 35 | 36 | // Full version at https://github.com/kstenerud/ARCSafe-MemManagement 37 | #if __has_feature(objc_arc) 38 | #define as_release(X) 39 | #define as_autorelease(X) (X) 40 | #define as_autorelease_noref(X) 41 | #define as_superdealloc() 42 | #define as_bridge __bridge 43 | #else 44 | #define as_release(X) [(X) release] 45 | #define as_autorelease(X) [(X) autorelease] 46 | #define as_autorelease_noref(X) [(X) autorelease] 47 | #define as_superdealloc() [super dealloc] 48 | #define as_bridge 49 | #endif 50 | 51 | 52 | #define kKVOProperty_Flags @"flags" 53 | #define kKVOProperty_Reachable @"reachable" 54 | #define kKVOProperty_WWANOnly @"WWANOnly" 55 | 56 | 57 | // ---------------------------------------------------------------------- 58 | #pragma mark - KSReachability - 59 | // ---------------------------------------------------------------------- 60 | 61 | @interface KSReachability () 62 | 63 | @property(nonatomic,readwrite,retain) NSString* hostname; 64 | @property(nonatomic,readwrite,assign) SCNetworkReachabilityFlags flags; 65 | @property(nonatomic,readwrite,assign) BOOL reachable; 66 | @property(nonatomic,readwrite,assign) BOOL WWANOnly; 67 | @property(nonatomic,readwrite,assign) SCNetworkReachabilityRef reachabilityRef; 68 | @property(atomic,readwrite,assign) BOOL initialized; 69 | 70 | @end 71 | 72 | static void onReachabilityChanged(SCNetworkReachabilityRef target, 73 | SCNetworkReachabilityFlags flags, 74 | void* info); 75 | 76 | 77 | @implementation KSReachability 78 | 79 | @synthesize onInitializationComplete = _onInitializationComplete; 80 | @synthesize onReachabilityChanged = _onReachabilityChanged; 81 | @synthesize flags = _flags; 82 | @synthesize reachable = _reachable; 83 | @synthesize WWANOnly = _WWANOnly; 84 | @synthesize reachabilityRef = _reachabilityRef; 85 | @synthesize hostname = _hostname; 86 | @synthesize notificationName = _notificationName; 87 | @synthesize initialized = _initialized; 88 | 89 | + (KSReachability*) reachabilityToHost:(NSString*) hostname 90 | { 91 | return as_autorelease([[self alloc] initWithHost:hostname]); 92 | } 93 | 94 | + (KSReachability*) reachabilityToLocalNetwork 95 | { 96 | struct sockaddr_in address; 97 | bzero(&address, sizeof(address)); 98 | address.sin_len = sizeof(address); 99 | address.sin_family = AF_INET; 100 | address.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM); 101 | 102 | return as_autorelease([[self alloc] initWithAddress:(const struct sockaddr*)&address]); 103 | } 104 | 105 | + (KSReachability*) reachabilityToInternet 106 | { 107 | struct sockaddr_in address; 108 | bzero(&address, sizeof(address)); 109 | address.sin_len = sizeof(address); 110 | address.sin_family = AF_INET; 111 | 112 | return as_autorelease([[self alloc] initWithAddress:(const struct sockaddr*)&address]); 113 | } 114 | 115 | - (id) initWithHost:(NSString*) hostname 116 | { 117 | hostname = [self extractHostName:hostname]; 118 | const char* name = [hostname UTF8String]; 119 | 120 | struct sockaddr_in6 address; 121 | bzero(&address, sizeof(address)); 122 | address.sin6_len = sizeof(address); 123 | address.sin6_family = AF_INET; 124 | 125 | if([hostname length] > 0) 126 | { 127 | if(inet_pton(address.sin6_family, name, &address.sin6_addr) != 1) 128 | { 129 | address.sin6_family = AF_INET6; 130 | if(inet_pton(address.sin6_family, name, &address.sin6_addr) != 1) 131 | { 132 | return [self initWithReachabilityRef:SCNetworkReachabilityCreateWithName(NULL, name) 133 | hostname:hostname]; 134 | } 135 | } 136 | } 137 | 138 | return [self initWithAddress:(const struct sockaddr*)&address]; 139 | } 140 | 141 | - (id) initWithAddress:(const struct sockaddr*) address 142 | { 143 | return [self initWithReachabilityRef:SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, address) 144 | hostname:nil]; 145 | } 146 | 147 | - (id) initWithReachabilityRef:(SCNetworkReachabilityRef) reachabilityRef 148 | hostname:(NSString*)hostname 149 | { 150 | if((self = [super init])) 151 | { 152 | if(reachabilityRef == NULL) 153 | { 154 | NSLog(@"KSReachability Error: %s: Could not resolve reachability destination", __PRETTY_FUNCTION__); 155 | goto init_failed; 156 | } 157 | else 158 | { 159 | self.hostname = hostname; 160 | self.reachabilityRef = reachabilityRef; 161 | 162 | SCNetworkReachabilityContext context = {0, (as_bridge void*)self, NULL, NULL, NULL}; 163 | if(!SCNetworkReachabilitySetCallback(self.reachabilityRef, 164 | onReachabilityChanged, 165 | &context)) 166 | { 167 | NSLog(@"KSReachability Error: %s: SCNetworkReachabilitySetCallback failed", __PRETTY_FUNCTION__); 168 | goto init_failed; 169 | } 170 | 171 | if(!SCNetworkReachabilityScheduleWithRunLoop(self.reachabilityRef, 172 | CFRunLoopGetMain(), 173 | kCFRunLoopDefaultMode)) 174 | { 175 | NSLog(@"KSReachability Error: %s: SCNetworkReachabilityScheduleWithRunLoop failed", __PRETTY_FUNCTION__); 176 | goto init_failed; 177 | } 178 | 179 | // If you create a reachability ref using SCNetworkReachabilityCreateWithAddress(), 180 | // it won't trigger from the runloop unless you kick it with SCNetworkReachabilityGetFlags() 181 | if([hostname length] == 0) 182 | { 183 | SCNetworkReachabilityFlags flags; 184 | // Note: This won't block because there's no host to look up. 185 | if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) 186 | { 187 | NSLog(@"KSReachability Error: %s: SCNetworkReachabilityGetFlags failed", __PRETTY_FUNCTION__); 188 | goto init_failed; 189 | } 190 | 191 | dispatch_async(dispatch_get_main_queue(), ^ 192 | { 193 | [self onReachabilityFlagsChanged:flags]; 194 | }); 195 | } 196 | } 197 | } 198 | return self; 199 | 200 | init_failed: 201 | as_release(self); 202 | self = nil; 203 | return self; 204 | } 205 | 206 | - (void) dealloc 207 | { 208 | if(_reachabilityRef != NULL) 209 | { 210 | SCNetworkReachabilityUnscheduleFromRunLoop(_reachabilityRef, 211 | CFRunLoopGetMain(), 212 | kCFRunLoopDefaultMode); 213 | CFRelease(_reachabilityRef); 214 | } 215 | as_release(_hostname); 216 | as_release(_notificationName); 217 | as_release(_onReachabilityChanged); 218 | as_superdealloc(); 219 | } 220 | 221 | - (NSString*) extractHostName:(NSString*) potentialURL 222 | { 223 | if(potentialURL == nil) 224 | { 225 | return nil; 226 | } 227 | 228 | NSString* host = [[NSURL URLWithString:potentialURL] host]; 229 | if(host != nil) 230 | { 231 | return host; 232 | } 233 | return potentialURL; 234 | } 235 | 236 | - (BOOL) isReachableWithFlags:(SCNetworkReachabilityFlags) flags 237 | { 238 | if(!(flags & kSCNetworkReachabilityFlagsReachable)) 239 | { 240 | // Not reachable at all. 241 | return NO; 242 | } 243 | 244 | if(!(flags & kSCNetworkReachabilityFlagsConnectionRequired)) 245 | { 246 | // Reachable with no connection required. 247 | return YES; 248 | } 249 | 250 | if((flags & (kSCNetworkReachabilityFlagsConnectionOnDemand | 251 | kSCNetworkReachabilityFlagsConnectionOnTraffic)) && 252 | !(flags & kSCNetworkReachabilityFlagsInterventionRequired)) 253 | { 254 | // Automatic connection with no user intervention required. 255 | return YES; 256 | } 257 | 258 | return NO; 259 | } 260 | 261 | - (BOOL) isReachableWWANOnlyWithFlags:(SCNetworkReachabilityFlags) flags 262 | { 263 | #if TARGET_OS_IPHONE 264 | BOOL isReachable = [self isReachableWithFlags:flags]; 265 | BOOL isWWANOnly = (flags & kSCNetworkReachabilityFlagsIsWWAN) != 0; 266 | return isReachable && isWWANOnly; 267 | #else 268 | #pragma unused(flags) 269 | return NO; 270 | #endif 271 | } 272 | 273 | - (KSReachabilityCallback) onInitializationComplete 274 | { 275 | @synchronized(self) 276 | { 277 | return _onInitializationComplete; 278 | } 279 | } 280 | 281 | - (void) setOnInitializationComplete:(KSReachabilityCallback) onInitializationComplete 282 | { 283 | @synchronized(self) 284 | { 285 | as_autorelease_noref(_onInitializationComplete); 286 | _onInitializationComplete = [onInitializationComplete copy]; 287 | if(_onInitializationComplete != nil && self.initialized) 288 | { 289 | dispatch_async(dispatch_get_main_queue(), ^ 290 | { 291 | [self callInitializationComplete]; 292 | }); 293 | } 294 | } 295 | } 296 | 297 | - (void) callInitializationComplete 298 | { 299 | // This method expects to be called on the main run loop so that 300 | // all callbacks occur on the main run loop. 301 | @synchronized(self) 302 | { 303 | KSReachabilityCallback callback = self.onInitializationComplete; 304 | self.onInitializationComplete = nil; 305 | if(callback != nil) 306 | { 307 | callback(self); 308 | } 309 | } 310 | } 311 | 312 | - (void) onReachabilityFlagsChanged:(SCNetworkReachabilityFlags) flags 313 | { 314 | // This method expects to be called on the main run loop so that 315 | // all callbacks occur on the main run loop. 316 | @synchronized(self) 317 | { 318 | BOOL wasInitialized = self.initialized; 319 | 320 | if(_flags != flags || !wasInitialized) 321 | { 322 | BOOL reachable = [self isReachableWithFlags:flags]; 323 | BOOL WWANOnly = [self isReachableWWANOnlyWithFlags:flags]; 324 | BOOL rChanged = (_reachable != reachable) || !wasInitialized; 325 | BOOL wChanged = (_WWANOnly != WWANOnly) || !wasInitialized; 326 | 327 | [self willChangeValueForKey:kKVOProperty_Flags]; 328 | if(rChanged) [self willChangeValueForKey:kKVOProperty_Reachable]; 329 | if(wChanged) [self willChangeValueForKey:kKVOProperty_WWANOnly]; 330 | 331 | _flags = flags; 332 | _reachable = reachable; 333 | _WWANOnly = WWANOnly; 334 | 335 | if(!wasInitialized) 336 | { 337 | self.initialized = YES; 338 | } 339 | 340 | [self didChangeValueForKey:kKVOProperty_Flags]; 341 | if(rChanged) [self didChangeValueForKey:kKVOProperty_Reachable]; 342 | if(wChanged) [self didChangeValueForKey:kKVOProperty_WWANOnly]; 343 | 344 | if(self.onReachabilityChanged != nil) 345 | { 346 | self.onReachabilityChanged(self); 347 | } 348 | 349 | if(self.notificationName != nil) 350 | { 351 | NSNotificationCenter* nCenter = [NSNotificationCenter defaultCenter]; 352 | [nCenter postNotificationName:self.notificationName object:self]; 353 | } 354 | 355 | if(!wasInitialized) 356 | { 357 | [self callInitializationComplete]; 358 | } 359 | } 360 | } 361 | } 362 | 363 | 364 | static void onReachabilityChanged(__unused SCNetworkReachabilityRef target, 365 | SCNetworkReachabilityFlags flags, 366 | void* info) 367 | { 368 | KSReachability* reachability = (as_bridge KSReachability*) info; 369 | [reachability onReachabilityFlagsChanged:flags]; 370 | } 371 | 372 | @end 373 | 374 | 375 | // ---------------------------------------------------------------------- 376 | #pragma mark - KSReachableOperation - 377 | // ---------------------------------------------------------------------- 378 | 379 | @interface KSReachableOperation () 380 | 381 | @property(nonatomic,readwrite,retain) KSReachability* reachability; 382 | 383 | @end 384 | 385 | 386 | @implementation KSReachableOperation 387 | 388 | @synthesize reachability = _reachability; 389 | 390 | + (KSReachableOperation*) operationWithHost:(NSString*) host 391 | allowWWAN:(BOOL) allowWWAN 392 | onReachabilityAchieved:(dispatch_block_t) onReachabilityAchieved 393 | { 394 | return as_autorelease([[self alloc] initWithHost:host 395 | allowWWAN:allowWWAN 396 | onReachabilityAchieved:onReachabilityAchieved]); 397 | } 398 | 399 | + (KSReachableOperation*) operationWithReachability:(KSReachability*) reachability 400 | allowWWAN:(BOOL) allowWWAN 401 | onReachabilityAchieved:(dispatch_block_t) onReachabilityAchieved 402 | { 403 | return as_autorelease([[self alloc] initWithReachability:reachability 404 | allowWWAN:allowWWAN 405 | onReachabilityAchieved:onReachabilityAchieved]); 406 | } 407 | 408 | - (id) initWithHost:(NSString*) host 409 | allowWWAN:(BOOL) allowWWAN 410 | onReachabilityAchieved:(dispatch_block_t) onReachabilityAchieved 411 | { 412 | return [self initWithReachability:[KSReachability reachabilityToHost:host] 413 | allowWWAN:allowWWAN 414 | onReachabilityAchieved:onReachabilityAchieved]; 415 | } 416 | 417 | - (id) initWithReachability:(KSReachability*) reachability 418 | allowWWAN:(BOOL) allowWWAN 419 | onReachabilityAchieved:(dispatch_block_t) onReachabilityAchieved 420 | { 421 | if((self = [super init])) 422 | { 423 | self.reachability = reachability; 424 | if(self.reachability == nil || onReachabilityAchieved == nil) 425 | { 426 | as_release(self); 427 | self = nil; 428 | } 429 | else 430 | { 431 | onReachabilityAchieved = as_autorelease([onReachabilityAchieved copy]); 432 | KSReachabilityCallback onReachabilityChanged = ^(KSReachability* reachability2) 433 | { 434 | @synchronized(reachability2) 435 | { 436 | if(reachability2.onReachabilityChanged != nil && 437 | reachability2.reachable && 438 | (allowWWAN || !reachability2.WWANOnly)) 439 | { 440 | reachability2.onReachabilityChanged = nil; 441 | onReachabilityAchieved(); 442 | } 443 | } 444 | }; 445 | 446 | self.reachability.onReachabilityChanged = onReachabilityChanged; 447 | 448 | // Check once manually in case the host is already reachable. 449 | onReachabilityChanged(self.reachability); 450 | } 451 | } 452 | return self; 453 | } 454 | 455 | - (void) dealloc 456 | { 457 | as_release(_reachability); 458 | as_superdealloc(); 459 | } 460 | 461 | @end 462 | -------------------------------------------------------------------------------- /Auto-renewing-subscriptions.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7C9D396D22664B780010F619 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9D396C22664B780010F619 /* AppDelegate.swift */; }; 11 | 7C9D396F22664B780010F619 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9D396E22664B780010F619 /* ViewController.swift */; }; 12 | 7C9D397222664B780010F619 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C9D397022664B780010F619 /* Main.storyboard */; }; 13 | 7C9D397422664B790010F619 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C9D397322664B790010F619 /* Assets.xcassets */; }; 14 | 7C9D397722664B7A0010F619 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C9D397522664B7A0010F619 /* LaunchScreen.storyboard */; }; 15 | 7C9D397F22664BB60010F619 /* InAppPurchaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9D397E22664BB60010F619 /* InAppPurchaseManager.swift */; }; 16 | 7C9D398122664BE70010F619 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9D398022664BE70010F619 /* Reachability.swift */; }; 17 | 7C9D398522664CA10010F619 /* KSReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 7C9D398422664CA10010F619 /* KSReachability.m */; }; 18 | 7C9D398822664DB80010F619 /* DemoTransactionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9D398722664DB80010F619 /* DemoTransactionHandler.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 7C9D396922664B780010F619 /* Auto-renewing-subscriptions.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Auto-renewing-subscriptions.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 7C9D396C22664B780010F619 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 7C9D396E22664B780010F619 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | 7C9D397122664B780010F619 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 7C9D397322664B790010F619 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | 7C9D397622664B7A0010F619 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | 7C9D397822664B7A0010F619 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 7C9D397E22664BB60010F619 /* InAppPurchaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppPurchaseManager.swift; sourceTree = ""; }; 30 | 7C9D398022664BE70010F619 /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; 31 | 7C9D398222664CA00010F619 /* Auto-renewing-subscriptions-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Auto-renewing-subscriptions-Bridging-Header.h"; sourceTree = ""; }; 32 | 7C9D398322664CA10010F619 /* KSReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KSReachability.h; sourceTree = ""; }; 33 | 7C9D398422664CA10010F619 /* KSReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSReachability.m; sourceTree = ""; }; 34 | 7C9D398722664DB80010F619 /* DemoTransactionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoTransactionHandler.swift; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 7C9D396622664B780010F619 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 7C9D396022664B780010F619 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 7C9D396B22664B780010F619 /* Auto-renewing-subscriptions */, 52 | 7C9D396A22664B780010F619 /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | 7C9D396A22664B780010F619 /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 7C9D396922664B780010F619 /* Auto-renewing-subscriptions.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | 7C9D396B22664B780010F619 /* Auto-renewing-subscriptions */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 7C9D396C22664B780010F619 /* AppDelegate.swift */, 68 | 7C9D398622664CA90010F619 /* In-app Purchases */, 69 | 7C9D396E22664B780010F619 /* ViewController.swift */, 70 | 7C9D397022664B780010F619 /* Main.storyboard */, 71 | 7C9D397322664B790010F619 /* Assets.xcassets */, 72 | 7C9D397522664B7A0010F619 /* LaunchScreen.storyboard */, 73 | 7C9D397822664B7A0010F619 /* Info.plist */, 74 | 7C9D398222664CA00010F619 /* Auto-renewing-subscriptions-Bridging-Header.h */, 75 | ); 76 | path = "Auto-renewing-subscriptions"; 77 | sourceTree = ""; 78 | }; 79 | 7C9D398622664CA90010F619 /* In-app Purchases */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 7C9D398722664DB80010F619 /* DemoTransactionHandler.swift */, 83 | 7C9D397E22664BB60010F619 /* InAppPurchaseManager.swift */, 84 | 7C9D398022664BE70010F619 /* Reachability.swift */, 85 | 7C9D398322664CA10010F619 /* KSReachability.h */, 86 | 7C9D398422664CA10010F619 /* KSReachability.m */, 87 | ); 88 | path = "In-app Purchases"; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | 7C9D396822664B780010F619 /* Auto-renewing-subscriptions */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = 7C9D397B22664B7A0010F619 /* Build configuration list for PBXNativeTarget "Auto-renewing-subscriptions" */; 97 | buildPhases = ( 98 | 7C9D396522664B780010F619 /* Sources */, 99 | 7C9D396622664B780010F619 /* Frameworks */, 100 | 7C9D396722664B780010F619 /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = "Auto-renewing-subscriptions"; 107 | productName = "Auto-renewing-subscriptions"; 108 | productReference = 7C9D396922664B780010F619 /* Auto-renewing-subscriptions.app */; 109 | productType = "com.apple.product-type.application"; 110 | }; 111 | /* End PBXNativeTarget section */ 112 | 113 | /* Begin PBXProject section */ 114 | 7C9D396122664B780010F619 /* Project object */ = { 115 | isa = PBXProject; 116 | attributes = { 117 | LastSwiftUpdateCheck = 1020; 118 | LastUpgradeCheck = 1020; 119 | ORGANIZATIONNAME = "Lucid Software"; 120 | TargetAttributes = { 121 | 7C9D396822664B780010F619 = { 122 | CreatedOnToolsVersion = 10.2; 123 | LastSwiftMigration = 1020; 124 | }; 125 | }; 126 | }; 127 | buildConfigurationList = 7C9D396422664B780010F619 /* Build configuration list for PBXProject "Auto-renewing-subscriptions" */; 128 | compatibilityVersion = "Xcode 9.3"; 129 | developmentRegion = en; 130 | hasScannedForEncodings = 0; 131 | knownRegions = ( 132 | en, 133 | Base, 134 | ); 135 | mainGroup = 7C9D396022664B780010F619; 136 | productRefGroup = 7C9D396A22664B780010F619 /* Products */; 137 | projectDirPath = ""; 138 | projectRoot = ""; 139 | targets = ( 140 | 7C9D396822664B780010F619 /* Auto-renewing-subscriptions */, 141 | ); 142 | }; 143 | /* End PBXProject section */ 144 | 145 | /* Begin PBXResourcesBuildPhase section */ 146 | 7C9D396722664B780010F619 /* Resources */ = { 147 | isa = PBXResourcesBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | 7C9D397722664B7A0010F619 /* LaunchScreen.storyboard in Resources */, 151 | 7C9D397422664B790010F619 /* Assets.xcassets in Resources */, 152 | 7C9D397222664B780010F619 /* Main.storyboard in Resources */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXResourcesBuildPhase section */ 157 | 158 | /* Begin PBXSourcesBuildPhase section */ 159 | 7C9D396522664B780010F619 /* Sources */ = { 160 | isa = PBXSourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 7C9D398822664DB80010F619 /* DemoTransactionHandler.swift in Sources */, 164 | 7C9D396F22664B780010F619 /* ViewController.swift in Sources */, 165 | 7C9D398522664CA10010F619 /* KSReachability.m in Sources */, 166 | 7C9D396D22664B780010F619 /* AppDelegate.swift in Sources */, 167 | 7C9D398122664BE70010F619 /* Reachability.swift in Sources */, 168 | 7C9D397F22664BB60010F619 /* InAppPurchaseManager.swift in Sources */, 169 | ); 170 | runOnlyForDeploymentPostprocessing = 0; 171 | }; 172 | /* End PBXSourcesBuildPhase section */ 173 | 174 | /* Begin PBXVariantGroup section */ 175 | 7C9D397022664B780010F619 /* Main.storyboard */ = { 176 | isa = PBXVariantGroup; 177 | children = ( 178 | 7C9D397122664B780010F619 /* Base */, 179 | ); 180 | name = Main.storyboard; 181 | sourceTree = ""; 182 | }; 183 | 7C9D397522664B7A0010F619 /* LaunchScreen.storyboard */ = { 184 | isa = PBXVariantGroup; 185 | children = ( 186 | 7C9D397622664B7A0010F619 /* Base */, 187 | ); 188 | name = LaunchScreen.storyboard; 189 | sourceTree = ""; 190 | }; 191 | /* End PBXVariantGroup section */ 192 | 193 | /* Begin XCBuildConfiguration section */ 194 | 7C9D397922664B7A0010F619 /* Debug */ = { 195 | isa = XCBuildConfiguration; 196 | buildSettings = { 197 | ALWAYS_SEARCH_USER_PATHS = NO; 198 | CLANG_ANALYZER_NONNULL = YES; 199 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 200 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 201 | CLANG_CXX_LIBRARY = "libc++"; 202 | CLANG_ENABLE_MODULES = YES; 203 | CLANG_ENABLE_OBJC_ARC = YES; 204 | CLANG_ENABLE_OBJC_WEAK = YES; 205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 206 | CLANG_WARN_BOOL_CONVERSION = YES; 207 | CLANG_WARN_COMMA = YES; 208 | CLANG_WARN_CONSTANT_CONVERSION = YES; 209 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 211 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 212 | CLANG_WARN_EMPTY_BODY = YES; 213 | CLANG_WARN_ENUM_CONVERSION = YES; 214 | CLANG_WARN_INFINITE_RECURSION = YES; 215 | CLANG_WARN_INT_CONVERSION = YES; 216 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 218 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 220 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 221 | CLANG_WARN_STRICT_PROTOTYPES = YES; 222 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 223 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 224 | CLANG_WARN_UNREACHABLE_CODE = YES; 225 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 226 | CODE_SIGN_IDENTITY = "iPhone Developer"; 227 | COPY_PHASE_STRIP = NO; 228 | DEBUG_INFORMATION_FORMAT = dwarf; 229 | ENABLE_STRICT_OBJC_MSGSEND = YES; 230 | ENABLE_TESTABILITY = YES; 231 | GCC_C_LANGUAGE_STANDARD = gnu11; 232 | GCC_DYNAMIC_NO_PIC = NO; 233 | GCC_NO_COMMON_BLOCKS = YES; 234 | GCC_OPTIMIZATION_LEVEL = 0; 235 | GCC_PREPROCESSOR_DEFINITIONS = ( 236 | "DEBUG=1", 237 | "$(inherited)", 238 | ); 239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 241 | GCC_WARN_UNDECLARED_SELECTOR = YES; 242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 243 | GCC_WARN_UNUSED_FUNCTION = YES; 244 | GCC_WARN_UNUSED_VARIABLE = YES; 245 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 246 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 247 | MTL_FAST_MATH = YES; 248 | ONLY_ACTIVE_ARCH = YES; 249 | SDKROOT = iphoneos; 250 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 251 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 252 | }; 253 | name = Debug; 254 | }; 255 | 7C9D397A22664B7A0010F619 /* Release */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | ALWAYS_SEARCH_USER_PATHS = NO; 259 | CLANG_ANALYZER_NONNULL = YES; 260 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 261 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 262 | CLANG_CXX_LIBRARY = "libc++"; 263 | CLANG_ENABLE_MODULES = YES; 264 | CLANG_ENABLE_OBJC_ARC = YES; 265 | CLANG_ENABLE_OBJC_WEAK = YES; 266 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 267 | CLANG_WARN_BOOL_CONVERSION = YES; 268 | CLANG_WARN_COMMA = YES; 269 | CLANG_WARN_CONSTANT_CONVERSION = YES; 270 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 271 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 272 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 273 | CLANG_WARN_EMPTY_BODY = YES; 274 | CLANG_WARN_ENUM_CONVERSION = YES; 275 | CLANG_WARN_INFINITE_RECURSION = YES; 276 | CLANG_WARN_INT_CONVERSION = YES; 277 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 279 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 281 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 282 | CLANG_WARN_STRICT_PROTOTYPES = YES; 283 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 284 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 285 | CLANG_WARN_UNREACHABLE_CODE = YES; 286 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 287 | CODE_SIGN_IDENTITY = "iPhone Developer"; 288 | COPY_PHASE_STRIP = NO; 289 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 290 | ENABLE_NS_ASSERTIONS = NO; 291 | ENABLE_STRICT_OBJC_MSGSEND = YES; 292 | GCC_C_LANGUAGE_STANDARD = gnu11; 293 | GCC_NO_COMMON_BLOCKS = YES; 294 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 295 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 296 | GCC_WARN_UNDECLARED_SELECTOR = YES; 297 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 298 | GCC_WARN_UNUSED_FUNCTION = YES; 299 | GCC_WARN_UNUSED_VARIABLE = YES; 300 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 301 | MTL_ENABLE_DEBUG_INFO = NO; 302 | MTL_FAST_MATH = YES; 303 | SDKROOT = iphoneos; 304 | SWIFT_COMPILATION_MODE = wholemodule; 305 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 306 | VALIDATE_PRODUCT = YES; 307 | }; 308 | name = Release; 309 | }; 310 | 7C9D397C22664B7A0010F619 /* Debug */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 314 | CLANG_ENABLE_MODULES = YES; 315 | CODE_SIGN_STYLE = Automatic; 316 | DEVELOPMENT_TEAM = V6KTU5CWEW; 317 | INFOPLIST_FILE = "Auto-renewing-subscriptions/Info.plist"; 318 | LD_RUNPATH_SEARCH_PATHS = ( 319 | "$(inherited)", 320 | "@executable_path/Frameworks", 321 | ); 322 | PRODUCT_BUNDLE_IDENTIFIER = "com.demo.Auto-renewing-subscriptions"; 323 | PRODUCT_NAME = "$(TARGET_NAME)"; 324 | SWIFT_OBJC_BRIDGING_HEADER = "Auto-renewing-subscriptions/Auto-renewing-subscriptions-Bridging-Header.h"; 325 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 326 | SWIFT_VERSION = 5.0; 327 | TARGETED_DEVICE_FAMILY = "1,2"; 328 | }; 329 | name = Debug; 330 | }; 331 | 7C9D397D22664B7A0010F619 /* Release */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 335 | CLANG_ENABLE_MODULES = YES; 336 | CODE_SIGN_STYLE = Automatic; 337 | DEVELOPMENT_TEAM = V6KTU5CWEW; 338 | INFOPLIST_FILE = "Auto-renewing-subscriptions/Info.plist"; 339 | LD_RUNPATH_SEARCH_PATHS = ( 340 | "$(inherited)", 341 | "@executable_path/Frameworks", 342 | ); 343 | PRODUCT_BUNDLE_IDENTIFIER = "com.demo.Auto-renewing-subscriptions"; 344 | PRODUCT_NAME = "$(TARGET_NAME)"; 345 | SWIFT_OBJC_BRIDGING_HEADER = "Auto-renewing-subscriptions/Auto-renewing-subscriptions-Bridging-Header.h"; 346 | SWIFT_VERSION = 5.0; 347 | TARGETED_DEVICE_FAMILY = "1,2"; 348 | }; 349 | name = Release; 350 | }; 351 | /* End XCBuildConfiguration section */ 352 | 353 | /* Begin XCConfigurationList section */ 354 | 7C9D396422664B780010F619 /* Build configuration list for PBXProject "Auto-renewing-subscriptions" */ = { 355 | isa = XCConfigurationList; 356 | buildConfigurations = ( 357 | 7C9D397922664B7A0010F619 /* Debug */, 358 | 7C9D397A22664B7A0010F619 /* Release */, 359 | ); 360 | defaultConfigurationIsVisible = 0; 361 | defaultConfigurationName = Release; 362 | }; 363 | 7C9D397B22664B7A0010F619 /* Build configuration list for PBXNativeTarget "Auto-renewing-subscriptions" */ = { 364 | isa = XCConfigurationList; 365 | buildConfigurations = ( 366 | 7C9D397C22664B7A0010F619 /* Debug */, 367 | 7C9D397D22664B7A0010F619 /* Release */, 368 | ); 369 | defaultConfigurationIsVisible = 0; 370 | defaultConfigurationName = Release; 371 | }; 372 | /* End XCConfigurationList section */ 373 | }; 374 | rootObject = 7C9D396122664B780010F619 /* Project object */; 375 | } 376 | --------------------------------------------------------------------------------