Coder Social home page Coder Social logo

tuant2n / dyfstore Goto Github PK

View Code? Open in Web Editor NEW

This project forked from itenfay/dyfstore

0.0 0.0 0.0 248 KB

A lightweight and easy-to-use iOS library for In-App Purchases (Swift). `DYFStore` uses blocks and notifications to wrap StoreKit, provides receipt verification and transaction persistence. ([Objective-C] https://github.com/chenxing640/DYFStoreKit)

Home Page: https://www.jianshu.com/p/de030cd6e4a3

License: Other

Ruby 0.94% Swift 99.06%

dyfstore's Introduction

DYFStore

A lightweight and easy-to-use iOS library for In-App Purchases. (Swift)

DYFStore uses blocks and notifications to wrap StoreKit, provides receipt verification and transaction persistence. DYFStore doesn't require any external dependencies.

License MIT  CocoaPods  CocoaPods 

Chinese Instructions (中文说明)

Features

  • Super simple in-app purchases.
  • Built-in support for remembering your purchases.
  • Built-in receipt validation (remote).
  • Built-in hosted content downloads and notifications.

Group (ID:614799921)

Installation

Using CocoaPods:

pod 'DYFStore', '~> 1.1.5'

Or

pod 'DYFStore'

Or manually add the files from the DYFStore directory.

Check out the wiki for more options.

Usage

Next I'll show you how to use DYFStore.

Initialization

The initialization is as follows.

  • Whether to allow the logs output to the console, set 'true' in debug mode, view the logs of the whole process of in-app purchase, and set 'false' when publishing app in release mode.
  • Adds the observer of transactions and monitors the change of transactions.
  • Instantiates data persistent object and stores the related information of transactions.
  • Follows the agreement DYFStoreAppStorePaymentDelegate and processes payments for products purchased from the App Store.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    // Wether to allow the logs output to console.
    DYFStore.default.enableLog = true

    // Adds an observer that responds to updated transactions to the payment queue.
    // If an application quits when transactions are still being processed, those transactions are not lost. The next time the application launches, the payment queue will resume processing the transactions. Your application should always expect to be notified of completed transactions.
    // 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

    DYFStore.default.keychainPersister = DYFStoreKeychainPersistence()

    return true
}

You can process the purchase which was initiated by user from the App Store and provide your own implementation using the DYFStoreAppStorePaymentDelegate protocol:

// Processes the purchase which was initiated by user from the App Store.
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 = DYF_SHA256_HashValue(accountName) ?? ""
    DYFStoreLog("userIdentifier: \(userIdentifier)")
    
    DYFStore.default.purchaseProduct(product.productIdentifier, userIdentifier: userIdentifier)
}

Request products

There are two strategies for retrieving information about the products from the App Store.

Strategy 1: To begin the purchase process, your app must know its product identifiers. Your app can uses a product identifier to fetch information about product available for sale in the App Store and to submit payment request directly.

@IBAction func fetchesProductAndSubmitsPayment(_ sender: Any) {
    self.showLoading("Loading...")
    
    let productId = "com.hncs.szj.coin42"
    
    DYFStore.default.requestProduct(withIdentifier: productId, success: { (products, invalidIdentifiers) in
        
        self.hideLoading()
        
        if products.count == 1 {
            
            let productId = products[0].productIdentifier
            self.addPayment(productId)
            
        } else {
            
            self.showTipsMessage("There is no this product for sale!")
        }
        
    }) { (error) in
        
        self.hideLoading()
        
        let value = error.userInfo[NSLocalizedDescriptionKey] as? String
        let msg = value ?? "\(error.localizedDescription)"
        self.sendNotice("An error occurs, \(error.code), " + msg)
    }
}

private func addPayment(_ productId: String) {
    
    // Get account name from your own user system.
    let accountName = "Handsome Jon"
    
    // This algorithm is negotiated with server developer.
    let userIdentifier = DYF_SHA256_HashValue(accountName) ?? ""
    DYFStoreLog("userIdentifier: \(userIdentifier)")
    
    DYFStore.default.purchaseProduct(productId, userIdentifier: userIdentifier)
}

