{"id":15038080,"url":"https://github.com/jinsasaki/inapppurchase","last_synced_at":"2025-10-04T03:31:56.726Z","repository":{"id":27449634,"uuid":"102446630","full_name":"jinSasaki/InAppPurchase","owner":"jinSasaki","description":"A Simple and Lightweight framework for In App Purchase (Store Kit 1)","archived":true,"fork":false,"pushed_at":"2022-03-04T08:55:29.000Z","size":254,"stargazers_count":289,"open_issues_count":10,"forks_count":30,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-12-29T18:15:55.487Z","etag":null,"topics":["appstore","carthage","inapppurchase","ios","ios11","purchase","store","storekit","swift","swift4"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jinSasaki.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-09-05T07:07:09.000Z","updated_at":"2024-10-21T05:53:23.000Z","dependencies_parsed_at":"2022-08-07T12:16:46.343Z","dependency_job_id":null,"html_url":"https://github.com/jinSasaki/InAppPurchase","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jinSasaki%2FInAppPurchase","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jinSasaki%2FInAppPurchase/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jinSasaki%2FInAppPurchase/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jinSasaki%2FInAppPurchase/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jinSasaki","download_url":"https://codeload.github.com/jinSasaki/InAppPurchase/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":235216187,"owners_count":18954252,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appstore","carthage","inapppurchase","ios","ios11","purchase","store","storekit","swift","swift4"],"created_at":"2024-09-24T20:37:01.163Z","updated_at":"2025-10-04T03:31:51.302Z","avatar_url":"https://github.com/jinSasaki.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# InAppPurchase\n\n[![Build Status](https://travis-ci.org/jinSasaki/InAppPurchase.svg?branch=master)](https://travis-ci.org/jinSasaki/InAppPurchase)\n[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)\n[![Version](https://img.shields.io/cocoapods/v/InAppPurchase.svg?style=flat)](http://cocoapods.org/pods/InAppPurchase)\n[![Platform](https://img.shields.io/cocoapods/p/InAppPurchase.svg?style=flat)](http://cocoapods.org/pods/InAppPurchase)\n[![codecov](https://codecov.io/gh/jinSasaki/InAppPurchase/branch/master/graph/badge.svg)](https://codecov.io/gh/jinSasaki/InAppPurchase)\n\nA Simple, Lightweight and Safe framework for In App Purchase\n\n## Feature\n\n- Simple and Light :+1:\n- Support [Promoting In-App Purchases](https://developer.apple.com/app-store/promoting-in-app-purchases/) :moneybag:\n- No need to consider `StoreKit`! :sunglasses:\n- High coverage and safe :white_check_mark:\n\n## Installation\n\n### Carthage\n\n```txt\ngithub \"jinSasaki/InAppPurchase\"\n```\n\n### CocoaPods\n\n```ruby\npod \"InAppPurchase\"\n```\n\n## Usage\n\n### Setup Observer\n\n**NOTE: This method should be called at launch.**\n\n```swift\nlet iap = InAppPurchase.default\niap.addTransactionObserver(fallbackHandler: {\n    // Handle the result of payment added by Store\n    // See also `InAppPurchase#purchase`\n})\n```\n\nIf you want to detect the unexpected transactions, pass `addTransactionObserver()` with `fallbackHandler`.  \nFor example, your app requested a payment, but it crashed in that process. That transaction is not finished, and then will receive at next launch.  \nThis `fallbackHandler` is called when any handlers are not set to `InAppPurchase` via `purchase(productIdentifier: handler:)` method and so on. \n\n#### Promoting In App Purchases is available from iOS 11. `InAppPurchase` supports it!\n\nAdd observer with `shouldAddStorePaymentHandler`.  \nSee also [`SKPaymentTransactionObserver#paymentQueue(_:shouldAddStorePayment:for:)`](https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver/2877502-paymentqueue)and [Promoting In-App Purchases Guides](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/PromotingIn-AppPurchases/PromotingIn-AppPurchases.html#//apple_ref/doc/uid/TP40008267-CH11-SW1)\n\n![promoting](./assets/promoting.png)\n\n```swift\nlet iap = InAppPurchase.default\niap.set(shouldAddStorePaymentHandler: { (product) -\u003e Bool in\n    // Return whether starting payment\n}, handler: { (result) in\n    // Handle the result of payment added by Store\n    // See also `InAppPurchase#purchase`\n})\n```\n\n**:warning: Do not use `Product#priceLocale`**\n\nOnly if purchase via AppStore Promoting, `SKProduct#priceLocale` has been not initialized. It occurs a BAD_ACCESS crash. This is a StoreKit bug.\nInAppPurchace resolved the crash that is occurred when received the payment, but it occurs when accessed `Product#priceLocale` yet.\nSo, I recommend not to use `Product#priceLocale` in AppStore Promoting Payment process.\n\n#### Stop payment observing if needed.\n\n```swift\nlet iap = InAppPurchase.default\niap.removeTransactionObserver()\n```\n\n### Fetch Product Information\n\n```swift\nlet iap = InAppPurchase.default\niap.fetchProduct(productIdentifiers: [\"PRODUCT_ID\"], handler: { (result) in\n    switch result {\n    case .success(let products):\n        // Use products\n    case .failure(let error):\n        // Handle `InAppPurchase.Error`\n    }\n})\n```\n\n### Restore Completed Transaction\n\n```swift\nlet iap = InAppPurchase.default\niap.restore(handler: { (result) in\n    switch result {\n    case .success(let productIds):\n        // Restored with product ids\n    case .failure(let error):\n        // Handle `InAppPurchase.Error`\n    }\n})\n```\n\n### Purchase\n\n```swift\nlet iap = InAppPurchase.default\niap.purchase(productIdentifier: \"PRODUCT_ID\", handler: { (result) in\n    // This handler is called if the payment purchased, restored, deferred or failed.\n\n    switch result {\n    case .success(let response):\n        // Handle `PaymentResponse`\n    case .failure(let error):\n        // Handle `InAppPurchase.Error`\n    }\n})\n```\n\n### Transaction handling\nIf you want to handle the timing to complete transaction, set `shouldCompleteImmediately` to `false` at initializing.\n\n\n```swift\nlet iap = InAppPurchase(shouldCompleteImmediately: false)\niap.purchase(productIdentifier: \"PRODUCT_ID\", handler: { (result) in\n    // This handler is called if the payment purchased, restored, deferred or failed.\n\n    switch result {\n    case .success(let response):\n        // Handle `PaymentResponse`\n        // MUST: InAppPurchase does not complete transaction, if purchased, restored. Your app must call `InAppPurchase.finish(transaction:)`.\n        if response.state == .purchased || response.state == .restored {\n            iap.finish(transaction: response.transaction)\n        }\n    case .failure(let error):\n        // Handle `InAppPurchase.Error`\n    }\n})\n\n```\n\n### Multiple instances of InAppPurchase\nIf you want to use multiple InAppPurchase, make each instance.  \n**However, be careful the fallback handling because of duplicate handlings.**\n\nThis is duplicate handling example:\n\n```swift\nlet iap1 = InAppPurchase.default\nlet iap2 = InAppPurchase(shouldCompleteImmediately: false)\niap1.addTransactionObserver(fallbackHandler: {\n    // NOT CALLED\n    // This fallback handler is NOT called because the purchase handler is used.\n})\niap2.addTransactionObserver(fallbackHandler: {\n    // CALLED\n    // This fallback handler is called because the purchase handler is not associated to iap2.\n})\niap1.purchase(productIdentifier: \"your.purchase.item1\", handler: { (result) in\n    // CALLED\n})\n\n```\n\nTo avoid this situation, I recommend to **specify product IDs for each instance**.\n\n```swift\nlet iap1 = InAppPurchase(shouldCompleteImmediately: true, productIds: [\"your.purchase.item1\", \"your.purchase.item2\"])\nlet iap2 = InAppPurchase(shouldCompleteImmediately: false, productIds: [\"your.purchase.item3\", \"your.purchase.item4\"])\niap1.addTransactionObserver(fallbackHandler: {\n    // NOT CALLED\n    // This fallback handler is NOT called because the purchase handler is used.\n})\niap2.addTransactionObserver(fallbackHandler: {\n    // NOT CALLED\n    // This fallback handler is NOT called because \"your.purchase.item1\" is not specified for iap2.\n})\niap1.purchase(productIdentifier: \"your.purchase.item1\", handler: { (result) in\n    // CALLED\n})\n\n```\n\nIn addition, if you do not specify `productIds` or set `productIds: nil`, the iap instance allow all product ids.\n\n## For Dependency Injection\n\nThe purchase logic in the App should be safe and testable. \n\nFor example, you implemented a class to execute In-App-Purchase as follows.\n\n```swift\n// PurchaseService.swift\n\nimport Foundation\nimport InAppPurchase\n\nfinal class PurchaseService {\n    static let shared = PurchaseService()\n\n    func purchase() {\n        // Purchase with `InAppPurchase`\n        InAppPurchase.default.purchase(productIdentifier: ...) {\n            // Do something\n        }\n    }\n}\n```\n\nIt is hard to test this class because using the `InAppPurchase.default` in the purchase process.\n\nThis `PurchaseService` can be refactored to inject the dependency.  \nUse `InAppPurchaseProvidable` protocol.\n\n```swift\n// PurchaseService.swift\n\nimport Foundation\nimport InAppPurchase\n\nfinal class PurchaseService {\n    static let shared = PurchaseService()\n\n    let iap: InAppPurchaseProvidable\n\n    init(iap: InAppPurchaseProvidable = InAppPurchase.default) {\n        self.iap = iap\n    }\n\n    func purchase() {\n        // Purchase with `InAppPurchase`\n        iap.purchase(productIdentifier: ...) {\n            // Do something\n        }\n    }\n}\n```\n\nAnd then you can test `PurchaseService` easily with `InAppPurchaseStubs.framework`.\n\n```swift\n// PurchaseServiceTests.swift\n\nimport XCTest\n@testable import YourApp\nimport InAppPurchaseStubs\n\n// Test\nfinal class PurchaseServiceTests: XCTestCase {\n    func testPurchase() {\n        let expectation = self.expectation(description: \"purchase handler was called.\")\n        let iap = StubInAppPurchase(purchaseHandler: { productIdentifier, handler in\n            // Assert productIdentifier, handler, and so on.\n        })\n        let purchaseService = PurchaseService(iap: iap)\n        purchaseService.purchase(productIdentifier: ...) {\n            // Assert result\n            expectation.fulfill()\n        }\n\n        wait(for: [expectation], timeout: 1)\n    }\n}\n```\n\nIf you want more information for test, see also [InAppPurchaseStubs](./InAppPurchaseStubs/Stubs/) and [Tests](./Tests/).\n\n## Requirements\n\n- iOS 9.0+\n- Xcode 9+\n- Swift 4+\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjinsasaki%2Finapppurchase","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjinsasaki%2Finapppurchase","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjinsasaki%2Finapppurchase/lists"}