diff --git a/ios-template.xcodeproj/project.pbxproj b/ios-template.xcodeproj/project.pbxproj index c05e0be..d3da60f 100644 --- a/ios-template.xcodeproj/project.pbxproj +++ b/ios-template.xcodeproj/project.pbxproj @@ -33,6 +33,17 @@ D5FF445026A80FFB00CF155C /* KeychainPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF444F26A80FFB00CF155C /* KeychainPasswordItem.swift */; }; D5FF445426A948F600CF155C /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5FF445326A948F500CF155C /* StoreKit.framework */; }; D5FF445726A94B8500CF155C /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = D5FF445626A94B8500CF155C /* SwiftyJSON */; }; + D5FF445B26A9561000CF155C /* DYFStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF445A26A9561000CF155C /* DYFStore.swift */; }; + D5FF445D26A9563400CF155C /* DYFStoreConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF445C26A9563400CF155C /* DYFStoreConverter.swift */; }; + D5FF445F26A9565C00CF155C /* DYFStoreKeychainPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF445E26A9565C00CF155C /* DYFStoreKeychainPersistence.swift */; }; + D5FF446126A9567300CF155C /* DYFStoreReceiptVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446026A9567300CF155C /* DYFStoreReceiptVerifier.swift */; }; + D5FF446326A9568A00CF155C /* DYFStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446226A9568A00CF155C /* DYFStoreTransaction.swift */; }; + D5FF446526A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446426A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift */; }; + D5FF446726A957A900CF155C /* DYFSwiftRuntimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446626A957A900CF155C /* DYFSwiftRuntimeProvider.swift */; }; + D5FF446926A959A600CF155C /* ZExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446826A959A600CF155C /* ZExtensions.swift */; }; + D5FF446B26A959FB00CF155C /* ZLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446A26A959FB00CF155C /* ZLoadingView.swift */; }; + D5FF446D26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446C26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift */; }; + D5FF447026A95AFB00CF155C /* ZStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446F26A95AFB00CF155C /* ZStoreManager.swift */; }; ED50D6502436EB69005B9E1E /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED50D64F2436EB69005B9E1E /* CoreMedia.framework */; }; ED50D6522436EB70005B9E1E /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED50D6512436EB70005B9E1E /* AVKit.framework */; }; ED9FBC2E21830D2A005A5DF0 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED9FBC2D21830D2A005A5DF0 /* UIKit.framework */; }; @@ -74,6 +85,17 @@ D5FF444F26A80FFB00CF155C /* KeychainPasswordItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainPasswordItem.swift; sourceTree = ""; }; D5FF445226A832BE00CF155C /* 武极天下.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = "武极天下.entitlements"; path = "../武极天下.entitlements"; sourceTree = ""; }; D5FF445326A948F500CF155C /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + D5FF445A26A9561000CF155C /* DYFStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStore.swift; sourceTree = ""; }; + D5FF445C26A9563400CF155C /* DYFStoreConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreConverter.swift; sourceTree = ""; }; + D5FF445E26A9565C00CF155C /* DYFStoreKeychainPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreKeychainPersistence.swift; sourceTree = ""; }; + D5FF446026A9567300CF155C /* DYFStoreReceiptVerifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreReceiptVerifier.swift; sourceTree = ""; }; + D5FF446226A9568A00CF155C /* DYFStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreTransaction.swift; sourceTree = ""; }; + D5FF446426A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreUserDefaultsPersistence.swift; sourceTree = ""; }; + D5FF446626A957A900CF155C /* DYFSwiftRuntimeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFSwiftRuntimeProvider.swift; sourceTree = ""; }; + D5FF446826A959A600CF155C /* ZExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZExtensions.swift; sourceTree = ""; }; + D5FF446A26A959FB00CF155C /* ZLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZLoadingView.swift; sourceTree = ""; }; + D5FF446C26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZIndefiniteAnimatedSpinner.swift; sourceTree = ""; }; + D5FF446F26A95AFB00CF155C /* ZStoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZStoreManager.swift; sourceTree = ""; }; ED50D64F2436EB69005B9E1E /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; ED50D6512436EB70005B9E1E /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; }; ED9FBC2D21830D2A005A5DF0 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; @@ -146,11 +168,12 @@ 306A7A8F200C83F500E1EBB6 /* ios-template */ = { isa = PBXGroup; children = ( + D5FF445926A955F200CF155C /* DYFStore */, D5FF445226A832BE00CF155C /* 武极天下.entitlements */, EDF3B7E424580A4B009DFBBD /* LaunchScreen.storyboard */, - D5FF444D26A80FF600CF155C /* IDUtil.swift */, - D5FF444F26A80FFB00CF155C /* KeychainPasswordItem.swift */, + D5FF445826A955DA00CF155C /* utils */, D5FF442E26A80E5F00CF155C /* AppDelegate.swift */, + D5FF446E26A95A9200CF155C /* common */, 306A7A96200C83F500E1EBB6 /* ViewController.h */, D5FF443026A80F8600CF155C /* ViewController.swift */, 306A7A97200C83F500E1EBB6 /* ViewController.mm */, @@ -208,6 +231,40 @@ path = doc; sourceTree = ""; }; + D5FF445826A955DA00CF155C /* utils */ = { + isa = PBXGroup; + children = ( + D5FF444D26A80FF600CF155C /* IDUtil.swift */, + ); + name = utils; + sourceTree = ""; + }; + D5FF445926A955F200CF155C /* DYFStore */ = { + isa = PBXGroup; + children = ( + D5FF445A26A9561000CF155C /* DYFStore.swift */, + D5FF445C26A9563400CF155C /* DYFStoreConverter.swift */, + D5FF445E26A9565C00CF155C /* DYFStoreKeychainPersistence.swift */, + D5FF446026A9567300CF155C /* DYFStoreReceiptVerifier.swift */, + D5FF446226A9568A00CF155C /* DYFStoreTransaction.swift */, + D5FF446626A957A900CF155C /* DYFSwiftRuntimeProvider.swift */, + D5FF446426A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift */, + ); + path = DYFStore; + sourceTree = ""; + }; + D5FF446E26A95A9200CF155C /* common */ = { + isa = PBXGroup; + children = ( + D5FF446826A959A600CF155C /* ZExtensions.swift */, + D5FF444F26A80FFB00CF155C /* KeychainPasswordItem.swift */, + D5FF446C26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift */, + D5FF446A26A959FB00CF155C /* ZLoadingView.swift */, + D5FF446F26A95AFB00CF155C /* ZStoreManager.swift */, + ); + path = common; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -288,11 +345,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D5FF447026A95AFB00CF155C /* ZStoreManager.swift in Sources */, D5FF445026A80FFB00CF155C /* KeychainPasswordItem.swift in Sources */, D5FF443126A80F8600CF155C /* ViewController.swift in Sources */, + D5FF446B26A959FB00CF155C /* ZLoadingView.swift in Sources */, + D5FF445D26A9563400CF155C /* DYFStoreConverter.swift in Sources */, D5FF442F26A80E5F00CF155C /* AppDelegate.swift in Sources */, + D5FF446326A9568A00CF155C /* DYFStoreTransaction.swift in Sources */, + D5FF446926A959A600CF155C /* ZExtensions.swift in Sources */, + D5FF446126A9567300CF155C /* DYFStoreReceiptVerifier.swift in Sources */, D5FF444E26A80FF600CF155C /* IDUtil.swift in Sources */, 306A7A98200C83F500E1EBB6 /* ViewController.mm in Sources */, + D5FF445B26A9561000CF155C /* DYFStore.swift in Sources */, + D5FF446D26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift in Sources */, + D5FF446526A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift in Sources */, + D5FF445F26A9565C00CF155C /* DYFStoreKeychainPersistence.swift in Sources */, + D5FF446726A957A900CF155C /* DYFSwiftRuntimeProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios-template.xcodeproj/project.xcworkspace/xcuserdata/zhl.xcuserdatad/UserInterfaceState.xcuserstate b/ios-template.xcodeproj/project.xcworkspace/xcuserdata/zhl.xcuserdatad/UserInterfaceState.xcuserstate index 994c9cc..9cb8b79 100644 Binary files a/ios-template.xcodeproj/project.xcworkspace/xcuserdata/zhl.xcuserdatad/UserInterfaceState.xcuserstate and b/ios-template.xcodeproj/project.xcworkspace/xcuserdata/zhl.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios-template/AppDelegate.swift b/ios-template/AppDelegate.swift index 1c06b61..f9bc8c9 100644 --- a/ios-template/AppDelegate.swift +++ b/ios-template/AppDelegate.swift @@ -8,9 +8,11 @@ import UIKit import Foundation +import StoreKit @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { +class AppDelegate: UIResponder, UIApplicationDelegate, DYFStoreAppStorePaymentDelegate { + var window: UIWindow? var _viewController: UIViewController? var _imageView: UIImageView? @@ -34,6 +36,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } self.setExternalInterfaces() + // Wether to allow the logs output to console. + DYFStore.default.enableLog = true + // If more than one transaction observer is attached to the payment queue, no guarantees are made as to the order they will be called in. It is recommended that you use a single observer to process and finish the transaction. + DYFStore.default.addPaymentTransactionObserver() + + // Sets the delegate processes the purchase which was initiated by user from the App Store. + DYFStore.default.delegate = self + + let networkState:String? = _native.getNetworkState(); if (networkState == "NotReachable") { print(">>>>>>>>>>>>>>>>>>>>no network") @@ -118,10 +129,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _imageView?.frame = _viewController!.view.frame; _viewController!.view.addSubview(_imageView!); _viewController!.view.bringSubviewToFront(_imageView!); + self.showLoading("加载中") } func hideLoadingView(){ _imageView?.removeFromSuperview(); + self.hideLoading() } + // 处理用户从应用商店发起的购买 + func didReceiveAppStorePurchaseRequest(_ queue: SKPaymentQueue, payment: SKPayment, forProduct product: SKProduct) { + + if !DYFStore.canMakePayments() { + self.showTipsMessage("Your device is not able or allowed to make payments!") + return + } + + // Get account name from your own user system. + let accountName = "Handsome Jon" + + // This algorithm is negotiated with server developer. + let userIdentifier = Z_SHA256_HashValue(accountName) ?? "" + DYFStoreLog("userIdentifier: \(userIdentifier)") + + ZStoreManager.shared.addPayment(product.productIdentifier, userIdentifier: userIdentifier) + } } diff --git a/ios-template/DYFStore/DYFStore.swift b/ios-template/DYFStore/DYFStore.swift new file mode 100644 index 0000000..3e41805 --- /dev/null +++ b/ios-template/DYFStore/DYFStore.swift @@ -0,0 +1,1297 @@ +// +// DYFStore.swift +// 武极天下 +// +// Created by zhl on 2021/7/22. +// Copyright © 2021 egret. All rights reserved. +// + +import Foundation +import StoreKit + +/// A StoreKit wrapper for in-app purchase. +open class DYFStore: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver { + + /// Whether to enable log. The default is false. + public var enableLog: Bool = false + + /// The valid products that were available for sale in the App Store. + public var availableProducts: NSMutableArray! + /// The product identifiers were invalid. + public var invalidIdentifiers: NSMutableArray! + + /// Records those transcations that have been purchased. + public var purchasedTranscations: NSMutableArray! + /// Records those transcations that have been restored. + public var restoredTranscations: NSMutableArray! + + /// The delegate processes the purchase which was initiated by user from the App Store. + public weak var delegate: DYFStoreAppStorePaymentDelegate? + + /// The keychain persister that supervises the `DYFStoreTransaction` transactions. + ///public var keychainPersister: DYFStoreKeychainPersistence? + + /// Returns a store singleton. + public static let `default` = DYFStore() + + /// Constructs a store singleton with class method. + /// + /// - Returns: A store singleton. + public class func defaultStore() -> DYFStore { + return DYFStore.self.default + } + + /// A struct named "Inner". + // private struct Inner { + // static var instance: DYFStore? = nil + // } + + /// Returns a store singleton. + /// + /// DispatchQueue.once(token: "com.storekit.DYFStore") { + /// if Inner.instance == nil { + /// Inner.instance = DYFStore() + /// } + /// } + /// return instance + /// + // public class var `default`: DYFStore { + // + // objc_sync_enter(self) + // defer { objc_sync_exit(self) } + // + // guard let instance = Inner.instance else { + // let store = DYFStore() + // Inner.instance = store + // return store + // } + // + // return instance + // } + + /// Overrides default constructor. + private override init() { + super.init() + + self.availableProducts = NSMutableArray(capacity: 0) + self.invalidIdentifiers = NSMutableArray(capacity: 0) + + self.purchasedTranscations = NSMutableArray(capacity: 0) + self.restoredTranscations = NSMutableArray(capacity: 0) + } + + /// deinit + deinit { + removePaymentTransactionObserver() + } + + /// Make sure the class has only one instance. + open override func copy() -> Any { + return self + } + + /// Make sure the class has only one instance. + open override func mutableCopy() -> Any { + return self + } + + // MARK: - StoreKit Wrapper + + /// Adds an observer to the payment queue. This must be invoked after the app has finished launching. + public func addPaymentTransactionObserver() { + SKPaymentQueue.default().add(self) + } + + /// Removes an observer from the payment queue. + private func removePaymentTransactionObserver() { + SKPaymentQueue.default().remove(self) + } + + /// Whether the user is allowed to make payments. + /// + /// - Returns: NO if this device is not able or allowed to make payments. + public class func canMakePayments() -> Bool { + return SKPaymentQueue.canMakePayments() + } + + /// Accepts the response from the App Store that contains the requested product information. + private var productsRequestDidFinish: (([SKProduct], [String]) -> Void)? + /// Tells the user that the request failed to execute. + private var productsRequestDidFail: ((NSError) -> Void)? + + /// An object that can retrieve localized information from the App Store about a specified list of products. + private var productsRequest: SKProductsRequest? + + /// Requests localized information about a product from the Apple App Store. `success` will be called if the products request is successful, `failure` if it isn't. + /// + /// - Parameters: + /// - id: The product identifier for the product you wish to retrieve information of. + /// - success: The closure to be called if the products request is sucessful. Can be `nil`. It takes two parameters: `products`, an array of SKProducts, one product for each valid product identifier provided in the original request, and `invalidProductIdentifiers`, an array of product identifiers that were not recognized by the App Store. + /// - failure: The closure to be called if the products request fails. Can be `nil`. + public func requestProduct(withIdentifier id: String?, success: @escaping ([SKProduct], [String]) -> Void, failure: @escaping (NSError) -> Void) { + + guard let identifier = id, !identifier.isEmpty else { + + self.productsRequestDidFail = failure + + DYFStoreLog("This product identifier is null or empty") + + let errDesc = NSLocalizedString("This product identifier is null or empty", tableName: "DYFStore", comment: "Error description") + let userInfo = [NSLocalizedDescriptionKey: errDesc] + let error = NSError(domain: DYFStoreError.domain, + code: DYFStoreError.invalidParameter.rawValue, + userInfo: userInfo) + + self.productsRequestDidFail?(error) + + return + } + + DYFStoreLog() + + self.requestProduct(withIdentifiers: [identifier], success: success, failure: failure) + } + + /// Requests localized information about a set of products from the Apple App Store. `success` will be called if the products request is successful, `failure` if it isn't. + /// + /// - Parameters: + /// - ids: The array of product identifiers for the products you wish to retrieve information of. + /// - success: The closure to be called if the products request is sucessful. Can be `nil`. It takes two parameters: `products`, an array of SKProducts, one product for each valid product identifier provided in the original request, and `invalidProductIdentifiers`, an array of product identifiers that were not recognized by the App Store. + /// - failure: The closure to be called if the products request fails. Can be `nil`. + public func requestProduct(withIdentifiers ids: Array?, success: @escaping ([SKProduct], [String]) -> Void, failure: @escaping (NSError) -> Void) { + + guard let identifiers = ids, !identifiers.isEmpty else { + + self.productsRequestDidFail = failure + + DYFStoreLog("An array of product identifiers is null or empty") + + let errorDesc = NSLocalizedString("An array of product identifiers is null or empty", tableName: "DYFStore", comment: "Error description") + let userInfo = [NSLocalizedDescriptionKey: errorDesc] + let error = NSError(domain: DYFStoreError.domain, + code: DYFStoreError.invalidParameter.rawValue, + userInfo: userInfo) + + self.productsRequestDidFail?(error) + + return + } + + DYFStoreLog("product identifiers: \(identifiers)") + + if self.productsRequest == nil { + + self.productsRequestDidFinish = success + self.productsRequestDidFail = failure + + let setOfProductId = Set(identifiers) + // Creates a product request object and initialize it with our product identifiers. + self.productsRequest = SKProductsRequest(productIdentifiers: setOfProductId) + self.productsRequest?.delegate = self; + // Sends the request to the App Store. + self.productsRequest?.start() + } + } + + // MARK: - Product management + + /// Whether the product is contained in the list of available products. + /// + /// - Parameter product: An `SKProduct` object. + /// - Returns: True if it is contained, otherwise, false. + public func containsProduct(_ product: SKProduct) -> Bool { + var shouldContain: Bool = false + + for e in self.availableProducts { + let aProduct = e as! SKProduct + let id = aProduct.productIdentifier + + if id == product.productIdentifier { + shouldContain = true + break + } + } + + return shouldContain + } + + /// Fetches the product by matching a given product identifier. + /// + /// - Parameter productIdentifier: A given product identifier. + /// - Returns: An `SKProduct` object. + public func product(forIdentifier productIdentifier: String) -> SKProduct? { + var product: SKProduct? = nil + + for e in self.availableProducts { + let aProduct = e as! SKProduct + let id = aProduct.productIdentifier + + if id == productIdentifier { + product = aProduct + break + } + } + + return product + } + + /// Fetches the localized price of a given product. + /// + /// - Parameter product: A given product. + /// - Returns: The localized price of a given product. + public func localizedPrice(ofProduct product: SKProduct?) -> String? { + + if let p = product { + + let numberFormatter = NumberFormatter() + numberFormatter.formatterBehavior = NumberFormatter.Behavior.behavior10_4 + numberFormatter.numberStyle = NumberFormatter.Style.currency + numberFormatter.locale = product!.priceLocale + + let formattedString = numberFormatter.string(from: p.price) + + return formattedString + } + + return nil + } + + // MARK: - SKProductsRequestDelegate + + // Accepts the response from the App Store that contains the requested product information. + public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + DYFStoreLog("products request received response") + + /// The array contains products whose identifiers have been recognized by the App Store. + let products: [SKProduct] = response.products + /// The array contains all product identifiers have not been recognized by the App Store. + let invalidProductIdentifiers = response.invalidProductIdentifiers + + for product in products { + DYFStoreLog("received product with id: \(product.productIdentifier)") + + if !self.containsProduct(product) { + self.availableProducts.add(product) + } + } + + for (idx, value) in invalidProductIdentifiers.enumerated() { + DYFStoreLog("invalid product with id: \(value), index: \(idx)") + + if !self.invalidIdentifiers.contains(value) { + self.invalidIdentifiers.add(value) + } + } + + DispatchQueue.main.async { + self.productsRequestDidFinish?(products, invalidProductIdentifiers) + } + } + + // MARK: - SKRequestDelegate + + // Tells the delegate that the request has completed. When this method is called, your delegate receives no further communication from the request and can release it. + public func requestDidFinish(_ request: SKRequest) { + + if let req = self.productsRequest, req === request { + + DYFStoreLog("products request finished") + + self.productsRequest = nil + + } else if let req = self.refreshReceiptRequest, req === request { + + DYFStoreLog("refresh receipt finished") + + DispatchQueue.main.async { + self.refreshReceiptSuccessBlock?() + } + + self.refreshReceiptRequest = nil + } + } + + // Tells the delegate that the request failed to execute. The requestDidFinish(_:) method is not called after this method is called. + public func request(_ request: SKRequest, didFailWithError error: Error) { + + let err = error as NSError + + if let req = self.productsRequest, req === request { + + // Prints the cause of the product request failure. + DYFStoreLog("products request failed with error: \(err.code), \(err.localizedDescription)") + + DispatchQueue.main.async { + self.productsRequestDidFail?(err) + } + + self.productsRequest = nil + + } else if let req = self.refreshReceiptRequest, req === request { + + DYFStoreLog("refresh receipt failed with error: \(err.code), \(err.localizedDescription)") + + DispatchQueue.main.async { + self.refreshReceiptFailureBlock?(err) + } + + self.refreshReceiptRequest = nil + } + } + + // MARK: - Posts Notification + + /// Creates a notification with a given name and sender and posts it to the notification center. + /// + /// - Parameters: + /// - name: The name of the notification. The default is DYFStore.purchasedNotification. + /// - info: The `DYFStore.NotificationInfo` object posting the notification. + fileprivate func postNotification(withName name: Notification.Name = DYFStore.purchasedNotification, _ info: DYFStore.NotificationInfo) { + NotificationCenter.default.post(name: name, object: info) + } + + // MARK: - Purchases Product + + /// Whether there are purchases. + /// + /// - Returns: YES if it contains some items and NO, otherwise. + public func hasPurchasedTransactions() -> Bool { + return self.purchasedTranscations.count > 0 + } + + /// Whether there are restored purchases. + /// + /// - Returns: YES if it contains some items and NO, otherwise. + public func hasRestoredTransactions() -> Bool { + return self.restoredTranscations.count > 0 + } + + /// Extracts a purchased transaction with a given transaction identifier. + /// + /// - Parameter transactionIdentifier: The unique server-provided identifier. + /// - Returns: A purchased `SKPaymentTransaction` object. + public func extractPurchasedTransaction(_ transactionIdentifier: String?) -> SKPaymentTransaction? { + + var transaction: SKPaymentTransaction? = nil + + guard let transactionId = transactionIdentifier, !transactionId.isEmpty else { + return transaction + } + + self.purchasedTranscations.enumerateObjects { (obj: Any, idx: Int, stop: UnsafeMutablePointer) in + + let tempTransaction = obj as! SKPaymentTransaction + + let id = tempTransaction.transactionIdentifier ?? "" + + DYFStoreLog("index: \(idx), transactionId: \(id)") + + if id == transactionId { + transaction = tempTransaction + } + } + + return transaction + } + + /// Extracts a restored transaction with a given transaction identifier. + /// + /// - Parameter transactionIdentifier: The unique server-provided identifier. + /// - Returns: A restored `SKPaymentTransaction` object. + public func extractRestoredTransaction(_ transactionIdentifier: String?) -> SKPaymentTransaction? { + + var transaction: SKPaymentTransaction? = nil + + guard let transactionId = transactionIdentifier, !transactionId.isEmpty else { + return transaction + } + + self.restoredTranscations.enumerateObjects { (obj: Any, idx: Int, stop: UnsafeMutablePointer) in + + let tempTransaction = obj as! SKPaymentTransaction + + let id = tempTransaction.transactionIdentifier ?? "" + let originalId = tempTransaction.original?.transactionIdentifier ?? "" + + DYFStoreLog("index: \(idx), transactionId: \(id), originalTransactionId: \(originalId)") + + if id == transactionId { + transaction = tempTransaction + } + } + + return transaction + } + + /// The number of items the user wants to purchase. It must be greater than 0, the default value is 1. + public private(set) var quantity: Int = 1 + + /// Requests payment of the product with the given product identifier, an opaque identifier for the user’s account on your system and the number of items the user wants to purchase. + /// + /// - Parameters: + /// - productIdentifier: The identifier of the product whose payment will be requested. + /// - userIdentifier: An opaque identifier for the user’s account on your system. The recommended implementation is to use a one-way hash of the user’s account name to calculate the value for this property. + /// - quantity: The number of items the user wants to purchase. The default value is 1. + public func purchaseProduct(_ productIdentifier: String?, userIdentifier: String? = nil, quantity: Int = 1) { + + guard let identifier = productIdentifier, !identifier.isEmpty else { + + DYFStoreLog("The given product identifier is null or empty") + + let errDesc = NSLocalizedString("The given product identifier is null or empty", tableName: "DYFStore", comment: "Error description") + let userInfo = [NSLocalizedDescriptionKey: errDesc] + let error = NSError(domain: DYFStoreError.domain, + code: DYFStoreError.invalidParameter.rawValue, + userInfo: userInfo) + + var info = DYFStore.NotificationInfo() + info.state = DYFStore.PurchaseState.failed + info.error = error + self.postNotification(withName: DYFStore.purchasedNotification, info) + + return + } + + if let product = self.product(forIdentifier: identifier) { + + DYFStoreLog("productIdentifier: \(identifier), quantity: \(quantity)") + + self.quantity = quantity + + let payment = SKMutablePayment(product: product) + payment.quantity = quantity + if #available(iOS 7.0, *) { + payment.applicationUsername = userIdentifier + } + SKPaymentQueue.default().add(payment) + + } else { + + DYFStoreLog("Unknown product identifier: \(identifier)") + + let errDesc = NSLocalizedString("Unknown product identifier", tableName: "DYFStore", comment: "Error description") + let userInfo = [NSLocalizedDescriptionKey: errDesc] + let error = NSError(domain: DYFStoreError.domain, + code: DYFStoreError.unknownProductIdentifier.rawValue, + userInfo: userInfo) + + var info = DYFStore.NotificationInfo() + info.state = DYFStore.PurchaseState.failed + info.productIdentifier = identifier + info.error = error + self.postNotification(withName: DYFStore.purchasedNotification, info) + } + } + + /// Requests to restore previously completed purchases that refer to auto-renewable subscriptions, free subscriptions or non-expendable items. + /// + /// The usage scenes are as follows: + /// The apple users log in to other devices and install app. + /// The app corresponding to in-app purchase has been uninstalled and reinstalled. + /// + /// - Parameter userIdentifier: An opaque identifier for the user’s account on your system. + public func restoreTransactions(userIdentifier: String? = nil) { + self.restoredTranscations = NSMutableArray(capacity: 0) + + if let identifier = userIdentifier, !identifier.isEmpty { + + assert(SKPaymentQueue.default().responds(to: #selector(SKPaymentQueue.restoreCompletedTransactions(withApplicationUsername:))), "restoreCompletedTransactions(withApplicationUsername:) not supported in this iOS version. Use restoreCompletedTransactions() instead.") + + if #available(iOS 7.0, *) { + SKPaymentQueue.default().restoreCompletedTransactions(withApplicationUsername: identifier) + } else { + SKPaymentQueue.default().restoreCompletedTransactions() + } + + } else { + + SKPaymentQueue.default().restoreCompletedTransactions() + } + } + + /// Completes a pending transaction. + /// + /// Your application should call this method from a transaction observer that received a notification from the payment queue. Calling finishTransaction(_:) on a transaction removes it from the queue. Your application should call finishTransaction(_:) only after it has successfully processed the transaction and unlocked the functionality purchased by the user. + /// Calling finishTransaction(_:) on a transaction that is in the SKPaymentTransactionState.purchasing state throws an exception. + /// + /// - Parameter transaction: The transaction to finish. + public func finishTransaction(_ transaction: SKPaymentTransaction?) { + if let tx = transaction { + DYFStoreLog("transactionIdentifier: \(tx.transactionIdentifier ?? "")") + SKPaymentQueue.default().finishTransaction(tx) + } + } + + // MARK: - Receipt + + /// Fetches the url of the bundle’s App Store receipt, or nil if the receipt is missing. + /// If this method returns `nil` you should refresh the receipt by calling `refreshReceipt`. + /// + /// - Returns: The url of the bundle’s App Store receipt. + public class func receiptURL() -> URL? { + // The general best practice of weak linking using the respondsToSelector: method cannot be used here. Prior to iOS 7, the method was implemented as private API, but that implementation called the doesNotRecognizeSelector: method. + assert(floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1, "appStoreReceiptURL not supported in this iOS version.") + let receiptURL = Bundle.main.appStoreReceiptURL + return receiptURL + } + + /// The block to be called if the refresh receipt request is sucessful. + private var refreshReceiptSuccessBlock: (() -> Void)? + /// The block to be called if the refresh receipt request fails. + private var refreshReceiptFailureBlock: ((NSError) -> Void)? + + /// A request to refresh the receipt, which represents the user's transactions with your app. + private var refreshReceiptRequest: SKReceiptRefreshRequest? + + /// Requests to refresh the App Store receipt in case the receipt is invalid or missing. `successBlock` will be called if the refresh receipt request is successful, `failureBlock` if it isn't. + /// + /// - Parameters: + /// - successBlock: The block to be called if the refresh receipt request is sucessful. Can be `nil`. + /// - failureBlock: The block to be called if the refresh receipt request fails. Can be `nil`. + public func refreshReceipt(onSuccess successBlock: @escaping () -> Void, failure failureBlock: @escaping (NSError) -> Void) { + + if self.refreshReceiptRequest == nil { + + self.refreshReceiptSuccessBlock = successBlock + self.refreshReceiptFailureBlock = failureBlock + + self.refreshReceiptRequest = SKReceiptRefreshRequest(receiptProperties: [:]) + self.refreshReceiptRequest?.delegate = self + self.refreshReceiptRequest?.start() + } + } + + // MARK: - SKPaymentTransactionObserver + + // Tells an observer that one or more transactions have been updated. + public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + + for transaction in transactions { + + switch transaction.transactionState { + case .purchasing: + self.purchasingTransaction(transaction, queue: queue) + break + case .purchased: + self.didPurchaseTransaction(transaction, queue: queue) + break + case .failed: + self.didFailWithTransaction(transaction, queue: queue, error: transaction.error! as NSError) + break + case .restored: + self.didRestoreTransaction(transaction, queue: queue) + break + case .deferred: + self.didDeferTransaction(transaction, queue: queue) + break + @unknown default: + DYFStoreLog("Unknown transaction state") + break + } + + } + } + + // Tells the observer that the payment queue has updated one or more download objects. + public func paymentQueue(_ queue: SKPaymentQueue, updatedDownloads downloads: [SKDownload]) { + + for download in downloads { + + let state = self.state(forDownload: download) + switch state { + + case .waiting: + DYFStoreLog("The download is inactive, waiting to be downloaded") + //queue.start([download]) + break + case .active: + self.didUpdateDownload(download, queue: queue) + break + case .paused: + self.didPauseDownload(download, queue: queue) + break + case .finished: + self.didFinishDownload(download, queue: queue) + break + case .failed: + self.didFailWithDownload(download, queue: queue) + break + case .cancelled: + self.didCancelDownload(download, queue: queue) + break + } + + } + } + + // Tells the observer that the payment queue has finished sending restored transactions. + public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { + DYFStoreLog("The payment queue has finished sending restored transactions") + } + + // Tells the observer that an error occurred while restoring transactions. + public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { + let err = error as NSError + + DYFStoreLog("The restored transactions failed with error: \(err.code), \(err.localizedDescription)") + + var info = DYFStore.NotificationInfo() + + // The user cancels the purchase. + if err.code == SKError.paymentCancelled.rawValue { + info.state = DYFStore.PurchaseState.cancelled + } else { + info.state = DYFStore.PurchaseState.restoreFailed + } + + info.error = err + + self.postNotification(info) + } + + // Tells an observer that one or more transactions have been removed from the queue. + public func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) { + + for transaction in transactions { + // Logs all transactions that have been removed from the payment queue. + let productId = transaction.payment.productIdentifier + DYFStoreLog("\(productId) has been removed from the payment queue") + } + } + + // Tells the observer that a user initiated an in-app purchase from the App Store. + public func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool { + + if #available(iOS 11.0, *) { + + if !containsProduct(product) { + self.availableProducts.add(product) + } + + self.delegate?.didReceiveAppStorePurchaseRequest(queue, payment: payment, forProduct: product) + + } else { /* Fallback on earlier versions. Never execute. */ } + + return false + } + + // MARK: - Process Transaction + + /// The transaction is being processed by the App Store. + /// + /// - Parameters: + /// - transaction: An `SKPaymentTransaction` object in the payment queue. + /// - queue: The payment queue that updated the transactions. + private func purchasingTransaction(_ transaction: SKPaymentTransaction, queue: SKPaymentQueue) { + DYFStoreLog("The transaction is purchasing") + + var info = DYFStore.NotificationInfo() + info.state = DYFStore.PurchaseState.purchasing + self.postNotification(info) + } + + /// The App Store successfully processed payment. Your application should provide the content the user purchased. + /// + /// - Parameters: + /// - transaction: An `SKPaymentTransaction` object in the payment queue. + /// - queue: The payment queue that updated the transactions. + private func didPurchaseTransaction(_ transaction: SKPaymentTransaction, queue: SKPaymentQueue) { + DYFStoreLog("The transaction purchased. Deliver the content for \(transaction.payment.productIdentifier)") + + self.purchasedTranscations.add(transaction) + // Checks whether the purchased product has content hosted with Apple. + if transaction.downloads.count > 0 { + + // Starts the download process and send a DYFStoreDownload.State.started notification. + queue.start(transaction.downloads) + + var info = DYFStore.NotificationInfo() + info.downloadState = DYFStoreDownload.State.started + self.postNotification(withName: DYFStore.downloadedNotification, info) + + } else { + + self.didFinishTransaction(transaction, queue: queue, forState: DYFStore.PurchaseState.succeeded) + } + } + + /// The transaction failed. Check the error property to determine what happened. + /// + /// - Parameters: + /// - transaction: An `SKPaymentTransaction` object in the payment queue. + /// - queue: The payment queue that updated the transactions. + /// - error: An object describing the error that occurred while processing the transaction. + private func didFailWithTransaction(_ transaction: SKPaymentTransaction, queue: SKPaymentQueue, error: NSError) { + + DYFStoreLog("The transaction failed with product(\(transaction.payment.productIdentifier)) and error(\(error.debugDescription))") + + var info = DYFStore.NotificationInfo() + + // The user cancels the purchase. + if error.code == SKError.paymentCancelled.rawValue { + info.state = DYFStore.PurchaseState.cancelled + } else { + info.state = DYFStore.PurchaseState.failed + } + + info.error = error + info.productIdentifier = transaction.payment.productIdentifier + + self.postNotification(info) + self.finishTransaction(transaction) + } + + /// This transaction restores content previously purchased by the user. Read the original property to obtain information about the original purchase. + /// + /// - Parameters: + /// - transaction: An `SKPaymentTransaction` object in the payment queue. + /// - queue: The payment queue that updated the transactions. + private func didRestoreTransaction(_ transaction: SKPaymentTransaction, queue: SKPaymentQueue) { + + DYFStoreLog("The transaction restored. Restore the content for \(transaction.payment.productIdentifier)") + + self.restoredTranscations.add(transaction) + // Sends a DYFStoreDownload.State.started notification if it has. + if transaction.downloads.count > 0 { + + queue.start(transaction.downloads) + + var info = DYFStore.NotificationInfo() + info.downloadState = DYFStoreDownload.State.started + self.postNotification(withName: DYFStore.downloadedNotification, info) + + } else { + + self.didFinishTransaction(transaction, queue: queue, forState: DYFStore.PurchaseState.restored) + } + } + + /// The transaction is in the queue, but its final status is pending external action such as Ask to Buy. Update your UI to show the deferred state, and wait for another callback that indicates the final status. + /// + /// - Parameters: + /// - transaction: An `SKPaymentTransaction` object in the payment queue. + /// - queue: The payment queue that updated the transactions. + private func didDeferTransaction(_ transaction: SKPaymentTransaction, queue: SKPaymentQueue) { + // Do not block your UI. Allow the user to continue using your app. + DYFStoreLog("The transaction deferred. Do not block your UI. Allow the user to continue using your app.") + + var info = DYFStore.NotificationInfo() + info.state = DYFStore.PurchaseState.deferred + self.postNotification(info) + } + + /// Notifies the user about the purchase process finished. + /// + /// - Parameters: + /// - transaction: An `SKPaymentTransaction` object in the payment queue. + /// - queue: The payment queue that updated the transactions. + /// - forState: The state of purchase. + private func didFinishTransaction(_ transaction: SKPaymentTransaction, queue: SKPaymentQueue, forState state: DYFStore.PurchaseState) { + + var info = DYFStore.NotificationInfo() + info.state = state + info.productIdentifier = transaction.payment.productIdentifier + if #available(iOS 7.0, *) { + info.userIdentifier = transaction.payment.applicationUsername + } + info.transactionDate = transaction.transactionDate + info.transactionIdentifier = transaction.transactionIdentifier + + if let originalTx = transaction.original { + info.originalTransactionDate = originalTx.transactionDate + info.originalTransactionIdentifier = originalTx.transactionIdentifier + } + + self.postNotification(info) + } + + // MARK: - Download Transaction + + private func didUpdateDownload(_ download: SKDownload, queue: SKPaymentQueue) { + DYFStoreLog("The download(\(download.contentIdentifier)) for product(\(download.transaction.payment.productIdentifier)) updated") + + // The content is being downloaded. Let's provide a download progress to the user. + var info = DYFStore.NotificationInfo() + info.downloadState = DYFStoreDownload.State.inProgress + info.downloadProgress = download.progress * 100 + + self.postNotification(withName: DYFStore.downloadedNotification, info) + } + + private func didPauseDownload(_ download: SKDownload, queue: SKPaymentQueue) { + DYFStoreLog("The download(\(download.contentIdentifier)) for product(\(download.transaction.payment.productIdentifier)) paused") + } + + private func didCancelDownload(_ download: SKDownload, queue: SKPaymentQueue) { + let transaction: SKPaymentTransaction = download.transaction + + DYFStoreLog("The download(\(download.contentIdentifier)) for product(\(transaction.payment.productIdentifier)) canceled") + + // StoreKit saves your downloaded content in the Caches directory. Let's remove it. + do { + try FileManager.default.removeItem(at: download.contentURL ?? URL(string: "")!) + } catch let error { + DYFStoreLog("FileManager.default.removeItem(at:): \(error.localizedDescription)") + } + + var info = DYFStore.NotificationInfo() + info.downloadState = DYFStoreDownload.State.cancelled + self.postNotification(withName: DYFStore.downloadedNotification, info) + + let hasPendingDownloads = DYFStore.hasPendingDownloadsInTransaction(transaction) + if !hasPendingDownloads { + + let errDesc = NSLocalizedString("The download cancelled", tableName: "DYFStore", comment: "Error description") + let userInfo = [NSLocalizedDescriptionKey: errDesc] + let error = NSError(domain: DYFStoreError.domain, + code: DYFStoreError.Code.downloadCancelled.rawValue, + userInfo: userInfo) + + self.didFailWithTransaction(transaction, queue: queue, error: error) + } + } + + private func didFailWithDownload(_ download: SKDownload, queue: SKPaymentQueue) { + let transaction: SKPaymentTransaction = download.transaction + let error = download.error! as NSError + + DYFStoreLog("The download(\(download.contentIdentifier)) for product(\(transaction.payment.productIdentifier)) failed with error(\(error.localizedDescription))") + + // If a download fails, remove it from the Caches, then finish the transaction. + // It is recommended to retry downloading the content in this case. + do { + try FileManager.default.removeItem(at: download.contentURL ?? URL(string: "")!) + } catch let error { + DYFStoreLog("FileManager.default.removeItem(at:): \(error.localizedDescription)") + } + + var info = DYFStore.NotificationInfo() + info.downloadState = DYFStoreDownload.State.failed + info.error = error + self.postNotification(withName: DYFStore.downloadedNotification, info) + + let hasPendingDownloads = DYFStore.hasPendingDownloadsInTransaction(transaction) + if !hasPendingDownloads { + self.didFailWithTransaction(transaction, queue: queue, error: error) + } + } + + private func didFinishDownload(_ download: SKDownload, queue: SKPaymentQueue) { + let transaction: SKPaymentTransaction = download.transaction + + // The download is complete. StoreKit saves the downloaded content in the Caches directory. + DYFStoreLog("The download(\(download.contentIdentifier)) for product(\(transaction.payment.productIdentifier)) finished. Location of downloaded file(\(download.contentURL!.absoluteString))") + + // Post a DYFStoreDownload.State.succeeded notification if the download is completed. + var info = DYFStore.NotificationInfo() + info.downloadState = DYFStoreDownload.State.succeeded + self.postNotification(withName: DYFStore.downloadedNotification, info) + + // It indicates whether all content associated with the transaction were downloaded. + var allAssetsDownloaded: Bool = true + if DYFStore.hasPendingDownloadsInTransaction(transaction) { + // We found an ongoing download. Therefore, there are still pending downloads. + allAssetsDownloaded = false + } + + if allAssetsDownloaded { + + var state: DYFStore.PurchaseState + + if transaction.transactionState == .restored { + state = DYFStore.PurchaseState.restored + } else { + state = DYFStore.PurchaseState.succeeded + } + + self.didFinishTransaction(transaction, queue: queue, forState:state) + } + } + + /// Returns the state that a download operation can be in. + /// + /// - Parameter download: Downloadable content associated with a product. + /// - Returns: The state that a download operation can be in. + private func state(forDownload download: SKDownload) -> SKDownloadState { + + var state: SKDownloadState + + if #available(iOS 12.0, *) { + state = download.state + } else { + state = download.downloadState + } + + return state + } + + /// Whether there are pending downloads in the transaction. + /// + /// - Parameter transaction: An `SKPaymentTransaction` object in the payment queue. + /// - Returns: YES if there are pending downloads and NO, otherwise. + public class func hasPendingDownloadsInTransaction(_ transaction: SKPaymentTransaction) -> Bool { + + // A download is complete if its state is SKDownloadState.cancelled, SKDownloadState.failed, or SKDownloadState.finished + // and pending, otherwise. We finish a transaction if and only if all its associated downloads are complete. + // For the SKDownloadState.failed case, it is recommended to try downloading the content again before finishing the transaction. + for download in transaction.downloads { + + let state = DYFStore.default.state(forDownload: download) + + switch state { + case .active, .paused, .waiting: + return true + case .cancelled, .failed, .finished: + continue + } + } + + return false + } + +} + +// MARK: - Extension Date +extension Date { + + /// Returns a string representation of a given date formatted using the receiver’s current settings. + /// + /// - Returns: A string representation of a given date formatted using the receiver’s current settings. + public func toString() -> String { + + let dateFormatter = DateFormatter() + dateFormatter.locale = NSLocale.current + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + + let dateString = dateFormatter.string(from: self) + + return dateString + } + + /// Returns a string representation of a given date formatted using the receiver’s current settings. + /// + /// - Returns: A string representation of a given date formatted using the receiver’s current settings. + public func toGTMString() -> String { + + let dateFormatter = DateFormatter() + dateFormatter.locale = NSLocale.current + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z" + + let dateString = dateFormatter.string(from: self) + + return dateString + } + + /// Returns a time interval between the date object and 00:00:00 UTC on 1 January 1970. + /// + /// - Returns: A time interval between the date object and 00:00:00 UTC on 1 January 1970. + public func timestamp() -> String { + let timeInterval = self.timeIntervalSince1970 + return "\(timeInterval)" + } + +} + +// MARK: - Data, Base64 +extension Data { + + /// Creates a Base64, UTF-8 encoded data object from the data object. + /// + /// - Returns: A Base64, UTF-8 encoded data object. + public func base64Encode() -> Data? { + return self.base64EncodedData(options: []) + } + + /// Creates a Base64 encoded string from the data object. + /// + /// - Returns: A Base64 encoded string. + public func base64EncodedString() -> String? { + return self.base64EncodedString(options: []) + } + + /// Creates a data object with the given Base64 encoded data. + /// + /// - Returns: A data object containing the Base64 decoded data. Returns nil if the data object could not be decoded. + public func base64Decode() -> Data? { + return NSData(base64Encoded: self) as Data? + } + + /// Creates a string object with the given Base64 encoded data. + /// + /// - Returns: A string object containing the Base64 decoded data. Returns nil if the data object could not be decoded. + public func base64DecodedString() -> String? { + + guard let data = base64Decode() else { + return nil + } + + return String(data: data, encoding: String.Encoding.utf8) + } + +} + +// MARK: - String, Base64 +extension String { + + /// Creates and returns a date object set to the given number of seconds from 00:00:00 UTC on 1 January 1970. + /// + /// - Returns: A date object set to seconds seconds from the reference date. + public func timestampToDate() -> Date { + + let s = NSString(string: self) + let t: TimeInterval = s.doubleValue + + return Date(timeIntervalSince1970: t) + } + + /// Creates a Base64 encoded string from the string. + /// + /// - Returns: A Base64 encoded string. + public func base64Encode() -> String? { + + guard let data = self.data(using: String.Encoding.utf8) else { + return nil + } + + return data.base64EncodedString(options: []) + } + + /// Creates a Base64, UTF-8 encoded data object from the string. + /// + /// - Returns: A Base64, UTF-8 encoded data object. + public func base64EncodedData() -> Data? { + + guard let data = self.data(using: String.Encoding.utf8) else { + return nil + } + + return data.base64EncodedData(options: []) + } + + /// Creates a string object with the given Base64 encoded string. + /// + /// - Returns: A string object built by Base64 decoding the provided string. Returns nil if the string object could not be decoded. + public func base64Decode() -> String? { + + guard let data = base64EncodedData() else { + return nil + } + + return String(data: data, encoding: String.Encoding.utf8) + } + + /// Creates a data object with the given Base64 encoded string. + /// + /// - Returns: A data object built by Base64 decoding the provided string. Returns nil if the string object could not be decoded. + public func base64DecodedData() -> Data? { + return NSData(base64Encoded: self, options: []) as Data? + } + +} + +// MARK: - Extension DYFStore +extension DYFStore { + + /// Uses enumeration to inicate the state of purchase. + public enum PurchaseState: UInt8 { + + /// Indicates that the state is purchasing. + case purchasing + + /// Indicates the user cancels the purchase. + case cancelled + + /// Indicates that the purchase failed. + case failed + + /// Indicates that the purchase was successful. + case succeeded + + /// Indicates that the restoring transaction was successful. + case restored + + /// Indicates that the restoring transaction failed. + case restoreFailed + + /// Indicates that the transaction was deferred. + @available(iOS 8.0, *) + case deferred + } + + /// Provides notification about the purchase. + public static let purchasedNotification: NSNotification.Name = NSNotification.Name(rawValue: "DYFStorePurchasedNotification") + + /// Provides notification about the download. + public static let downloadedNotification: NSNotification.Name = NSNotification.Name(rawValue: "DYFStoreDownloadedNotification") + +} + +// MARK: - DYFStoreDownload +public struct DYFStoreDownload { + + /// Uses enumeration to inicate the state of download. + public enum State: UInt8 { + + /// Indicates that downloading a hosted content has started. + case started + + /// Indicates that a hosted content is currently being downloaded. + case inProgress + + /// Indicates that your app cancelled the download. + case cancelled + + /// Indicates that downloading a hosted content failed. + case failed + + /// Indicates that a hosted content was successfully downloaded. + case succeeded + } + +} + +// MARK: - DYFStoreError +public struct DYFStoreError { + + /// The error domain for store. + public static let domain: String = "SKErrorDomain.dyfstore" + + public enum Code: Int { + + /// Unknown product identifier. + case unknownProductIdentifier = 100 + + /// Invalid parameter indicates that the received value is nil or empty. + case invalidParameter = 136 + + /// Indicates that your app cancelled the download. + case downloadCancelled = 300 + } + + public static var unknownProductIdentifier: DYFStoreError.Code { + return DYFStoreError.Code(rawValue: 100)! + } + + public static var invalidParameter: DYFStoreError.Code { + return DYFStoreError.Code(rawValue: 136)! + } + + public static var downloadCanceled: DYFStoreError.Code { + return DYFStoreError.Code(rawValue: 300)! + } + +} + +// MARK: - DYFStore.NotificationInfo +extension DYFStore { + + public struct NotificationInfo { + + /// The state of purchase. + public var state: DYFStore.PurchaseState? + + /// The state of the download. Only valid if downloading a hosted content. + public var downloadState: DYFStoreDownload.State? + + /// A value that indicates how much of the file has been downloaded. Only valid if state is DYFStoreDownload.State.inProgress. + public var downloadProgress: Float = 0 + + /// This indicates an error occurred. + public var error: NSError? + + /// A string used to identify a product that can be purchased from within your app. + public var productIdentifier: String? + + /// An opaque identifier for the user’s account on your system. + public var userIdentifier: String? + + /// When a transaction is restored, the current transaction holds a new transaction date. Your app will read this property to retrieve the restored transaction date. + public var originalTransactionDate: Date? + + /// When a transaction is restored, the current transaction holds a new transaction identifier. Your app will read this property to retrieve the restored transaction identifier. + public var originalTransactionIdentifier: String? + + /// The date when the transaction was added to the server queue. Only valid if state is SKPaymentTransactionState.purchased or SKPaymentTransactionState.restored. + public var transactionDate: Date? + + /// The transaction identifier of purchase. + public var transactionIdentifier: String? + } + +} + +/// Processes the purchase which was initiated by user from the App Store. +@objc public protocol DYFStoreAppStorePaymentDelegate: NSObjectProtocol { + + /// A user initiated an in-app purchase from the App Store. + /// + /// - Parameters: + /// - queue: The payment queue on which the payment request was made. + /// - payment: The payment request. + /// - product: The in-app purchase product. + @available(iOS 11.0, *) + @objc func didReceiveAppStorePurchaseRequest(_ queue: SKPaymentQueue, payment: SKPayment, forProduct product: SKProduct) + +} + +// MARK: - Extends the properties and method for the dispatch queue. +extension DispatchQueue { + + /// Declares an array of string to record the token. + private static var _onceTracker = [String]() + + /// Executes a block of code associated with a given token, only once. The code is thread safe and will only execute the code once even in the presence of multi-thread calls. + /// + /// - Parameters: + /// - token: A unique idetifier. + /// - block: A block to execute once. + public class func once(token: String, block: () -> Void) { + + objc_sync_enter(self) + defer { objc_sync_exit(self) } + + if _onceTracker.contains(token) { + return + } + + _onceTracker.append(token) + + block() + } + + /// Submits a task to a dispatch queue for asynchronous execution. + /// + /// - Parameter block: The block to be invoked on the queue. + public func asyncTask(block: @escaping () -> Void) { + self.async(execute: block) + } + + /// Submits a task to a dispatch queue for asynchronous execution after a specified time. + /// + /// - Parameters: + /// - time: The block should be executed after a few time delay. + /// - block: The block to be invoked on the queue. + public func asyncAfter(delay time: Double, block: @escaping () -> Void) { + self.asyncAfter(deadline: .now() + time, execute: block) + } + +} + +/// // Outputs log to the console in the process of purchasing the `SKProduct` product. +/// +/// - Parameters: +/// - format: The format string. +/// - args: The arguments for outputting to the console. +/// - funcName: The name of a function. +/// - lineNum: The number of a code line. +public func DYFStoreLog(_ format: String = "", _ args: CVarArg..., funcName: String = #function, lineNum: Int = #line) { + + if DYFStore.default.enableLog { + + let fileName = (#file as NSString).lastPathComponent + + let output = String(format: format, args) + + print("[\(fileName):\(funcName)] [line: \(lineNum)]" + " [DYFStore] " + output) + } +} diff --git a/ios-template/DYFStore/DYFStoreConverter.swift b/ios-template/DYFStore/DYFStoreConverter.swift new file mode 100644 index 0000000..48a5c27 --- /dev/null +++ b/ios-template/DYFStore/DYFStoreConverter.swift @@ -0,0 +1,242 @@ +// +// DYFStoreTransaction.swift +// +// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStore ) +// Copyright © 2016 dyf. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// The converter is used to convert json and json object to each other, +/// convert an object and a data object to each other. +public class DYFStoreConverter: NSObject { + + /// Instantiates a DYFStoreConverter object. + public override init() { + super.init() + } + + /// Encodes an object. + /// + /// - Parameter object: An object you want to encode. + /// - Returns: The data object into which the archive is written. + @objc public static func encodeObject(_ object: Any?) -> Data? { + + guard let obj = object else { + return nil + } + + if #available(iOS 11.0, *) { + + let archiver = NSKeyedArchiver(requiringSecureCoding: false) + archiver.encode(obj) + return archiver.encodedData // archiver.finishEncoding() and return the data. + } + + return NSKeyedArchiver.archivedData(withRootObject: obj); + } + + /// Returns an object initialized for decoding data. + /// + /// - Parameter data: An archive previously encoded by NSKeyedArchiver. + /// - Returns: An object initialized for decoding data. + @objc public static func decodeObject(_ data: Data?) -> Any? { + + guard let tData = data else { + return nil + } + + if #available(iOS 11.0, *) { + + do { + let unarchiver = try NSKeyedUnarchiver(forReadingFrom: tData) + unarchiver.requiresSecureCoding = false + let object = unarchiver.decodeObject() + unarchiver.finishDecoding() + + return object + } catch let error { + + #if DEBUG + print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error.localizedDescription)") + #endif + + return nil + } + } + + return NSKeyedUnarchiver.unarchiveObject(with: tData) + } + + /// Returns JSON data from a Foundation object. The Options for writing JSON data is equivalent to kNilOptions in Objective-C. + /// + /// - Parameter obj: The object from which to generate JSON data. + /// - Returns: JSON data for obj, or nil if an internal error occurs. + @objc public static func json(withObject obj: Any?) -> Data? { + return json(withObject: obj, options: []) + } + + /// Returns JSON data from a Foundation object. + /// + /// - Parameters: + /// - obj: The object from which to generate JSON data. + /// - options: Options for writing JSON data. The default value is equivalent to kNilOptions in Objective-C. + /// - Returns: JSON data for obj, or nil if an internal error occurs. + @objc public static func json(withObject obj: Any?, options: JSONSerialization.WritingOptions = []) -> Data? { + + guard let anObj = obj else { return nil } + + do { + // let encoder = JSONEncoder() + // encoder.outputFormatting = .prettyPrinted /* The pretty output formatting. */ + // let data = try encoder.encode(obj) /* The object complies with the Codable protocol. */ + let data = try JSONSerialization.data(withJSONObject: anObj, options: options) + + return data + } catch let error { + + #if DEBUG + print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error.localizedDescription)") + #endif + + return nil + } + } + + /// Returns JSON string from a Foundation object. The Options for writing JSON data is equivalent to kNilOptions in Objective-C. + /// + /// - Parameter obj: The object from which to generate JSON string. + /// - Returns: JSON string for obj, or nil if an internal error occurs. + @objc public static func jsonString(withObject obj: Any?) -> String? { + return jsonString(withObject: obj, options: []) + } + + /// Returns JSON string from a Foundation object. + /// + /// - Parameters: + /// - obj: The object from which to generate JSON string. + /// - options: Options for writing JSON data. The default value is equivalent to kNilOptions in Objective-C. + /// - Returns: JSON string for obj, or nil if an internal error occurs. + @objc public static func jsonString(withObject obj: Any?, options: JSONSerialization.WritingOptions = []) -> String? { + + guard let anObj = obj else { return nil } + + do { + // let encoder = JSONEncoder() + // encoder.outputFormatting = .prettyPrinted /* The pretty output formatting. */ + // let data = try encoder.encode(obj) /* The object complies with the Codable protocol. */ + let data = try JSONSerialization.data(withJSONObject: anObj, options: options) + + return String(data: data, encoding: String.Encoding.utf8) + } catch let error { + + #if DEBUG + print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error.localizedDescription)") + #endif + + return nil + } + } + + /// Returns a Foundation object from given JSON data. The options used when creating Foundation objects from JSON data is equivalent to kNilOptions in Objective-C. + /// + /// - Parameter data: A data object containing JSON data. + /// - Returns: A Foundation object from the JSON data in data, or nil if an error occurs. + @objc public static func jsonObject(withData data: Data?) -> Any? { + return jsonObject(withData: data, options: []) + } + + /// Returns a Foundation object from given JSON data. + /// + /// - Parameters: + /// - data: A data object containing JSON data. + /// - options: Options used when creating Foundation objects from JSON data. The default value is equivalent to kNilOptions in Objective-C. + /// - Returns: A Foundation object from the JSON data in data, or nil if an error occurs. + @objc public static func jsonObject(withData data: Data?, options: JSONSerialization.ReadingOptions = []) -> Any? { + + guard let aData = data else { return nil } + + do { + // struct GroceryProduct: Codable { + // var name: String + // var points: Int + // var description: String? + // } + + // let json = """ + // { + // "name": "Durian", + // "points": 600, + // "description": "A fruit with a distinctive scent." + // } + // """.data(using: .utf8)! + + // let decoder = JSONDecoder() + // let obj = try decoder.decode(GroceryProduct.self, from: json) /* The object complies with the Codable protocol. */ + let obj = try JSONSerialization.jsonObject(with: aData, options: options) + + return obj + } catch let error { + + #if DEBUG + print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error.localizedDescription)") + #endif + + return nil + } + } + + /// Returns a Foundation object from given JSON string. The options used when creating Foundation objects from JSON data is equivalent to kNilOptions in Objective-C. + /// + /// - Parameter json: A string object containing JSON string. + /// - Returns: A Foundation object from the JSON data in data, or nil if an error occurs. + @objc public static func jsonObject(withJSON json: String?) -> Any? { + return jsonObject(withJSON: json, options: []) + } + + /// Returns a Foundation object from given JSON string. + /// + /// - Parameters: + /// - json: A string object containing JSON string. + /// - options: Options used when creating Foundation objects from JSON data. The default value is equivalent to kNilOptions in Objective-C. + /// - Returns: A Foundation object from the JSON data in data, or nil if an error occurs. + @objc public static func jsonObject(withJSON json: String?, options: JSONSerialization.ReadingOptions = []) -> Any? { + + guard let data = json?.data(using: String.Encoding.utf8) else { + return nil + } + + do { + let obj = try JSONSerialization.jsonObject(with: data, options: options) + + return obj + } catch let error { + + #if DEBUG + print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error)") + #endif + + return nil + } + } + +} + diff --git a/ios-template/DYFStore/DYFStoreKeychainPersistence.swift b/ios-template/DYFStore/DYFStoreKeychainPersistence.swift new file mode 100644 index 0000000..e05e71d --- /dev/null +++ b/ios-template/DYFStore/DYFStoreKeychainPersistence.swift @@ -0,0 +1,173 @@ +// +// DYFStoreKeychainPersistence.swift +// +// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStore ) +// Copyright © 2016 dyf. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/** Deprecated. + +/// The transaction persistence using the keychain. +open class DYFStoreKeychainPersistence: NSObject { + + /// Instantiates a DYFSwiftKeychain object. + private lazy var keychain: DYFSwiftKeychain = { + let keychain = DYFSwiftKeychain() + return keychain + }() + + /// Loads an array whose elements are the `Dictionary` objects from the keychain. + /// + /// - Returns: An array whose elements are the `Dictionary` objects. + private func loadDataFromKeychain() -> [[String : Any]]? { + + let data = self.keychain.getData(DYFStoreTransactionsKey) + + let array = DYFStoreConverter.jsonObject(withData: data) as? [[String : Any]] + + return array + } + + /// Returns a Boolean value that indicates whether a transaction is present in the keychain with a given transaction ientifier. + /// + /// - Parameter transactionIdentifier: The unique server-provided identifier. + /// - Returns: True if a transaction is present in the keychain, otherwise false. + public func containsTransaction(_ transactionIdentifier: String) -> Bool { + + let array = loadDataFromKeychain() + guard let arr = array, arr.count > 0 else { + return false + } + + for item in arr { + + let transaction = DYFSwiftRuntimeProvider.model(withDictionary: item, forClass: DYFStoreTransaction.self) + let identifier = transaction?.transactionIdentifier + + if let id = identifier, id == transactionIdentifier { + return true + } + } + + return false + } + + /// Stores an `DYFStoreTransaction` object in the keychain item. + /// + /// - Parameter transaction: An `DYFStoreTransaction` object. + public func storeTransaction(_ transaction: DYFStoreTransaction?) { + + let obj = DYFSwiftRuntimeProvider.dictionary(withModel: transaction) + guard let dict = obj else { + return + } + + var transactions = loadDataFromKeychain() ?? [[String : Any]]() + transactions.append(dict) + + let tData = DYFStoreConverter.json(withObject: transactions) + self.keychain.set(tData, forKey: DYFStoreTransactionsKey) + } + + /// Retrieves an array whose elements are the `DYFStoreTransaction` objects from the keychain. + /// + /// - Returns: An array whose elements are the `DYFStoreTransaction` objects. + public func retrieveTransactions() -> [DYFStoreTransaction]? { + + let array = loadDataFromKeychain() + guard let arr = array else { + return nil + } + + var transactions = [DYFStoreTransaction]() + for item in arr { + + let transaction = DYFSwiftRuntimeProvider.model(withDictionary: item, forClass: DYFStoreTransaction.self) + if let t = transaction { + transactions.append(t) + } + } + + return transactions + } + + /// Retrieves an `DYFStoreTransaction` object from the keychain with a given transaction ientifier. + /// + /// - Parameter transactionIdentifier: The unique server-provided identifier. + /// - Returns: An `DYFStoreTransaction` object from the keychain. + public func retrieveTransaction(_ transactionIdentifier: String) -> DYFStoreTransaction? { + + let array = retrieveTransactions() + guard let arr = array else { + return nil + } + + for transaction in arr { + + let identifier = transaction.transactionIdentifier + if identifier == transactionIdentifier { + return transaction + } + } + + return nil + } + + /// Removes an `DYFStoreTransaction` object from the keychain with a given transaction ientifier. + /// + /// - Parameter transactionIdentifier: The unique server-provided identifier. + public func removeTransaction(_ transactionIdentifier: String) { + + let array = loadDataFromKeychain() + guard var arr = array else { + return + } + + var index: Int = -1 + for (idx, item) in arr.enumerated() { + + let transaction = DYFSwiftRuntimeProvider.model(withDictionary: item, forClass: DYFStoreTransaction.self) + let identifier = transaction?.transactionIdentifier + + if let id = identifier, id == transactionIdentifier { + index = idx + break + } + } + + guard index >= 0 else { return } + arr.remove(at: index) + + let tData = DYFStoreConverter.json(withObject: arr) + self.keychain.set(tData, forKey: DYFStoreTransactionsKey) + } + + /// Removes all transactions from the keychain. + public func removeTransactions() { + self.keychain.delete(DYFStoreTransactionsKey) + } + +} + +*/ + diff --git a/ios-template/DYFStore/DYFStoreReceiptVerifier.swift b/ios-template/DYFStore/DYFStoreReceiptVerifier.swift new file mode 100644 index 0000000..786860a --- /dev/null +++ b/ios-template/DYFStore/DYFStoreReceiptVerifier.swift @@ -0,0 +1,273 @@ +// +// DYFStoreReceiptVerifier.swift +// +// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStoreReceiptVerifier ) +// Copyright © 2016 dyf. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// The class is used to verify in-app purchase receipts. +open class DYFStoreReceiptVerifier: NSObject { + + /// Callbacks the result of the request that verifies the in-app purchase receipt. + @objc public weak var delegate: DYFStoreReceiptVerifierDelegate? + + /// The url for sandbox in the test environment. + private let sandboxUrl = "https://sandbox.itunes.apple.com/verifyReceipt" + /// The url for production in the production environment. + private let productUrl = "https://buy.itunes.apple.com/verifyReceipt" + + /// The data for a POST request. + private var requestData: Data? + + /// A default session configuration object. + private var urlSessionConfig = URLSessionConfiguration.default + + /// An object that coordinates a group of related network data transfer tasks. + private var urlSession: URLSession? + + /// A URL session task that returns downloaded data directly to the app in memory. + private var dataTask: URLSessionDataTask? + + /// Whether all outstanding tasks have been cancelled and the session has been invalidated. + private var canInvalidateSession: Bool = false + + /// Instantiates a DYFStoreReceiptVerifier object. + public override init() { + super.init() + self.instantiateUrlSession() + } + + /// Checks the url session configuration object. + private func checkUrlSessionConfig() { + self.urlSessionConfig.allowsCellularAccess = true + } + + /// Instantiates the url session object. + private func instantiateUrlSession() { + self.checkUrlSessionConfig() + self.urlSession = URLSession(configuration: urlSessionConfig) + } + + /// Cancels the task. + @objc public func cancel() { + self.dataTask?.cancel() + } + + /// Cancels all outstanding tasks and then invalidates the session. + @objc public func invalidateAndCancel() { + self.urlSession?.invalidateAndCancel() + self.canInvalidateSession = true + } + + /// Verifies the in-app purchase receipt, but it is not recommended to use. It is better to use your own server to obtain the parameters uploaded from the client to verify the receipt from the app store server (C -> Uploaded Parameters -> S -> App Store S -> S -> Receive And Parse Data -> C). + /// If the receipts are verified by your own server, the client needs to upload these parameters, such as: "transaction identifier, bundle identifier, product identifier, user identifier, shared sceret(Subscription), receipt(Safe URL Base64), original transaction identifier(Optional), original transaction time(Optional) and the device information, etc.". + /// + /// - Parameter receiptData: A signed receipt that records all information about a successful payment transaction. + @objc public func verifyReceipt(_ receiptData: Data?) { + verifyReceipt(receiptData, sharedSecret: nil) + } + + /// Verifies the in-app purchase receipt, but it is not recommended to use. It is better to use your own server to obtain the parameters uploaded from the client to verify the receipt from the app store server (C -> Uploaded Parameters -> S -> App Store S -> S -> Receive And Parse Data -> C). + /// If the receipts are verified by your own server, the client needs to upload these parameters, such as: "transaction identifier, bundle identifier, product identifier, user identifier, shared sceret(Subscription), receipt(Safe URL Base64), original transaction identifier(Optional), original transaction time(Optional) and the device information, etc.". + /// + /// - Parameters: + /// - receiptData: A signed receipt that records all information about a successful payment transaction. + /// - secretKey: Your app’s shared secret (a hexadecimal string). Only used for receipts that contain auto-renewable subscriptions. + @objc public func verifyReceipt(_ receiptData: Data?, sharedSecret secretKey: String? = nil) { + + guard let data = receiptData else { + + let messae = "The received data is null." + let error = NSError(domain: "SRErrorDomain.DYFStore", + code: -12, + userInfo: [NSLocalizedDescriptionKey : messae]) + + self.delegate?.verifyReceipt(self, didFailWithError: error) + + return + } + + let receiptBase64 = data.base64EncodedString() + + // Creates the JSON object that describes the request. + var requestContents: [String: Any] = [String: Any]() + requestContents["receipt-data"] = receiptBase64 + if let key = secretKey { + requestContents["password"] = key + } + + do { + self.requestData = try JSONSerialization.data(withJSONObject: requestContents) + + self.connect(withUrl: productUrl) + } catch let error { + self.delegate?.verifyReceipt(self, didFailWithError: error as NSError) + } + } + + // Make a connection to the iTunes Store on a background queue. + private func connect(withUrl url: String) { + let aURL: URL = URL(string: url)! + + // Creates a POST request with the receipt data. + var request = URLRequest(url: aURL, cachePolicy: URLRequest.CachePolicy.reloadIgnoringCacheData, timeoutInterval: 15.0) + request.httpMethod = "POST" + request.httpBody = self.requestData + + if self.canInvalidateSession { + self.instantiateUrlSession() + self.canInvalidateSession = false + } + + self.dataTask = self.urlSession?.dataTask(with: request) { (data, response, error) in + self.didReceiveData(data, response: response, error: error) + } + self.dataTask?.resume() + } + + private func didReceiveData(_ data: Data?, response: URLResponse?, error: Error?) { + + if let err = error { + + let nsError = err as NSError + + DispatchQueue.main.async { + self.delegate?.verifyReceipt(self, didFailWithError: nsError) + } + } else { + self.processResult(data!) + } + } + + private func processResult(_ data: Data) { + do { + let jsonObj = try JSONSerialization.jsonObject(with: data) + let dict = jsonObj as! Dictionary + + let status = dict["status"] as! Int + if status == 0 { + + DispatchQueue.main.async { + self.delegate?.verifyReceiptDidFinish(self, didReceiveData: dict) + } + } else if status == 21007 { // sandbox + + self.connect(withUrl: sandboxUrl) + } else { + + let (code, message) = matchMessage(withStatus: status) + let nsError = NSError(domain: "SRErrorDomain.DYFStore", + code: code, + userInfo: [NSLocalizedDescriptionKey : message]) + + DispatchQueue.main.async { + self.delegate?.verifyReceipt(self, didFailWithError: nsError) + } + } + + } catch let error { + + let nsError = error as NSError + + DispatchQueue.main.async { + self.delegate?.verifyReceipt(self, didFailWithError: nsError) + } + } + } + + /// Matches the message with the status code. + /// + /// - Parameter status: The status code of the request response. More, please see [Receipt Validation Programming Guide](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1) + /// - Returns: A tuple that contains status code and the description of status code. + public func matchMessage(withStatus status: Int) -> (Int, String) { + var message: String = "" + + switch status { + case 0: + message = "The receipt as a whole is valid." + break + case 21000: + message = "The App Store could not read the JSON object you provided." + break + case 21002: + message = "The data in the receipt-data property was malformed or missing." + break + case 21003: + message = "The receipt could not be authenticated." + break + case 21004: + message = "The shared secret you provided does not match the shared secret on file for your account." + break + case 21005: + message = "The receipt server is not currently available." + break + case 21006: + message = "This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response. Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions." + break + case 21007: + message = "This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead." + break + case 21008: + message = "This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead." + break + case 21010: + message = "This receipt could not be authorized. Treat this the same as if a purchase was never made." + break + default: /* 21100-21199 */ + message = "Internal data access error." + break + } + + return (status, message) + } + + /// Matches the message with the status code. + /// + /// - Parameter status: The status code of the request response. More, please see [Receipt Validation Programming Guide](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1) + /// - Returns: A string that contains the description of status code. + @objc public func matchMessage(withStatus status: Int) -> String { + let (_, msg) = matchMessage(withStatus: status) + return msg + } + +} + +/// The delegate is used to callback the result of verifying the in-app purchase receipt. +@objc public protocol DYFStoreReceiptVerifierDelegate: NSObjectProtocol { + + /// Tells the delegate that an in-app purchase receipt verification has completed. + /// + /// - Parameters: + /// - verifier: A `DYFStoreReceiptVerifier` object. + /// - data: The data received from the server, is converted to a dictionary of key-value pairs. + @objc func verifyReceiptDidFinish(_ verifier: DYFStoreReceiptVerifier, didReceiveData data: [String : Any]) + + /// Tells the delegate that an in-app purchase receipt verification occurs an error. + /// + /// - Parameters: + /// - verifier: A `DYFStoreReceiptVerifier` object. + /// - error: The error that caused the receipt validation to fail. + @objc func verifyReceipt(_ verifier: DYFStoreReceiptVerifier, didFailWithError error: NSError) + +} + diff --git a/ios-template/DYFStore/DYFStoreTransaction.swift b/ios-template/DYFStore/DYFStoreTransaction.swift new file mode 100644 index 0000000..a369b19 --- /dev/null +++ b/ios-template/DYFStore/DYFStoreTransaction.swift @@ -0,0 +1,96 @@ +// +// DYFStoreTransaction.swift +// +// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStore ) +// Copyright © 2016 dyf. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + + +open class DYFStoreTransaction: NSObject, NSCoding { + + /// The state of this transaction. 0: purchased, 1: restored. + @objc open var state: UInt8 = 0 + + /// A string used to identify a product that can be purchased from within your app. + @objc open var productIdentifier: String? + + /// An opaque identifier for the user’s account on your system. + @objc open var userIdentifier: String? + + /// When a transaction is restored, the current transaction holds a new transaction timestamp. Your app will read this property to retrieve the restored transaction timestamp. + @objc open var originalTransactionTimestamp: String? + + /// When a transaction is restored, the current transaction holds a new transaction identifier. Your app will read this property to retrieve the restored transaction identifier. + @objc open var originalTransactionIdentifier: String? + + /// The timestamp when the transaction was added to the server queue. Only valid if state is purchased or restored. + @objc open var transactionTimestamp: String? + + /// The unique server-provided identifier. Only valid if state is purchased or restored. + @objc open var transactionIdentifier: String? + + /// A base64 signed receipt that records all information about a successful payment transaction. + /// + /// The contents of this property are undefined except when transactionState is set to purchased. + /// The receipt is a signed chunk of data that can be sent to the App Store to verify that the payment was successfully processed. This is most useful when designing a store that uses a server separate from the iPhone to verify that payment was processed. For more information on verifying receipts, see [Receipt Validation Programming Guide](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573). + @objc open var transactionReceipt: String? + + /// Instantiates a DYFStoreTransaction object. + public override init() { + super.init() + } + + /// The Secure Coding Guide should be consulted when writing methods that decode data. + //public static var supportsSecureCoding: Bool { + // return true + //} + + /// Returns an object initialized from data in a given unarchiver. + /// + /// - Parameter aDecoder: An unarchiver object. + public required convenience init?(coder aDecoder: NSCoder) { + self.init() + DYFSwiftRuntimeProvider.decode(aDecoder, forObject: self) + } + + /// Encodes the receiver using a given archiver. + /// + /// - Parameter aCoder: An archiver object. + public func encode(with aCoder: NSCoder) { + DYFSwiftRuntimeProvider.encode(aCoder, forObject: self) + } +} + +/// Used to represent the state of a transaction. +@objc public enum DYFStoreTransactionState: UInt8 { + + /// Indicates that the transaction has been purchased. + case purchased + + /// Indicates that the transaction has been restored. + case restored +} + +/// The key UserDefaults and Keychain used. +public let DYFStoreTransactionsKey = "DYFStoreTransactions" + diff --git a/ios-template/DYFStore/DYFStoreUserDefaultsPersistence.swift b/ios-template/DYFStore/DYFStoreUserDefaultsPersistence.swift new file mode 100644 index 0000000..b4c8d9f --- /dev/null +++ b/ios-template/DYFStore/DYFStoreUserDefaultsPersistence.swift @@ -0,0 +1,165 @@ +// +// DYFStoreUserDefaultsPersistence.swift +// +// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStore ) +// Copyright © 2016 dyf. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Returns the shared defaults `UserDefaults` object. +fileprivate let kUserDefaults = UserDefaults.standard + +/// The transaction persistence using the UserDefaults. +open class DYFStoreUserDefaultsPersistence: NSObject { + + /// Loads an array whose elements are the `Data` objects from the shared preferences search list. + /// + /// - Returns: An array whose elements are the `Data` objects. + private func loadDataFromUserDefaults() -> [Data]? { + let obj = kUserDefaults.object(forKey: DYFStoreTransactionsKey) + return obj as? [Data] + } + + /// Returns a Boolean value that indicates whether a transaction is present in shared preferences search list with a given transaction ientifier. + /// + /// - Parameter transactionIdentifier: The unique server-provided identifier. + /// - Returns: True if a transaction is present in shared preferences search list, otherwise false. + public func containsTransaction(_ transactionIdentifier: String) -> Bool { + + let array = loadDataFromUserDefaults() + guard let arr = array, arr.count > 0 else { + return false + } + + for data in arr { + + let obj = DYFStoreConverter.decodeObject(data) + let transaction = obj as? DYFStoreTransaction + let identifier = transaction?.transactionIdentifier + + if let id = identifier, id == transactionIdentifier { + return true + } + } + + return false + } + + /// Stores an `DYFStoreTransaction` object in the shared preferences search list. + /// + /// - Parameter transaction: An `DYFStoreTransaction` object. + public func storeTransaction(_ transaction: DYFStoreTransaction?) { + + let data = DYFStoreConverter.encodeObject(transaction) + guard let aData = data else { + return + } + + var transactions = loadDataFromUserDefaults() ?? [Data]() + transactions.append(aData) + + kUserDefaults.set(transactions, forKey: DYFStoreTransactionsKey) + kUserDefaults.synchronize() + } + + /// Retrieves an array whose elements are the `DYFStoreTransaction` objects from the shared preferences search list. + /// + /// - Returns: An array whose elements are the `DYFStoreTransaction` objects. + public func retrieveTransactions() -> [DYFStoreTransaction]? { + + let array = loadDataFromUserDefaults() + guard let arr = array else { + return nil + } + + var transactions = [DYFStoreTransaction]() + for item in arr { + + let obj = DYFStoreConverter.decodeObject(item) + if let transaction = obj as? DYFStoreTransaction { + transactions.append(transaction) + } + } + + return transactions + } + + /// Retrieves an `DYFStoreTransaction` object from the shared preferences search list with a given transaction ientifier. + /// + /// - Parameter transactionIdentifier: The unique server-provided identifier. + /// - Returns: An `DYFStoreTransaction` object from the shared preferences search list. + public func retrieveTransaction(_ transactionIdentifier: String) -> DYFStoreTransaction? { + + let array = retrieveTransactions() + guard let arr = array else { + return nil + } + + for transaction in arr { + + let identifier = transaction.transactionIdentifier + if identifier == transactionIdentifier { + return transaction + } + } + + return nil + } + + /// Removes an `DYFStoreTransaction` object from the shared preferences search list with a given transaction ientifier. + /// + /// - Parameter transactionIdentifier: The unique server-provided identifier. + public func removeTransaction(_ transactionIdentifier: String) { + + let array = loadDataFromUserDefaults() + guard var arr = array else { + return + } + + var index = -1 + for (idx, data) in arr.enumerated() { + + let obj = DYFStoreConverter.decodeObject(data) + let transaction = obj as? DYFStoreTransaction + let identifier = transaction?.transactionIdentifier + + if let id = identifier, id == transactionIdentifier { + index = idx + break + } + } + + guard index >= 0 else { return } + arr.remove(at: index) + + kUserDefaults.setValue(arr, forKey: DYFStoreTransactionsKey) + kUserDefaults.synchronize() + } + + /// Removes all transactions from the shared preferences search list. + public func removeTransactions() { + kUserDefaults.removeObject(forKey: DYFStoreTransactionsKey); + kUserDefaults.synchronize() + } + +} + diff --git a/ios-template/DYFStore/DYFSwiftRuntimeProvider.swift b/ios-template/DYFStore/DYFSwiftRuntimeProvider.swift new file mode 100644 index 0000000..95c8464 --- /dev/null +++ b/ios-template/DYFStore/DYFSwiftRuntimeProvider.swift @@ -0,0 +1,359 @@ +// +// DYFSwiftRuntimeProvider.swift +// 武极天下 +// +// Created by zhl on 2021/7/22. +// Copyright © 2021 egret. All rights reserved. +// + +import Foundation + +/// The class for runtime wrapper that provides some common practical applications. +public class DYFSwiftRuntimeProvider: NSObject { + + /// Instantiates a DYFSwiftRuntimeProvider object. + public override init() { + super.init() + } + + /// Describes the instance methods implemented by a class. + /// + /// - Parameter cls: The class you want to inspect. + /// - Returns: String array of the instance methods. + @objc public class func methodList(withClass cls: AnyClass?) -> [String] { + var names: [String] = [String]() + + var count: UInt32 = 0 + let methodList: UnsafeMutablePointer? = class_copyMethodList(cls, &count) + + for index in 0.. [String] { + var names: [String] = [String]() + + var count: UInt32 = 0 + let methodList = class_copyMethodList(object_getClass(obj), &count) + + for index in 0.. [String] { + var names: [String] = [String]() + + var count: UInt32 = 0 + let ivarList = class_copyIvarList(cls, &count) + + for index in 0.. Bool { + + let _impCls: AnyClass? = impCls ?? cls + + guard let imp = class_getMethodImplementation(_impCls, impSel) else { + return false + } + + var types: UnsafePointer? = nil + let method = class_getInstanceMethod(_impCls, impSel) + if let m = method { + types = method_getTypeEncoding(m) + } + + return class_addMethod(cls, sel, imp, types) + } + + /// Adds a new method to a class with a given selector and implementation. + /// + /// - Parameters: + /// - cls: The class to which to add a method. + /// - sel: A selector that specifies the name of the method being added. + /// - impCls: The class you want to inspect. + /// - impSel: The selector of the method you want to retrieve. + /// - types: A string describing a method's parameter and return types. e.g.: "v@:" + /// - Returns: A Bool value. + @objc public class func addMethod(withClass cls: AnyClass?, selector sel: Selector, impClass impCls: AnyClass? = nil, impSelector impSel: Selector, types: String) -> Bool { + + let _impCls: AnyClass? = impCls ?? cls + + guard let imp = class_getMethodImplementation(_impCls, impSel) else { + return false + } + + let _types: UnsafePointer? = (types as NSString).utf8String + + return class_addMethod(cls, sel, imp, _types) + } + + /// Exchanges the implementations of two methods. + /// + /// - Parameters: + /// - cls: The class you want to modify. + /// - sel: A selector that identifies the method whose implementation you want to exchange. + /// - targetCls: The class you want to specify. + /// - targetSel: The selector of the method you want to retrieve. + @objc public class func exchangeMethod(withClass cls: AnyClass?, selector sel: Selector, targetClass targetCls: AnyClass?, targetSelector targetSel: Selector) { + + guard let m1 = class_getInstanceMethod(cls, sel), let m2 = class_getInstanceMethod(targetCls, targetSel) else { + return + } + + method_exchangeImplementations(m1, m2) + } + + /// Replaces the implementation of a method for a given class. + /// + /// - Parameters: + /// - cls: The class you want to modify. + /// - sel: A selector that identifies the method whose implementation you want to replace. + /// - targetCls: The class you want to specify. + /// - targetSel: The selector of the method you want to retrieve. + @objc public class func replaceMethod(withClass cls: AnyClass?, selector sel: Selector, targetClass targetCls: AnyClass? = nil, targetSelector targetSel: Selector) { + + let _targetCls: AnyClass? = targetCls ?? cls + guard let imp = class_getMethodImplementation(_targetCls, targetSel) else { + return + } + + var types: UnsafePointer? = nil + let method = class_getInstanceMethod(_targetCls, targetSel) + if let m = method { + types = method_getTypeEncoding(m) + } + + class_replaceMethod(cls, sel, imp, types) + } + + /// Describes the properties declared by a class. + /// + /// - Parameter cls: The class you want to inspect. + /// - Returns: String array of the properties. + @objc public class func propertyList(withClass cls: AnyClass?) -> [String] { + var names: [String] = [String]() + + var count: UInt32 = 0 + let pList = class_copyPropertyList(cls, &count) + + for index in 0.. String? { + // The name of the executable in this bundle (if any). + let executableKey = kCFBundleExecutableKey as String + //A dictionary, constructed from the bundle’s Info.plist file. + let infoDict = Bundle.main.infoDictionary ?? [String : Any]() + + // Fetches the value for a key. + guard let namespace = infoDict[executableKey] as? String else { + return nil + } + + return namespace + } + + /// Converts a dictionary whose elements are key-value pairs to a corresponding object. + /// + /// - Parameters: + /// - dictionary: A collection whose elements are key-value pairs. + /// - cls: A class that inherits the NSObject class. + /// - Returns: A corresponding object. + public class func model(withDictionary dictionary: Dictionary?, forClass cls: T.Type?) -> T? { + + // Gets the swift namespace. + //guard let namespace = swiftNamespace() else { + // return nil + //} + + //let className = String(cString: class_getName(cls)) + //if className.isEmpty { return nil } + + //let clsName = "\(namespace).\(className)" + //print("clsName: \(clsName)") + + //let aCls: AnyClass? = NSClassFromString(clsName) + //guard let clsType = aCls as? NSObject.Type else { + // return nil + //} + //let obj = clsType.init() + + guard let clsType = cls else { + return nil + } + let obj = clsType.init() + + guard let dict = dictionary else { + return obj + } + + let pList = propertyList(withClass: cls) + for (k, v) in dict { + if pList.contains(k) { + obj.setValue(v, forKey: k) + } + } + + return obj + } + + /// Converts a dictionary whose elements are key-value pairs to a corresponding object. + /// + /// - Parameters: + /// - dictionary: A collection whose elements are key-value pairs. + /// - cls: A class that inherits the NSObject class. + /// - Returns: A corresponding object. + @objc public class func model(withDictionary dictionary: Dictionary?, usingClass cls: NSObject.Type?) -> AnyObject? { + + guard let clsType = cls else { + return nil + } + + let obj = clsType.init() + + guard let dict = dictionary else { + return obj + } + + let pList = propertyList(withClass: clsType) + + for (k, v) in dict { + if pList.contains(k) { + obj.setValue(v, forKey: k) + } + } + + return obj + } + + /// Converts a dictionary whose elements are key-value pairs to a corresponding object. + /// + /// - Parameters: + /// - dictionary: A collection whose elements are key-value pairs. + /// - model: An object that inherits the NSObject class. + /// - Returns: A corresponding object. + @objc public class func model(withDictionary dictionary: Dictionary?, usingModel model: NSObject?) -> AnyObject? { + + guard let dict = dictionary else { + return model + } + + guard let obj = model else { return nil } + + let cls: AnyClass? = object_getClass(obj) + let pList = propertyList(withClass: cls) + + for (k, v) in dict { + if pList.contains(k) { + obj.setValue(v, forKey: k) + } + } + + return obj + } + + /// Converts a object to a corresponding dictionary whose elements are key-value pairs. + /// + /// - Parameter model: A NSObject object. + /// - Returns: A corresponding dictionary. + @objc public class func dictionary(withModel model: NSObject?) -> [String: Any]? { + + guard let m = model, let cls = object_getClass(m) else { + return nil + } + + let pList = propertyList(withClass: cls) + if pList.isEmpty { + return nil + } + + var dict = [String : Any]() + + for key in pList { + if let value = m.value(forKey: key) { + dict[key] = value + } else { + dict[key] = NSNull() + } + } + + return dict + } + + /// Encodes an object using a given archiver. + /// + /// - Parameters: + /// - encoder: An archiver object. + /// - obj: An object you want to encode. + @objc public class func encode(_ encoder: NSCoder, forObject obj: NSObject) { + + let ivarNames = ivarList(withClass: obj.classForCoder) + + for key in ivarNames { + let value = obj.value(forKey: key) + encoder.encode(value, forKey: key) + } + } + + /// Decodes an object initialized from data in a given unarchiver. + /// + /// - Parameters: + /// - decoder: An unarchiver object. + /// - obj: An object you want to decode. + @objc public class func decode(_ decoder: NSCoder, forObject obj: NSObject) { + + let ivarNames = ivarList(withClass: obj.classForCoder) + + for key in ivarNames { + let value = decoder.decodeObject(forKey: key) + obj.setValue(value, forKey: key) + } + } + +} diff --git a/ios-template/KeychainPasswordItem.swift b/ios-template/common/KeychainPasswordItem.swift similarity index 100% rename from ios-template/KeychainPasswordItem.swift rename to ios-template/common/KeychainPasswordItem.swift diff --git a/ios-template/common/ZExtensions.swift b/ios-template/common/ZExtensions.swift new file mode 100644 index 0000000..00c24e5 --- /dev/null +++ b/ios-template/common/ZExtensions.swift @@ -0,0 +1,183 @@ +// +// ZExtensions.swift +// 武极天下 +// +// Created by zhl on 2021/7/22. +// Copyright © 2021 egret. All rights reserved. +// + +import Foundation +import UIKit + +fileprivate var LoadingViewKey = "LoadingViewKey" + +extension NSObject { + + /// Returns The view controller associated with the currently visible view. + /// + /// - Returns: The view controller associated with the currently visible view. + public func currentViewController() -> UIViewController? { + let sharedApp = UIApplication.shared + + let window = sharedApp.keyWindow ?? sharedApp.windows[0] + let viewController = window.rootViewController + + return findCurrentViewController(from: viewController) + } + + private func findCurrentViewController(from viewController: UIViewController?) -> UIViewController? { + + guard var vc = viewController else { + return nil + } + + while true { + if let tvc = vc.presentedViewController { + vc = tvc + } else if vc.isKind(of: UITabBarController.self) { + let tbc = vc as! UITabBarController + if let tvc = tbc.selectedViewController { + vc = tvc + } + } else if vc.isKind(of: UINavigationController.self) { + let nc = vc as! UINavigationController + if let tvc = nc.visibleViewController { + vc = tvc + } + } else { + if vc.children.count > 0 { + if let tvc = vc.children.last { + vc = tvc + } + } + break + } + } + + return vc + } + + /// Shows the tips for user. + public func showTipsMessage(_ message: String) -> Void { + + guard let vc = self.currentViewController(), !vc.isKind(of: UIAlertController.self) else { + return + } + + let alertController = UIAlertController(title: message, message: nil, preferredStyle: UIAlertController.Style.alert) + + vc.present(alertController, animated: true, completion: nil) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + alertController.dismiss(animated: true, completion: nil) + } + } + + /// Shows an alert view controller. + public func showAlert(withTitle title: String?, + message: String?, + cancelButtonTitle: String? = nil, + cancel cancelHandler: ((UIAlertAction) -> Void)? = nil, + confirmButtonTitle: String?, + execute executableHandler: ((UIAlertAction) -> Void)? = nil) { + + guard let vc = self.currentViewController() else { + return + } + + let alertController = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert) + + if let t = cancelButtonTitle, t.count > 0 { + let action = UIAlertAction(title: t, style: UIAlertAction.Style.cancel, handler: cancelHandler) + alertController.addAction(action) + } + + if let t = confirmButtonTitle, t.count > 0 { + let action = UIAlertAction(title: t, style: UIAlertAction.Style.default, handler: executableHandler) + alertController.addAction(action) + } + + vc.present(alertController, animated: true, completion: nil) + } + + /// Shows a loading panel. + public func showLoading(_ text: String) { + + let value = objc_getAssociatedObject(self, &LoadingViewKey) + if value != nil { + return + } + + let loadingView = ZLoadingView() + loadingView.show(text) + loadingView.color = COLOR_RGBA(10, 10, 10, 0.75) + loadingView.indicatorColor = COLOR_RGB(54, 205, 64) + loadingView.textColor = COLOR_RGB(248, 248, 248) + + objc_setAssociatedObject(self, &LoadingViewKey, loadingView, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + /// Hides a loading panel. + public func hideLoading() { + + let value = objc_getAssociatedObject(self, &LoadingViewKey) + guard let loadingView = value as? ZLoadingView else { + return + } + + loadingView.hide() + + objc_setAssociatedObject(self, &LoadingViewKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + +} + +extension UIView { + + /// This method is used to set the corner. + /// + /// - Parameters: + /// - rectCorner: The corners of a rectangle. + /// - radius: The radius of each corner. + public func setCorner(rectCorner: UIRectCorner = UIRectCorner.allCorners, radius: CGFloat) { + + let maskLayer = CAShapeLayer() + let w = self.bounds.size.width + let h = self.bounds.size.height + maskLayer.frame = CGRect(x: 0, y: 0, width: w, height: h) + + let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: rectCorner, cornerRadii: CGSize(width: radius, height: radius)) + maskLayer.path = path.cgPath + + self.layer.mask = maskLayer + } + + /// This method is used to set the border. + /// + /// - Parameters: + /// - rectCorner: The corners of a rectangle. + /// - radius: The radius of each corner. + /// - lineWidth: Specifies the line width of the shape’s path. + /// - color: The color used to stroke the shape’s path. + public func setBorder(rectCorner: UIRectCorner = UIRectCorner.allCorners, radius: CGFloat, lineWidth: CGFloat, color: UIColor?) { + + let maskLayer = CAShapeLayer() + let w = self.bounds.size.width + let h = self.bounds.size.height + maskLayer.frame = CGRect(x: 0, y: 0, width: w, height: h) + + let borderLayer = CAShapeLayer() + borderLayer.frame = CGRect(x: 0, y: 0, width: w, height: h) + borderLayer.lineWidth = lineWidth + borderLayer.strokeColor = color?.cgColor + borderLayer.fillColor = UIColor.clear.cgColor + + let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: rectCorner, cornerRadii: CGSize(width: radius, height: radius)) + borderLayer.path = path.cgPath + maskLayer.path = path.cgPath + + self.layer.insertSublayer(borderLayer, at: 0) + self.layer.mask = maskLayer + } + +} diff --git a/ios-template/common/ZIndefiniteAnimatedSpinner.swift b/ios-template/common/ZIndefiniteAnimatedSpinner.swift new file mode 100644 index 0000000..64a9324 --- /dev/null +++ b/ios-template/common/ZIndefiniteAnimatedSpinner.swift @@ -0,0 +1,216 @@ +// +// ZIndefiniteAnimatedSpinner.swift +// 武极天下 +// +// Created by zhl on 2021/7/22. +// Copyright © 2021 egret. All rights reserved. +// + +import UIKit + +public class ZIndefiniteAnimatedSpinner: UIView { + + /// A structure is named "AnimationKey". + private struct AnimationKey { + static let stroke = "spinner.animkey.stroke" + static let rotation = "spinner.animkey.rotation" + } + + /// The property indicates whether the view is currently animating. + public private(set) var isAnimating: Bool = false + + /// Sets whether the view is hidden when not animating. + public var hidesWhenStopped: Bool = true + + /// Specifies the timing function to use for the control's animation. Defaults to kCAMediaTimingFunctionEaseInEaseOut. + public var timingFunction: CAMediaTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) + + /// A layer that draws a arc progress in its coordinate space. + private lazy var progressLayer: CAShapeLayer = { + let layer = CAShapeLayer() + + layer.strokeColor = nil + layer.fillColor = nil + layer.lineWidth = 1.0 + + return layer + }() + + /// Sets the line width of the spinner's circle. + public var lineWidth: CGFloat { + get { + return self.progressLayer.lineWidth + } + + set { + self.progressLayer.lineWidth = newValue + self.updatePath() + } + } + + /// Sets the line color of the spinner's circle. + public var lineColor: UIColor? { + get { + guard let color = self.progressLayer.strokeColor else { + return nil + } + + return UIColor.init(cgColor: color) + } + + set (newColor) { + self.progressLayer.strokeColor = newColor?.cgColor + } + } + + public override init(frame: CGRect) { + super.init(frame: frame) + self.setup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + // Supports an Interface Builder archive, or nib file. + } + + public override func awakeFromNib() { + super.awakeFromNib() + self.setup() + } + + private func setup() { + self.layer.addSublayer(self.progressLayer) + + let selector = #selector(ZIndefiniteAnimatedSpinner.resetAnimations) + let name = UIApplication.didBecomeActiveNotification + NotificationCenter.default.addObserver(self, selector: selector, name: name, object: nil) + } + + /// Starts animation of the spinner. + public func startAnimating() { + if self.isAnimating { + return + } + + self.isAnimating = true + self.addLayerAnimations() + self.isHidden = false + } + + /// Stops animation of the spinnner. + public func stopAnimating() { + if !self.isAnimating { + return + } + + self.isAnimating = false + + self.progressLayer.removeAnimation(forKey: AnimationKey.rotation) + self.progressLayer.removeAnimation(forKey: AnimationKey.stroke) + + if self.hidesWhenStopped { + self.isHidden = true + } + } + + private func addLayerAnimations() { + let animation = CABasicAnimation() + animation.keyPath = "transform.rotation" + animation.duration = 2.0 + animation.fromValue = NSNumber(value: 0.0) + animation.toValue = NSNumber(value: 2*Double.pi) + animation.repeatCount = Float.infinity + self.progressLayer.add(animation, forKey: AnimationKey.rotation) + + let headAnimation = CABasicAnimation() + headAnimation.keyPath = "strokeStart" + headAnimation.duration = 1.0 + headAnimation.fromValue = NSNumber(value: 0.0) + headAnimation.toValue = NSNumber(value: 0.25) + headAnimation.timingFunction = self.timingFunction + + let tailAnimation = CABasicAnimation() + tailAnimation.keyPath = "strokeEnd" + tailAnimation.duration = 1.0 + tailAnimation.fromValue = NSNumber(value: 0.0) + tailAnimation.toValue = NSNumber(value: 1.0) + tailAnimation.timingFunction = self.timingFunction + + let endHeadAnimation = CABasicAnimation() + endHeadAnimation.keyPath = "strokeStart" + endHeadAnimation.beginTime = 1.0 + endHeadAnimation.duration = 0.5 + endHeadAnimation.fromValue = NSNumber(value: 0.25) + endHeadAnimation.toValue = NSNumber(value: 1.0) + endHeadAnimation.timingFunction = self.timingFunction + + let endTailAnimation = CABasicAnimation() + endTailAnimation.keyPath = "strokeEnd" + endTailAnimation.beginTime = 1.0 + endTailAnimation.duration = 0.5 + endTailAnimation.fromValue = NSNumber(value: 1.0) + endTailAnimation.toValue = NSNumber(value: 1.0) + endTailAnimation.timingFunction = self.timingFunction + + let animGroup = CAAnimationGroup() + animGroup.repeatCount = Float.infinity + animGroup.duration = 1.5 + animGroup.animations = [headAnimation, + tailAnimation, + endHeadAnimation, + endTailAnimation] + self.progressLayer.add(animGroup, forKey: AnimationKey.stroke) + } + + @objc private func resetAnimations() { + if self.isAnimating { + self.stopAnimating() + self.startAnimating() + } + } + + public override func layoutSubviews() { + super.layoutSubviews() + + let sW = self.bounds.size.width + let sH = self.bounds.size.height + self.progressLayer.frame = CGRect(x: 0, y: 0, width: sW, height: sH) + + self.updatePath() + } + + private func updatePath() { + let sW = self.bounds.size.width + let sH = self.bounds.size.height + + let center = CGPoint(x: sW/2, y: sH/2) + let radius = min(sW/2, sH/2) - self.lineWidth/2 + let startAngle = CGFloat(0.0) + let endAngle = CGFloat(2*Double.pi) + + let path = UIBezierPath(arcCenter: center, + radius: radius, + startAngle: startAngle, + endAngle: endAngle, + clockwise: true) + self.progressLayer.path = path.cgPath + + self.progressLayer.strokeStart = 0.0 + self.progressLayer.strokeEnd = 0.0 + } + + private func executeWhenReleasing() { + self.stopAnimating() + + let name = UIApplication.didBecomeActiveNotification + NotificationCenter.default.removeObserver(self, name: name, object: nil) + } + + deinit { + #if DEBUG + print("[\((#file as NSString).lastPathComponent):\(#function)]") + #endif + self.executeWhenReleasing() + } + +} diff --git a/ios-template/common/ZLoadingView.swift b/ios-template/common/ZLoadingView.swift new file mode 100644 index 0000000..ada3912 --- /dev/null +++ b/ios-template/common/ZLoadingView.swift @@ -0,0 +1,299 @@ +// +// ZLoadingView.swift +// 武极天下 +// +// Created by zhl on 2021/7/22. +// Copyright © 2021 egret. All rights reserved. +// + + + +import UIKit + +/// Creates and returns a color object using the specified opacity and RGB component values. +/// +/// - Parameters: +/// - r: The red value of the color object, specified as a value from 0.0 to 255.0. +/// - g: The green value of the color object, specified as a value from 0.0 to 255.0. +/// - b: The blue value of the color object, specified as a value from 0.0 to 255.0. +/// - alp: The opacity value of the color object, specified as a value from 0.0 to 1.0. +/// - Returns: The color object. The color information represented by this object is in an RGB colorspace. +public func COLOR_RGBA(_ r: CGFloat, + _ g: CGFloat, + _ b: CGFloat, + _ alp: CGFloat) -> UIColor { + + return UIColor(red: r/255.0, green: g/255.0, blue: b/255.0, alpha: alp) +} + +/// Creates and returns a color object using the specified opacity and RGB component values. +/// +/// - Parameters: +/// - r: The red value of the color object, specified as a value from 0.0 to 255.0. +/// - g: The green value of the color object, specified as a value from 0.0 to 255.0. +/// - b: The blue value of the color object, specified as a value from 0.0 to 255.0. +/// - Returns: The color object. The color information represented by this object is in an RGB colorspace. +public func COLOR_RGB(_ r: CGFloat, + _ g: CGFloat, + _ b: CGFloat) -> UIColor { + + return COLOR_RGBA(r, g, b, 1.0) +} + +/// Returns the width of the screen for the device. +public let SCREEN_W = UIScreen.main.bounds.size.width + +/// Returns the height of the screen for the device. +public let SCREEN_H = UIScreen.main.bounds.size.height + +public class ZLoadingView: UIView { + + /// It is used to act as background mask panel. + private lazy var maskPanel: UIView = { + let view = UIView() + view.backgroundColor = COLOR_RGBA(20, 20, 20, 0.5) + return view + }() + + /// It is used to render the content. + private lazy var contentView: UIView = { + let view = UIView() + view.backgroundColor = COLOR_RGB(255, 255, 255) + return view + }() + + /// The spinner is used to provide an indefinite animation. + private lazy var indicator: ZIndefiniteAnimatedSpinner = { + let spinner = ZIndefiniteAnimatedSpinner() + spinner.backgroundColor = UIColor.clear + spinner.lineColor = COLOR_RGB(100, 100, 100) + return spinner + }() + + /// It is used to show the text. + private lazy var textLabel: UILabel = { + let label = UILabel() + label.backgroundColor = UIColor.clear + label.textColor = COLOR_RGB(60, 60, 60) + return label + }() + + /// Returns the current window of the app. + private func appWindow() -> UIWindow { + let sharedApp = UIApplication.shared + return sharedApp.keyWindow ?? sharedApp.windows[0] + } + + /// The color to set the background color of the content view. + public var color: UIColor? { + get { + return self.contentView.backgroundColor + } + + set { + self.contentView.backgroundColor = newValue + } + } + + /// The color to set the line color of the indicator. + public var indicatorColor: UIColor? { + get { + return self.indicator.lineColor + } + + set { + self.indicator.lineColor = newValue + } + } + + /// The color to set the text color of the text label. + public var textColor: UIColor? { + get { + return self.textLabel.textColor + } + + set (newColor) { + self.textLabel.textColor = newColor + } + } + + /// Initializes and returns a newly allocated view object with the specified frame rectangle. + /// - Parameter frame: The frame rectangle for the view. + public override init(frame: CGRect) { + super.init(frame: frame) + } + + /// Returns an object initialized from data in a given unarchiver. + /// - Parameter coder: An unarchiver object. + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + public override func awakeFromNib() { + // Prepares the receiver for service after it has been loaded + // from an Interface Builder archive, or nib file. + } + + /// It will be displayed on the screen with the text. + /// - Parameter text: The text to prompt the user. + public func show(_ text: String) { + self.configure(text) + self.loadView() + self.beginAnimating() + } + + /// Configures properties for the widget used. + private func configure(_ text: String) { + self.autoresizingMask = UIView.AutoresizingMask(rawValue: UIView.AutoresizingMask.flexibleLeftMargin.rawValue | + UIView.AutoresizingMask.flexibleTopMargin.rawValue | + UIView.AutoresizingMask.flexibleWidth.rawValue | + UIView.AutoresizingMask.flexibleHeight.rawValue + ) + + self.maskPanel.autoresizingMask = UIView.AutoresizingMask(rawValue: UIView.AutoresizingMask.flexibleLeftMargin.rawValue | + UIView.AutoresizingMask.flexibleTopMargin.rawValue | + UIView.AutoresizingMask.flexibleWidth.rawValue | + UIView.AutoresizingMask.flexibleHeight.rawValue + ) + + let cw = 200.0 + self.contentView.frame = CGRect(x: 0, y: 0, width: cw, height: 0.6*cw) + self.contentView.setCorner(radius: 10.0) + + let offset = 10.0 + let iw = 60.0 + let ix = cw/2 - iw/2 + let iy = 1.5*offset + self.indicator.frame = CGRect(x: ix, y: iy, width: iw, height: iw) + self.indicator.lineWidth = 2.0 + + let lh = 20.0 + self.textLabel.center = CGPoint(x: cw/2, y: 0.6*cw - lh/2 - 1.5*offset) + self.textLabel.bounds = CGRect(x: 0, y: 0, width: cw - 2*offset, height: lh) + self.textLabel.text = text + self.textLabel.font = UIFont.boldSystemFont(ofSize: 16.0) + self.textLabel.textAlignment = NSTextAlignment.center + self.textLabel.numberOfLines = 1 + } + + /// Addds the subviews to its corresponding superview. + private func loadView() { + if let vc = self.appCurrentViewController() { + + vc.view.addSubview(self) + vc.view.bringSubviewToFront(self) + + } else { + + let window = self.appWindow() + window.addSubview(self) + window.bringSubviewToFront(self) + } + + self.addSubview(self.maskPanel) + + self.addSubview(self.contentView) + self.bringSubviewToFront(self.contentView) + + self.contentView.addSubview(self.indicator) + self.contentView.addSubview(self.textLabel) + } + + /// Prepares to begin animating. + private func beginAnimating() { + self.indicator.startAnimating() + + self.alpha = 0.0 + UIView.animate(withDuration: 0.3) { + self.alpha = 1.0 + } + } + + /// Hides from its own superview. + public func hide() { + let opts = UIView.AnimationOptions.curveEaseInOut + + UIView.animate(withDuration: 0.3, delay: 1.0, options: opts, animations: { + + self.alpha = 0.0 + + }) { (finished) in + + self.indicator.stopAnimating() + self.removeAllViews() + } + } + + /// Removes all views at the end of the hidden animation. + private func removeAllViews() { + + for view in self.subviews { + view.removeFromSuperview() + } + + self.removeFromSuperview() + } + + /// Finds out the current view controller. + private func appCurrentViewController() -> UIViewController? { + + guard var vc = self.appWindow().rootViewController else { + return nil + } + + while true { + if let tvc = vc.presentedViewController { + vc = tvc + } else if vc.isKind(of: UITabBarController.self) { + + let tbc = vc as! UITabBarController + if let tvc = tbc.selectedViewController { + vc = tvc + } + } else if vc.isKind(of: UINavigationController.self) { + + let nc = vc as! UINavigationController + if let tvc = nc.visibleViewController { + vc = tvc + } + } else { + + if vc.children.count > 0 { + if let tvc = vc.children.last { + vc = tvc + } + } + break + } + } + + return vc + } + + public override func layoutSubviews() { + var self_w: CGFloat = 0.0 + var self_h: CGFloat = 0.0 + + if let supv = self.superview { + + self_w = supv.bounds.size.width + self_h = supv.bounds.size.height + + } else { + + self_w = SCREEN_W + self_h = SCREEN_H + } + self.frame = CGRect(x: 0, y: 0, width: self_w, height: self_h) + + self.maskPanel.frame = CGRect(x: 0, y: 0, width: self_w, height: self_h) + self.contentView.center = CGPoint(x: self_w/2, y: self_h/2) + } + + deinit { + #if DEBUG + print("[\((#file as NSString).lastPathComponent):\(#function)]") + #endif + } + +} diff --git a/ios-template/common/ZStoreManager.swift b/ios-template/common/ZStoreManager.swift new file mode 100644 index 0000000..52494c8 --- /dev/null +++ b/ios-template/common/ZStoreManager.swift @@ -0,0 +1,420 @@ +// +// ZStoreManager.swift +// 武极天下 +// +// Created by zhl on 2021/7/22. +// Copyright © 2021 egret. All rights reserved. +// + +import Foundation +import CommonCrypto + +/// Custom method to calculate the SHA-256 hash using Common Crypto. +/// +/// - Parameter s: A string to calculate hash. +/// - Returns: A SHA-256 hash value. +public func Z_SHA256_HashValue(_ s: String) -> String? { + + let digestLength = Int(CC_SHA256_DIGEST_LENGTH) // 32 + + let cStr = s.cString(using: String.Encoding.utf8)! + let cStrLen = Int(s.lengthOfBytes(using: String.Encoding.utf8)) + + // Confirm that the length of C string is small enough + // to be recast when calling the hash function. + if cStrLen > UINT32_MAX { + print("C string too long to hash: \(s)") + return nil + } + + let md = UnsafeMutablePointer.allocate(capacity: digestLength) + + CC_SHA256(cStr, CC_LONG(cStrLen), md) + + // Convert the array of bytes into a string showing its hex represention. + let hash = NSMutableString() + for i in 0.. Any { + return self + } + + /// Make sure the class has only one instance. + open override func mutableCopy() -> Any { + return self + } + + /// Requests payment of the product with the given product identifier, an opaque identifier for the user’s account on your system. + /// + /// - Parameters: + /// - productIdentifier: A given product identifier. + /// - userIdentifier: An opaque identifier for the user’s account on your system. + public func addPayment(_ productIdentifier: String?, userIdentifier: String? = nil) { + self.showLoading("Waiting...") // Initiate purchase request. + DYFStore.default.purchaseProduct(productIdentifier, userIdentifier: userIdentifier) + } + + /// Requests to restore previously completed purchases with an opaque identifier for the user’s account on your system. + /// + /// - Parameter userIdentifier: An opaque identifier for the user’s account on your system. + public func restorePurchases(_ userIdentifier: String? = nil) { + DYFStoreLog("userIdentifier: \(userIdentifier ?? "")") + self.showLoading("Restoring...") + DYFStore.default.restoreTransactions(userIdentifier: userIdentifier) + } + + private func addStoreObserver() { + NotificationCenter.default.addObserver(self, selector: #selector(ZStoreManager.processPurchaseNotification(_:)), name: DYFStore.purchasedNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(ZStoreManager.processDownloadNotification(_:)), name: DYFStore.downloadedNotification, object: nil) + } + + private func removeStoreObserver() { + NotificationCenter.default.removeObserver(self, name: DYFStore.purchasedNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: DYFStore.downloadedNotification, object: nil) + } + + @objc private func processPurchaseNotification(_ notification: Notification) { + + self.hideLoading() + + self.purchaseInfo = (notification.object as! DYFStore.NotificationInfo) + + switch self.purchaseInfo.state! { + case .purchasing: + self.showLoading("Purchasing...") + break + case .cancelled: + self.sendNotice("You cancel the purchase") + break + case .failed: + self.sendNotice(String(format: "An error occurred, \(self.purchaseInfo.error!.code)")) + break + case .succeeded, .restored: + self.completePayment() + break + case .restoreFailed: + self.sendNotice(String(format: "An error occurred, \(self.purchaseInfo.error!.code)")) + break + case .deferred: + DYFStoreLog("Deferred") + break + } + + } + + @objc private func processDownloadNotification(_ notification: Notification) { + + self.downloadInfo = (notification.object as! DYFStore.NotificationInfo) + + switch self.downloadInfo.downloadState! { + case .started: + DYFStoreLog("The download started") + break + case .inProgress: + DYFStoreLog("The download progress: \(self.downloadInfo.downloadProgress)%%") + break + case .cancelled: + DYFStoreLog("The download cancelled") + break + case .failed: + DYFStoreLog("The download failed") + break + case .succeeded: + DYFStoreLog("The download succeeded: 100%%") + break + } + } + + private func completePayment() { + + let info = self.purchaseInfo! + let persister = DYFStoreUserDefaultsPersistence() + + let identifier = info.transactionIdentifier! + if !persister.containsTransaction(identifier) { + self.storeReceipt() + return + } + + let transaction = persister.retrieveTransaction(identifier) + if let tx = transaction { + DYFStoreLog("transaction.state: \(tx.state)") + DYFStoreLog("transaction.productIdentifier: \(tx.productIdentifier!)") + DYFStoreLog("transaction.userIdentifier: \(tx.userIdentifier ?? "null")") + DYFStoreLog("transaction.transactionIdentifier: \(tx.transactionIdentifier!)") + DYFStoreLog("transaction.transactionTimestamp: \(tx.transactionTimestamp!)") + DYFStoreLog("transaction.originalTransactionIdentifier: \(tx.originalTransactionIdentifier ?? "null")") + DYFStoreLog("transaction.originalTransactionTimestamp: \(tx.originalTransactionTimestamp ?? "null")") + + if let receiptData = tx.transactionReceipt!.base64DecodedData() { + DYFStoreLog("transaction.transactionReceipt: \(receiptData)") + self.verifyReceipt(receiptData) + } + } + + } + + private func storeReceipt() { + DYFStoreLog() + + guard let url = DYFStore.receiptURL() else { + self.refreshReceipt() + return + } + + do { + let data = try Data(contentsOf: url) + + let info = self.purchaseInfo! + let persister = DYFStoreUserDefaultsPersistence() + + let transaction = DYFStoreTransaction() + if info.state! == .succeeded { + transaction.state = DYFStoreTransactionState.purchased.rawValue + } else if info.state! == .restored { + transaction.state = DYFStoreTransactionState.restored.rawValue + } + + transaction.productIdentifier = info.productIdentifier + transaction.userIdentifier = info.userIdentifier + transaction.transactionTimestamp = info.transactionDate?.timestamp() + transaction.transactionIdentifier = info.transactionIdentifier + transaction.originalTransactionTimestamp = info.originalTransactionDate?.timestamp() + transaction.originalTransactionIdentifier = info.originalTransactionIdentifier + + transaction.transactionReceipt = data.base64EncodedString() + persister.storeTransaction(transaction) + + self.verifyReceipt(data) + } catch let error { + + DYFStoreLog("error: \(error.localizedDescription)") + self.refreshReceipt() + + return + } + } + + private func refreshReceipt() { + DYFStoreLog() + self.showLoading("Refresh receipt...") + + DYFStore.default.refreshReceipt(onSuccess: { + self.storeReceipt() + }) { (error) in + self.failToRefreshReceipt() + } + } + + private func failToRefreshReceipt() { + DYFStoreLog() + self.hideLoading() + + self.showAlert(withTitle: NSLocalizedString("Notification", tableName: nil, comment: ""), + message: "Fail to refresh receipt! Please check if your device can access the internet.", + cancelButtonTitle: "Cancel", + cancel: { (cancelAction) in }, + confirmButtonTitle: NSLocalizedString("Retry", tableName: nil, comment: "")) + { (action) in + self.refreshReceipt() + } + } + + // It is better to use your own server to obtain the parameters uploaded from the client to verify the receipt from the app store server (C -> Uploaded Parameters -> S -> App Store S -> S -> Receive And Parse Data -> C). + // If the receipts are verified by your own server, the client needs to upload these parameters, such as: "transaction identifier, bundle identifier, product identifier, user identifier, shared sceret(Subscription), receipt(Safe URL Base64), original transaction identifier(Optional), original transaction time(Optional) and the device information, etc.". + private func verifyReceipt(_ receiptData: Data?) { + DYFStoreLog() + + self.hideLoading() + self.showLoading("Verify receipt...") + + var data: Data! + + if let tempData = receiptData { + data = tempData + } else { + if let url = DYFStore.receiptURL() { + do { + data = try Data(contentsOf: url) + } catch let error { + DYFStoreLog("error: \(error.localizedDescription)") + self.failToRefreshReceipt() + return + } + } + } + DYFStoreLog("data: \(data!)") + + self.receiptVerifier.verifyReceipt(data) + // Only used for receipts that contain auto-renewable subscriptions. + //self.receiptVerifier.verifyReceipt(data, sharedSecret: "A43512564ACBEF687924646CAFEFBDCAEDF4155125657") + } + + private func retryToVerifyReceipt() { + let info = self.purchaseInfo! + let persister = DYFStoreUserDefaultsPersistence() + + let identifier = info.transactionIdentifier! + let transaction = persister.retrieveTransaction(identifier) + if let tx = transaction, let receiptData = tx.transactionReceipt!.base64DecodedData() { + self.verifyReceipt(receiptData) + } + } + + private func sendNotice(_ message: String) { + self.showAlert(withTitle: NSLocalizedString("Notification", tableName: nil, comment: ""), + message: message, + cancelButtonTitle: nil, + cancel: nil, + confirmButtonTitle: NSLocalizedString("I see!", tableName: nil, comment: "")) + { (action) in + DYFStoreLog("alert action title: \(action.title!)") + } + } + + // MARK: - DYFStoreReceiptVerifierDelegate + + public func verifyReceiptDidFinish(_ verifier: DYFStoreReceiptVerifier, didReceiveData data: [String : Any]) { + DYFStoreLog("data: \(data)") + + self.hideLoading() + self.showTipsMessage("Purchase Successfully") + + DispatchQueue.main.asyncAfter(delay: 1.5) { + let info = self.purchaseInfo! + let store = DYFStore.default + let persister = DYFStoreUserDefaultsPersistence() + let identifier = info.transactionIdentifier! + + if info.state! == .restored { + + let transaction = store.extractRestoredTransaction(identifier) + store.finishTransaction(transaction) + + } else { + + let transaction = store.extractPurchasedTransaction(identifier) + // The transaction can be finished only after the client and server adopt secure communication and data encryption and the receipt verification is passed. In this way, we can avoid refreshing orders and cracking in-app purchase. If we were unable to complete the verification, we want `StoreKit` to keep reminding us that there are still outstanding transactions. + store.finishTransaction(transaction) + } + + persister.removeTransaction(identifier) + if let id = info.originalTransactionIdentifier { + persister.removeTransaction(id) + } + } + + } + + public func verifyReceipt(_ verifier: DYFStoreReceiptVerifier, didFailWithError error: NSError) { + + // Prints the reason of the error. + DYFStoreLog("error: \(error.code), \(error.localizedDescription)") + self.hideLoading() + + // An error occurs that has nothing to do with in-app purchase. Maybe it's the internet. + if error.code < 21000 { + + // After several attempts, you can cancel refreshing receipt. + self.showAlert(withTitle: NSLocalizedString("Notification", tableName: nil, comment: ""), + message: "Fail to verify receipt! Please check if your device can access the internet.", + cancelButtonTitle: "Cancel", + cancel: nil, + confirmButtonTitle: NSLocalizedString("Retry", tableName: nil, comment: "")) + { (action) in + DYFStoreLog("alert action title: \(action.title!)") + self.verifyReceipt(nil) + } + + return + } + + self.showTipsMessage("Fail to purchase product!") + + DispatchQueue.main.asyncAfter(delay: 1.5) { + let info = self.purchaseInfo! + let store = DYFStore.default + let persister = DYFStoreUserDefaultsPersistence() + let identifier = info.transactionIdentifier! + + if info.state! == .restored { + + let transaction = store.extractRestoredTransaction(identifier) + store.finishTransaction(transaction) + + } else { + + let transaction = store.extractPurchasedTransaction(identifier) + // The transaction can be finished only after the client and server adopt secure communication and data encryption and the receipt verification is passed. In this way, we can avoid refreshing orders and cracking in-app purchase. If we were unable to complete the verification, we want `StoreKit` to keep reminding us that there are still outstanding transactions. + store.finishTransaction(transaction) + } + + persister.removeTransaction(identifier) + if let id = info.originalTransactionIdentifier { + persister.removeTransaction(id) + } + } + } + +}