Strategy 2: To begin the purchase process, your app must know its product identifiers so it can retrieve information about the products from the App Store and present its store UI to the user. Every product sold in your app has a unique product identifier. Your app uses these product identifiers to fetch information about products available for sale in the App Store, such as pricing, and to submit payment requests when users purchase those products.

func fetchProductIdentifiersFromServer() -> [String] {
    
    let productIds = [
        "com.hncs.szj.coin42",   // 42 gold coins for ¥6.
        "com.hncs.szj.coin210",  // 210 gold coins for ¥30.
        "com.hncs.szj.coin686",  // 686 gold coins for ¥98.
        "com.hncs.szj.coin1386", // 1386 gold coins for ¥198.
        "com.hncs.szj.coin2086", // 2086 gold coins for ¥298.
        "com.hncs.szj.coin4886", // 4886 gold coins for ¥698.
        "com.hncs.szj.vip1",     // non-renewable vip subscription for a month.
        "com.hncs.szj.vip2"      // Auto-renewable vip subscription for three months.
    ]
    
    return productIds
}

@IBAction func fetchesProductsFromAppStore(_ sender: Any) {
    self.showLoading("Loading...")
    
    let productIds = fetchProductIdentifiersFromServer()
    
    DYFStore.default.requestProduct(withIdentifiers: productIds, success: { (products, invalidIdentifiers) in
        
        self.hideLoading()
        
        if products.count > 0 {
            
            self.processData(products)
            
        } else if products.count == 0 &&
            invalidIdentifiers.count > 0 {
            
            // Please check the product information you set up.
            self.showTipsMessage("There are no products for sale!")
        }
        
    }) { (error) in
        
        self.hideLoading()
        
        let value = error.userInfo[NSLocalizedDescriptionKey] as? String
        let msg = value ?? "\(error.localizedDescription)"
        self.sendNotice("An error occurs, \(error.code), " + msg)
    }
}

private func processData(_ products: [SKProduct]) {
    
    var modelArray = [DYFStoreProduct]()
    
    for product in products {
        
        let p = DYFStoreProduct()
        p.identifier = product.productIdentifier
        p.name = product.localizedTitle
        p.price = product.price.stringValue
        p.localePrice = DYFStore.default.localizedPrice(ofProduct: product)
        p.localizedDescription = product.localizedDescription
        
        modelArray.append(p)
    }
    
    self.displayStoreUI(modelArray)
}

private func displayStoreUI(_ dataArray: [DYFStoreProduct]) {
    
    if !DYFStore.canMakePayments() {
        self.showTipsMessage("Your device is not able or allowed to make payments!")
        return
    }
    
    let storeVC = DYFStoreViewController()
    storeVC.dataArray = dataArray
    self.navigationController?.pushViewController(storeVC, animated: true)
}

Add payment

Whether the device is allowed to make payments.

if !DYFStore.canMakePayments() {
    self.showTipsMessage("Your device is not able or allowed to make payments!")
    return
}

Requests payment of the product with the given product identifier.

DYFStore.default.purchaseProduct("com.hncs.szj.coin210")

If you need an opaque identifier for the user’s account on your system to add payment, you can use a one-way hash of the user’s account name to calculate the value for this property.

Calculates the SHA256 hash function:

public func DYF_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<CUnsignedChar>.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..<digestLength {

        // Add a dash every four bytes, for readability.
        if i != 0 && i%4 == 0 {
            //hash.append("-")
        }
        hash.appendFormat("%02x", md[i])
    }

    md.deallocate()

    return hash as String
}

Requests payment of the product with the given product identifier, an opaque identifier for the user’s account on your system.

DYFStore.default.purchaseProduct("com.hncs.szj.coin210", userIdentifier: "A43512564ACBEF687924646CAFEFBDCAEDF4155125657")

Restore transactions

  • Restores transactions without the user account identifier.
DYFStore.default.restoreTransactions()
  • Restores transactions with the user account identifier.
DYFStore.default.restoreTransactions(userIdentifier: "A43512564ACBEF687924646CAFEFBDCAEDF4155125657")

Refresh receipt

If Bundle.main.appStoreReceiptURL is null, you need to create a refresh receipt request to obtain a receipt for a payment transaction.

DYFStore.default.refreshReceipt(onSuccess: {
    self.storeReceipt()
}) { (error) in
    self.failToRefreshReceipt()
}

