├── .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 | 
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 |
--------------------------------------------------------------------------------