├── app └── app_delegate.rb ├── Gemfile ├── resources └── Default-568h@2x.png ├── .travis.yml ├── .gitignore ├── Rakefile ├── Gemfile.lock ├── lib ├── ProMotion-iap.rb └── project │ ├── product.rb │ └── iap.rb ├── ProMotion-iap.gemspec ├── spec ├── iap_product_spec.rb ├── restore_iaps_spec.rb ├── retrieve_iaps_spec.rb └── purchase_iap_spec.rb └── README.md /app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Define all dependencies in your .gemspec file 4 | gemspec 5 | -------------------------------------------------------------------------------- /resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinitered/ProMotion-iap/HEAD/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | before_install: 3 | - sudo chown -R travis ~/Library/RubyMotion 4 | - mkdir -p ~/Library/RubyMotion/build 5 | - sudo motion update 6 | gemfile: 7 | - Gemfile 8 | script: 9 | - bundle install 10 | - bundle exec rake spec 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | build 3 | tags 4 | app/pixate_code.rb 5 | resources/*.nib 6 | resources/*.momd 7 | resources/*.storyboardc 8 | .DS_Store 9 | nbproject 10 | .redcar 11 | #*# 12 | *~ 13 | *.sw[po] 14 | *.gem 15 | .eprj 16 | .sass-cache 17 | .idea 18 | .dat*.* 19 | .ruby-version 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | require 'motion/project/template/ios' 4 | require 'bundler' 5 | Bundler.require(:development) 6 | require 'ProMotion-iap' 7 | 8 | Motion::Project::App.setup do |app| 9 | # Use `rake config' to see complete project settings. 10 | app.name = 'ProMotion-iap' 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ProMotion-iap (0.2.1.beta1) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | motion-redgreen (1.0.0) 10 | motion-stump (0.3.2) 11 | rake (10.4.2) 12 | 13 | PLATFORMS 14 | ruby 15 | 16 | DEPENDENCIES 17 | ProMotion-iap! 18 | motion-redgreen (~> 1.0) 19 | motion-stump (~> 0.3) 20 | rake 21 | -------------------------------------------------------------------------------- /lib/ProMotion-iap.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | unless defined?(Motion::Project::Config) 4 | raise "ProMotion-iap must be required within a RubyMotion project." 5 | end 6 | 7 | Motion::Project::App.setup do |app| 8 | lib_dir_path = File.dirname(File.expand_path(__FILE__)) 9 | app.files.unshift File.join(lib_dir_path, "project/product.rb") 10 | app.files.unshift File.join(lib_dir_path, "project/iap.rb") 11 | app.frameworks << "StoreKit" 12 | end 13 | -------------------------------------------------------------------------------- /lib/project/product.rb: -------------------------------------------------------------------------------- 1 | module ProMotion 2 | class IAP::Product 3 | include ProMotion::IAP 4 | 5 | attr_reader :product_id 6 | 7 | def initialize(product_id) 8 | @product_id = product_id 9 | end 10 | 11 | def retrieve(&callback) 12 | retrieve_iaps(product_id) do |products, error| 13 | callback.call products.first, error 14 | end 15 | end 16 | 17 | def purchase(&callback) 18 | purchase_iaps(product_id, &callback) 19 | end 20 | 21 | def restore(&callback) 22 | restore_iaps(product_id) do |status, products| 23 | product = products.is_a?(Hash) ? products : products.find{|p| p[:product_id] == product_id } 24 | callback.call status, product 25 | end 26 | end 27 | end 28 | end 29 | ::PM = ProMotion unless defined?(PM) 30 | -------------------------------------------------------------------------------- /ProMotion-iap.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | Gem::Specification.new do |spec| 3 | spec.name = "ProMotion-iap" 4 | spec.version = "0.2.1" 5 | spec.authors = ["Jamon Holmgren", "Kevin VanGelder"] 6 | spec.email = ["jamon@clearsightstudio.com", "kevin@clearsightstudio.com"] 7 | spec.description = %q{Adds in-app purchase support to ProMotion.} 8 | spec.summary = %q{Adds in-app purchase support to ProMotion.} 9 | spec.homepage = "https://github.com/clearsightstudio/ProMotion-iap" 10 | spec.license = "MIT" 11 | 12 | files = [] 13 | files << 'README.md' 14 | files.concat(Dir.glob('lib/**/*.rb')) 15 | spec.files = files 16 | spec.test_files = spec.files.grep(%r{^(spec)/}) 17 | spec.require_paths = ["lib"] 18 | 19 | spec.add_development_dependency "motion-stump", "~> 0.3" 20 | spec.add_development_dependency "motion-redgreen", "~> 1.0" 21 | spec.add_development_dependency "rake" 22 | end 23 | -------------------------------------------------------------------------------- /spec/iap_product_spec.rb: -------------------------------------------------------------------------------- 1 | describe PM::IAP::Product do 2 | 3 | 4 | it "#retrieve" do 5 | subject = PM::IAP::Product.new("retrieveid") 6 | subject.mock!(:retrieve_iaps) do |product_ids, &callback| 7 | product_ids.should.include "retrieveid" 8 | end 9 | subject.retrieve do |products, error| 10 | end 11 | 12 | end 13 | 14 | it "#purchase" do 15 | subject = PM::IAP::Product.new("purchaseid") 16 | subject.mock!(:purchase_iaps) do |product_ids, &callback| 17 | product_ids.should.include "purchaseid" 18 | end 19 | subject.purchase do |status, transaction| 20 | end 21 | end 22 | 23 | it "#restore" do 24 | subject = PM::IAP::Product.new("restoreid") 25 | subject.mock!(:restore_iaps) do |product_ids, &callback| 26 | product_ids.should.include "restoreid" 27 | callback.call(:restored, [{product_id: "restoreid2"}, {product_id: "restoreid"}, {product_id: "restoreid4"}]) 28 | end 29 | subject.restore do |status, product| 30 | status.should == :restored 31 | product.should == { product_id: "restoreid" } 32 | end 33 | end 34 | 35 | end -------------------------------------------------------------------------------- /spec/restore_iaps_spec.rb: -------------------------------------------------------------------------------- 1 | describe "#restore_iaps" do 2 | class TestIAP 3 | include PM::IAP 4 | end 5 | 6 | mock_transaction = Struct.new(:transactionState, :error, :payment) do 7 | def matchingIdentifier; end 8 | end 9 | mock_payment = Struct.new(:productIdentifier) 10 | 11 | it "responds to #restore_iaps" do 12 | TestIAP.new.respond_to?(:restore_iaps).should.be.true 13 | end 14 | 15 | context "restored transaction" do 16 | restored_transaction = mock_transaction.new(SKPaymentTransactionStateRestored, Struct.new(:code).new(nil), mock_payment.new("restoredproductid")) 17 | 18 | it "returns success" do 19 | callback_called = false 20 | subject = TestIAP.new 21 | subject.mock!(:completion_handlers, return: { 22 | "restore-restoredproductid" => ->(status, data) { 23 | status.should == :restored 24 | data[:product_id].should == "restoredproductid" 25 | data[:error].code.should.be.nil 26 | data[:transaction].transactionState.should == SKPaymentTransactionStateRestored 27 | callback_called = true 28 | }, 29 | }) 30 | subject.paymentQueue(nil, updatedTransactions:[ restored_transaction ]) 31 | callback_called.should.be.true 32 | end 33 | end 34 | 35 | context "error in restore" do 36 | restored_transaction = mock_transaction.new(SKPaymentTransactionStateRestored, Struct.new(:code).new(nil), mock_payment.new("restoredproductid")) 37 | 38 | it "returns success" do 39 | callback_called = false 40 | subject = TestIAP.new 41 | subject.mock!(:completion_handlers, return: { 42 | "restore-all" => ->(status, data) { 43 | status.should == :error 44 | data[:product_id].should.be.nil 45 | data[:error].localizedDescription.should == "Failed to restore" 46 | data[:transaction].should.be.nil 47 | callback_called = true 48 | }, 49 | }) 50 | subject.paymentQueue(nil, restoreCompletedTransactionsFailedWithError:Struct.new(:localizedDescription).new("Failed to restore")) 51 | callback_called.should.be.true 52 | end 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /spec/retrieve_iaps_spec.rb: -------------------------------------------------------------------------------- 1 | describe "#retrieve_iaps" do 2 | class TestIAP 3 | include PM::IAP 4 | end 5 | 6 | mock_product = Struct.new(:productIdentifier, :localizedTitle, :localizedDescription, :price, :priceLocale, :isDownloadable, :downloadContentLengths, :downloadContentVersion) 7 | 8 | it "responds to #retrieve_iaps" do 9 | TestIAP.new.respond_to?(:retrieve_iaps).should.be.true 10 | end 11 | 12 | context "successful request" do 13 | 14 | it "returns an array of hashes with the result" do 15 | subject = TestIAP.new 16 | 17 | mock_response = Struct.new(:products, :error, :invalidProductIdentifiers).new([ 18 | prod = mock_product.new("id", "title", "desc", BigDecimal.new("0.99"), NSLocale.alloc.initWithLocaleIdentifier("en_US@currency=USD"), false, 0, nil) 19 | ], nil, []) 20 | 21 | subject.mock!(:completion_handlers, return: { "retrieve_iaps" => ->(products, error) { 22 | products.length.should == 1 23 | product = products.first 24 | product[:product_id].should == "id" 25 | product[:title].should == "title" 26 | product[:description].should == "desc" 27 | product[:price].should == BigDecimal.new("0.99") 28 | product[:formatted_price].should == "$0.99" 29 | product[:price_locale].should == NSLocale.alloc.initWithLocaleIdentifier("en_US@currency=USD") 30 | product[:downloadable].should == false 31 | product[:download_content_lengths].should == 0 32 | product[:download_content_version].should == nil 33 | product[:product].should == prod 34 | 35 | error.should.be.nil 36 | } }) 37 | 38 | subject.productsRequest(nil, didReceiveResponse:mock_response) 39 | end 40 | 41 | end 42 | 43 | context "unsuccessful request" do 44 | 45 | subject = TestIAP.new 46 | mock_response = Struct.new(:products, :error, :invalidProductIdentifiers).new([], "Invalid product ID", ["invalidproductid"]) 47 | 48 | it "fails with error" do 49 | subject.mock!(:completion_handlers, return: { 50 | "retrieve_iaps" => ->(products, error) { 51 | products.length.should == 0 52 | error.error.should == "Invalid product ID" 53 | }, 54 | }) 55 | subject.request(nil, didFailWithError:mock_response) 56 | end 57 | 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/purchase_iap_spec.rb: -------------------------------------------------------------------------------- 1 | describe "#purchase_iap" do 2 | class TestIAP 3 | include PM::IAP 4 | end 5 | 6 | mock_transaction = Struct.new(:transactionState, :error, :payment) do 7 | def matchingIdentifier; end 8 | end 9 | mock_payment = Struct.new(:productIdentifier) 10 | 11 | it "responds to #purchase_iap" do 12 | TestIAP.new.respond_to?(:purchase_iap).should.be.true 13 | end 14 | 15 | context "successful transaction" do 16 | successful_transaction = mock_transaction.new(SKPaymentTransactionStatePurchased, Struct.new(:code).new(nil), mock_payment.new("successfulproductid")) 17 | 18 | it "returns success" do 19 | called_callback = false 20 | subject = TestIAP.new 21 | subject.mock!(:completion_handlers, return: { 22 | "purchase-successfulproductid" => ->(status, data) { 23 | status.should === :purchased 24 | data[:transaction].transactionState.should == SKPaymentTransactionStatePurchased 25 | data[:error].code.should.be.nil 26 | called_callback = true 27 | }, 28 | }) 29 | subject.paymentQueue(nil, updatedTransactions:[ successful_transaction ]) 30 | called_callback.should.be.true 31 | end 32 | end 33 | 34 | context "canceled transaction" do 35 | canceled_transaction = mock_transaction.new(SKPaymentTransactionStateFailed, Struct.new(:code).new(SKErrorPaymentCancelled), mock_payment.new("canceledproductid")) 36 | 37 | it "returns nil error" do 38 | called_callback = false 39 | subject = TestIAP.new 40 | subject.mock!(:completion_handlers, return: { 41 | "purchase-canceledproductid" => ->(status, data) { 42 | status.should == :canceled 43 | data[:transaction].should == canceled_transaction 44 | data[:error].code.should == SKErrorPaymentCancelled 45 | called_callback = true 46 | }, 47 | }) 48 | subject.paymentQueue(nil, updatedTransactions:[ canceled_transaction ]) 49 | called_callback.should.be.true 50 | end 51 | end 52 | 53 | context "invalid product" do 54 | invalid_transaction = mock_transaction.new(SKPaymentTransactionStateFailed, Struct.new(:code).new(nil), mock_payment.new("invalidproductid")) 55 | 56 | it "returns an error" do 57 | called_callback = false 58 | subject = TestIAP.new 59 | subject.mock!(:completion_handlers, return: { 60 | "purchase-invalidproductid" => ->(status, data) { 61 | status.should == :error 62 | data[:transaction].should == invalid_transaction 63 | data[:error].code.should.be.nil 64 | called_callback = true 65 | }, 66 | }) 67 | subject.paymentQueue(nil, updatedTransactions:[ invalid_transaction ]) 68 | called_callback.should.be.true 69 | end 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /lib/project/iap.rb: -------------------------------------------------------------------------------- 1 | module ProMotion 2 | module IAP 3 | attr_accessor :completion_handlers 4 | 5 | def purchase_iaps(product_ids, options={}, &callback) 6 | iap_setup 7 | retrieve_iaps product_ids do |products| 8 | products.each do |product| 9 | self.completion_handlers["purchase-#{product[:product_id]}"] = callback 10 | 11 | payment = SKMutablePayment.paymentWithProduct(product[:product]) 12 | payment.applicationUsername = options[:username] if options[:username] 13 | 14 | SKPaymentQueue.defaultQueue.addPayment(payment) 15 | end 16 | end 17 | end 18 | alias purchase_iap purchase_iaps 19 | 20 | def restore_iaps(product_ids, options={}, &callback) 21 | iap_setup 22 | retrieve_iaps Array(product_ids) do |products| 23 | products.each do |product| 24 | self.completion_handlers["restore-#{product[:product_id]}"] = callback 25 | end 26 | self.completion_handlers["restore-all"] = callback # In case of error 27 | 28 | if options[:username] 29 | SKPaymentQueue.defaultQueue.restoreCompletedTransactionsWithApplicationUsername(options[:username]) 30 | else 31 | SKPaymentQueue.defaultQueue.restoreCompletedTransactions 32 | end 33 | end 34 | end 35 | alias restore_iap restore_iaps 36 | 37 | def retrieve_iaps(*product_ids, &callback) 38 | iap_setup 39 | self.completion_handlers["retrieve_iaps"] = callback 40 | @products_request = SKProductsRequest.alloc.initWithProductIdentifiers(NSSet.setWithArray(product_ids.flatten)) 41 | @products_request.delegate = self 42 | @products_request.start 43 | end 44 | alias retrieve_iap retrieve_iaps 45 | 46 | def completion_handlers 47 | @completion_handlers ||= {} 48 | end 49 | 50 | private 51 | 52 | def iap_setup 53 | SKPaymentQueue.defaultQueue.addTransactionObserver(self) 54 | end 55 | 56 | def iap_shutdown 57 | @completion_handlers = nil 58 | SKPaymentQueue.defaultQueue.removeTransactionObserver(self) 59 | end 60 | 61 | def retrieved_iaps_handler(products, &callback) 62 | sk_products = products.map do |sk_product| 63 | { 64 | product_id: sk_product.productIdentifier, 65 | title: sk_product.localizedTitle, 66 | description: sk_product.localizedDescription, 67 | formatted_price: formatted_iap_price(sk_product.price, sk_product.priceLocale), 68 | price: sk_product.price, 69 | price_locale: sk_product.priceLocale, 70 | downloadable: sk_product.isDownloadable, 71 | download_content_lengths: sk_product.downloadContentLengths, 72 | download_content_version: sk_product.downloadContentVersion, 73 | product: sk_product, 74 | } 75 | end 76 | 77 | callback.call(sk_products, nil) if callback.arity == 2 78 | callback.call(sk_products) if callback.arity < 2 79 | end 80 | 81 | def formatted_iap_price(price, price_locale) 82 | num_formatter = NSNumberFormatter.new 83 | num_formatter.setFormatterBehavior NSNumberFormatterBehaviorDefault 84 | num_formatter.setNumberStyle NSNumberFormatterCurrencyStyle 85 | num_formatter.setLocale price_locale 86 | num_formatter.stringFromNumber price 87 | end 88 | 89 | def iap_callback(status, transaction, finish=false) 90 | product_id = transaction_product_id(transaction) 91 | 92 | if self.completion_handlers["purchase-#{product_id}"] 93 | self.completion_handlers["purchase-#{product_id}"].call status, mapped_transaction(transaction) 94 | self.completion_handlers["purchase-#{product_id}"] = nil if finish 95 | end 96 | 97 | if self.completion_handlers["restore-#{product_id}"] 98 | self.completion_handlers["restore-#{product_id}"].call status, mapped_transaction(transaction) 99 | self.completion_handlers["restore-#{product_id}"] = nil if finish 100 | end 101 | 102 | SKPaymentQueue.defaultQueue.finishTransaction(transaction) if finish 103 | end 104 | 105 | def mapped_transaction(transaction) 106 | if transaction.respond_to?(:payment) 107 | { 108 | product_id: transaction.payment.productIdentifier, 109 | error: transaction.error, 110 | transaction: transaction 111 | } 112 | else 113 | { 114 | product_id: nil, 115 | error: transaction, 116 | transaction: nil 117 | } 118 | end 119 | end 120 | 121 | def transaction_product_id(transaction) 122 | transaction.respond_to?(:payment) ? transaction.payment.productIdentifier : "all" 123 | end 124 | 125 | def transaction_complete?(transactions) 126 | states = transactions.map(&:transactionState) 127 | return true unless states.include?(SKPaymentTransactionStatePurchasing) 128 | false 129 | end 130 | 131 | public 132 | 133 | # SKProductsRequestDelegate methods 134 | 135 | def productsRequest(_, didReceiveResponse:response) 136 | unless response.invalidProductIdentifiers.empty? 137 | red = "\e[0;31m" 138 | color_off = "\e[0m" 139 | end 140 | retrieved_iaps_handler(response.products, &self.completion_handlers["retrieve_iaps"]) if self.completion_handlers["retrieve_iaps"] 141 | @products_request = nil 142 | self.completion_handlers["retrieve_iaps"] = nil 143 | end 144 | 145 | def request(_, didFailWithError:error) 146 | self.completion_handlers["retrieve_iaps"].call([], error) if self.completion_handlers["retrieve_iaps"].arity == 2 147 | self.completion_handlers["retrieve_iaps"].call([]) if self.completion_handlers["retrieve_iaps"].arity < 2 148 | @products_request = nil 149 | self.completion_handlers["retrieve_iaps"] = nil 150 | end 151 | 152 | # SKPaymentTransactionObserver methods 153 | 154 | def paymentQueue(_, updatedTransactions:transactions) 155 | transactions.each do |transaction| 156 | case transaction.transactionState 157 | when SKPaymentTransactionStatePurchasing then iap_callback(:in_progress, transaction) 158 | when SKPaymentTransactionStateDeferred then iap_callback(:deferred, transaction) 159 | when SKPaymentTransactionStatePurchased then iap_callback(:purchased, transaction, true) 160 | when SKPaymentTransactionStateRestored then iap_callback(:restored, transaction, true) 161 | when SKPaymentTransactionStateFailed 162 | if transaction.error.code == SKErrorPaymentCancelled 163 | iap_callback(:canceled, transaction, true) 164 | else 165 | iap_callback(:error, transaction, true) 166 | end 167 | end 168 | end 169 | iap_shutdown if transaction_complete?(transactions) 170 | end 171 | 172 | def paymentQueue(_, restoreCompletedTransactionsFailedWithError:error) 173 | iap_callback(:error, error) 174 | end 175 | 176 | end 177 | end 178 | ::PM = ProMotion unless defined?(PM) 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProMotion-iap 2 | 3 | [![Gem Version](https://badge.fury.io/rb/ProMotion-iap.svg)](http://badge.fury.io/rb/ProMotion-iap) 4 | [![Build Status](https://travis-ci.org/infinitered/ProMotion-iap.svg)](https://travis-ci.org/infinitered/ProMotion-iap) 5 | 6 | ProMotion-iap is in-app purchase notification support for the 7 | popular RubyMotion gem [ProMotion](https://github.com/infinitered/ProMotion). It also works as a stand-alone gem if you are not using ProMotion. 8 | 9 | Read the introductory blog post here: [http://jamonholmgren.com/iap/](http://jamonholmgren.com/iap/) 10 | 11 | ProMotion-iap was created by [Infinite Red](http://infinite.red), a web and mobile development company based in Portland, OR and San Francisco, CA. While you're welcome to use it, please note that we rely on the community to maintain it. We are happy to merge pull requests and release new versions but are no longer driving primary development. 12 | 13 | ## Installation 14 | 15 | ```ruby 16 | gem 'ProMotion-iap' 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### PM::IAP::Product Class 22 | 23 | The `Product` class is an abstraction layer that provides a simpler interface when working with a single IAP product. 24 | If you are dealing with multiple products you will want to use the IAP Module directly (documented below). 25 | 26 | ```ruby 27 | class PurchaseScreen < PM::Screen 28 | 29 | def on_load 30 | 31 | product = PM::IAP::Product.new("productid") 32 | 33 | product.retrieve do |product, error| 34 | # product looks something like the following 35 | { 36 | product_id: "productid1", 37 | title: "title", 38 | description: "description", 39 | price: , 40 | formatted_price: "$0.99", 41 | price_locale: , 42 | downloadable: false, 43 | download_content_lengths: , # TODO: ? 44 | download_content_version: , # TODO: ? 45 | product: 46 | } 47 | end 48 | 49 | product.purchase do |status, data| 50 | case status 51 | when :in_progress 52 | # Usually do nothing, maybe a spinner 53 | when :deferred 54 | # Waiting on a prompt to the user 55 | when :purchased 56 | # Notify the user, update any affected UI elements 57 | when :canceled 58 | # They just canceled, no big deal. 59 | when :error 60 | # Failed to purchase 61 | data[:error].localizedDescription # => error message 62 | end 63 | end 64 | 65 | product.restore do |status, data| 66 | if status == :restored 67 | # Update your UI, notify the user 68 | # data is a hash with :product_id, :error, and :transaction 69 | else 70 | # Tell the user it failed to restore 71 | end 72 | end 73 | 74 | end 75 | end 76 | 77 | ``` 78 | 79 | #### Product.new(product_id) 80 | 81 | Stores the product_id for use in the instance methods. 82 | 83 | #### retrieve(&callback) 84 | 85 | Retrieves the product. 86 | 87 | #### purchase(&callback) 88 | 89 | Begins a purchase of the product. 90 | 91 | #### restore(&callback) 92 | 93 | Begins a restoration of the previously purchased product. 94 | 95 | 96 | ### IAP Module 97 | 98 | Include `PM::IAP` to add some in-app purchase methods to a screen, app delegate, or other class. 99 | 100 | ```ruby 101 | # app/screens/purchase_screen.rb 102 | class PurchaseScreen < PM::Screen 103 | include PM::IAP 104 | 105 | def on_load 106 | 107 | retrieve_iaps [ "productid1", "productid2" ] do |products, error| 108 | # products looks something like the following 109 | [{ 110 | product_id: "productid1", 111 | title: "title", 112 | description: "description", 113 | price: , 114 | formatted_price: "$0.99", 115 | price_locale: , 116 | downloadable: false, 117 | download_content_lengths: , # TODO: ? 118 | download_content_version: , # TODO: ? 119 | product: 120 | }, {...}] 121 | end 122 | 123 | purchase_iaps [ "productid" ], username: User.current.username do |status, transaction| 124 | case status 125 | when :in_progress 126 | # Usually do nothing, maybe a spinner 127 | when :deferred 128 | # Waiting on a prompt to the user 129 | when :purchased 130 | # Notify the user, update any affected UI elements 131 | when :canceled 132 | # They just canceled, no big deal. 133 | when :error 134 | # Failed to purchase 135 | transaction.error.localizedDescription # => error message 136 | end 137 | end 138 | 139 | restore_iaps [ "productid" ], username: User.current.username do |status, data| 140 | if status == :restored 141 | # Update your UI, notify the user 142 | # This is called once for each product you're trying to restore. 143 | data[:product_id] # => "productid" 144 | elsif status == :error 145 | # Update your UI to show that there was an error. 146 | # This will only be called once, no matter how many products you are trying to restore. 147 | # You'll get an NSError in your `data` hash. 148 | data[:error].localizedDescription # => description of error 149 | end 150 | end 151 | 152 | 153 | end 154 | end 155 | ``` 156 | 157 | #### retrieve_iaps(`*`product_ids, &callback) 158 | 159 | Retrieves in-app purchase products in an array of mapped hashes. The callback method should accept `products` and `error`. 160 | 161 | 162 | #### purchase_iaps(`*`product_ids, &callback) 163 | 164 | Prompts the user to login to their Apple ID and complete the purchase. The callback method should accept `status` and `transaction`. 165 | The callback method will be called several times with the various statuses in the process. If more than one `product_id` is provided 166 | the callback method will be called several times per product with the applicable transaction. 167 | 168 | 169 | #### restore_iaps(`*`product_ids, &callback) 170 | 171 | Restores a previously purchased IAP to the user (for example if they have upgraded their device). This relies on the Apple ID the user 172 | enters at the prompt. Unfortunately, if there is no purchase to restore for the signed-in account, no error message is generated and 173 | will fail silently. 174 | 175 | 176 | #### Important Note 177 | 178 | In order for this library to work correctly, you must provide your, `app.identifier` to the Rakefile. 179 | 180 | 181 | Find the Product ID here: 182 | 183 | ![product id](http://clrsight.co/jh/2015-02-11-d8xw6.png?+) 184 | 185 | 186 | ## Authors 187 | | Contributor | Twitter | 188 | | Jamon Holmgren | [@jamonholmgren](http://twitter.com/jamonholmgren) | 189 | | Kevin VanGelder | [@kevinvangelder](http://twitter.com/kevin_vangelder) | 190 | 191 | ## Inspired By 192 | - [Helu](https://github.com/ivanacostarubio/helu) 193 | - [Mark Rickert's Code Example](https://github.com/OTGApps/TheShowCloser/blob/master/app/helpers/iap_helper.rb) 194 | 195 | 196 | ## Contributing 197 | 198 | 1. Fork it 199 | 2. Create your feature branch (`git checkout -b my-new-feature`) 200 | 3. Commit your changes (`git commit -am 'Add some feature'`) 201 | 4. Make some specs pass 202 | 5. Push to the branch (`git iap origin my-new-feature`) 203 | 6. Create new Pull Request 204 | --------------------------------------------------------------------------------