Notifications

DYFStore sends notifications of StoreKit related events and extends NSNotification to provide relevant information. To receive them, add the observer to a DYFStore manager.

Add the store observer

func addStoreObserver() {
    NotificationCenter.default.addObserver(self, selector: #selector(DYFStoreManager.processPurchaseNotification(_:)), name: DYFStore.purchasedNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(DYFStoreManager.processDownloadNotification(_:)), name: DYFStore.downloadedNotification, object: nil)
}

Remove the store observer

When the application exits, you need to remove the store observer.

func removeStoreObserver() {
    NotificationCenter.default.removeObserver(self, name: DYFStore.purchasedNotification, object: nil)
    NotificationCenter.default.removeObserver(self, name: DYFStore.downloadedNotification, object: nil)
}

Payment transaction notifications

Payment transaction notifications are sent after a payment has been requested or for each restored transaction.

@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
    }

}

Download notifications

@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")
        reak
    case .failed:
        DYFStoreLog("The download failed")
        break
    case .succeeded:
        DYFStoreLog("The download succeeded: 100%%")
        break
    }
}

Receipt verification

DYFStore doesn't perform receipt verification by default, but provides reference implementations. You can implement your own custom verification or use the reference verifier provided by the library.

The reference verifier is outlined below. For more info, check out the wiki.

Reference verifier

You create and return a receipt verifier(DYFStoreReceiptVerifier) by using lazy loading.

lazy var receiptVerifier: DYFStoreReceiptVerifier = {
    let verifier = DYFStoreReceiptVerifier()
    verifier.delegate = self
    return verifier
}()

The receipt verifier delegates receipt verification, enabling you to provide your own implementation using the DYFStoreReceiptVerifierDelegate protocol:

@objc func verifyReceiptDidFinish(_ verifier: DYFStoreReceiptVerifier, didReceiveData data: [String : Any])

@objc func verifyReceipt(_ verifier: DYFStoreReceiptVerifier, didFailWithError error: NSError)

You can start verifying the in-app purchase receipt.

// Fetches the data of the bundle’s App Store receipt. 
let data = receiptData

self.receiptVerifier.verifyReceipt(data)

// Only used for receipts that contain auto-renewable subscriptions.
//self.receiptVerifier.verifyReceipt(data, sharedSecret: "A43512564ACBEF687924646CAFEFBDCAEDF4155125657")

If security is a concern you might want to avoid using an open source verification logic, and provide your own custom verifier instead.

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, C: client, S: server).

Finish transactions

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.

DYFStore.default.finishTransaction(transaction)

Transaction persistence

DYFStore provides two optional reference implementations for storing transactions in the Keychain(DYFStoreKeychainPersistence) or in NSUserDefaults(DYFStoreUserDefaultsPersistence).

When the client crashes during the payment process, it is particularly important to store transaction information. When storekit notifies the uncompleted payment again, it takes the data directly from keychain and performs the receipt verification until the transaction is completed.

Store transaction

func storeReceipt() {

    guard let url = DYFStore.receiptURL() else {
        self.refreshReceipt()
        return
    }
    
    do {
        let data = try Data(contentsOf: url)
        
        let info = self.purchaseInfo!
        let store = DYFStore.default
        let persister = store.keychainPersister!
        
        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)
        
        // Makes the backup data.
        let uPersister = DYFStoreUserDefaultsPersistence()
        if !uPersister.containsTransaction(info.transactionIdentifier!) {
            uPersister.storeTransaction(transaction)
        }
        
        self.verifyReceipt(data)
    } catch let error {
        
        DYFStoreLog("error: \(error.localizedDescription)")
        self.refreshReceipt()
        
        return
    }
}

Remove transaction

DispatchQueue.main.asyncAfter(delay: 1.5) {
    let info = self.purchaseInfo!
    let store = DYFStore.default
    let persister = store.keychainPersister!
    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)
    }
}

Requirements

DYFStore requires iOS 8.0 or above and ARC.

Demo

To learn more, please clone this project (git clone https://github.com/dgynfi/DYFStore.git) to the local directory.

Feedback is welcome

If you notice any issue, got stuck or just want to chat feel free to create an issue. I will be happy to help you.

dyfstore's People

Contributors

itenfay avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.