diff --git a/Gemfile.lock b/Gemfile.lock index 185c788..4a0dc39 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,7 +9,7 @@ GIT PATH remote: . specs: - ProMotion-iap (0.1.0) + ProMotion-iap (0.2.0) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index c4c6d1b..1c730c0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ProMotion-iap [![Gem Version](https://badge.fury.io/rb/ProMotion-iap.svg)](http://badge.fury.io/rb/ProMotion-iap) -[![Build Status](https://travis-ci.org/clearsightstudio/ProMotion-iap.svg)](https://travis-ci.org/clearsightstudio/ProMotion-iap) +[![Build Status](https://travis-ci.org/clearsightstudio/ProMotion-iap.svg)](https://travis-ci.org/clearsightstudio/ProMotion-iap) ProMotion-iap is in-app purchase notification support for the popular RubyMotion gem [ProMotion](https://github.com/clearsightstudio/ProMotion). @@ -42,7 +42,7 @@ class PurchaseScreen < PM::Screen } end - product.purchase do |status, transaction| + product.purchase do |status, data| case status when :in_progress # Usually do nothing, maybe a spinner @@ -54,13 +54,16 @@ class PurchaseScreen < PM::Screen # They just canceled, no big deal. when :error # Failed to purchase - transaction.error.localizedDescription # => error message + data[:error].localizedDescription # => error message end end - product.restore do |status, product| + product.restore do |status, data| if status == :restored # Update your UI, notify the user + # data is a hash with :product_id, :error, and :transaction + else + # Tell the user it failed to restore end end @@ -113,7 +116,7 @@ class PurchaseScreen < PM::Screen }, {...}] end - purchase_iap "productid" do |status, transaction| + purchase_iaps [ "productid" ], username: User.current.username do |status, transaction| case status when :in_progress # Usually do nothing, maybe a spinner @@ -129,9 +132,16 @@ class PurchaseScreen < PM::Screen end end - restore_iaps "productid" do |status, products| + restore_iaps [ "productid" ], username: User.current.username do |status, data| if status == :restored # Update your UI, notify the user + # This is called once for each product you're trying to restore. + data[:product_id] # => "productid" + elsif status == :error + # Update your UI to show that there was an error. + # This will only be called once, no matter how many products you are trying to restore. + # You'll get an NSError in your `data` hash. + data[:error].localizedDescription # => description of error end end @@ -155,7 +165,7 @@ Prompts the user to login to their Apple ID and complete the purchase. The callb #### restore_iaps(`*`product_ids, &callback) Restores a previously purchased IAP to the user (for example if they have upgraded their device). This relies on the Apple ID the user - enters at the prompt. Unfortunately, if there is no purchase to restore for the signed-in account, no error message is generated and + enters at the prompt. Unfortunately, if there is no purchase to restore for the signed-in account, no error message is generated and will fail silently. diff --git a/lib/ProMotion/iap.rb b/lib/ProMotion/iap.rb index fd1bcfb..ce5899e 100644 --- a/lib/ProMotion/iap.rb +++ b/lib/ProMotion/iap.rb @@ -2,23 +2,32 @@ module ProMotion module IAP attr_accessor :completion_handlers - def purchase_iaps(*product_ids, &callback) + def purchase_iaps(product_ids, options={}, &callback) iap_setup retrieve_iaps product_ids do |products| products.each do |product| self.completion_handlers["purchase-#{product[:product_id]}"] = callback - payment = SKPayment.paymentWithProduct(product[:product]) + + payment = SKMutablePayment.paymentWithProduct(product[:product]) + payment.applicationUsername = options[:username] if options[:username] + SKPaymentQueue.defaultQueue.addPayment(payment) end end end alias purchase_iap purchase_iaps - def restore_iaps(*product_ids, &callback) + def restore_iaps(product_ids, options={}, &callback) iap_setup - retrieve_iaps product_ids do |products| + retrieve_iaps Array(product_ids) do |products| products.each do |product| self.completion_handlers["restore-#{product[:product_id]}"] = callback + end + self.completion_handlers["restore-all"] = callback # In case of error + + if options[:username] + SKPaymentQueue.defaultQueue.restoreCompletedTransactionsWithApplicationUsername(options[:username]) + else SKPaymentQueue.defaultQueue.restoreCompletedTransactions end end @@ -47,6 +56,7 @@ def iap_setup end def iap_shutdown + @completion_handlers = nil SKPaymentQueue.defaultQueue.removeTransactionObserver(self) end @@ -79,18 +89,41 @@ def formatted_iap_price(price, price_locale) end def iap_callback(status, transaction, finish=false) - product_id = transaction.payment.productIdentifier + product_id = transaction_product_id(transaction) + if self.completion_handlers["purchase-#{product_id}"] - self.completion_handlers["purchase-#{product_id}"].call status, transaction + self.completion_handlers["purchase-#{product_id}"].call status, mapped_transaction(transaction) self.completion_handlers["purchase-#{product_id}"] = nil if finish end + if self.completion_handlers["restore-#{product_id}"] - self.completion_handlers["restore-#{product_id}"].call status, transaction + self.completion_handlers["restore-#{product_id}"].call status, mapped_transaction(transaction) self.completion_handlers["restore-#{product_id}"] = nil if finish end + SKPaymentQueue.defaultQueue.finishTransaction(transaction) if finish end + def mapped_transaction(transaction) + if transaction.respond_to?(:payment) + { + product_id: transaction.payment.productIdentifier, + error: transaction.error, + transaction: transaction + } + else + { + product_id: nil, + error: transaction, + transaction: nil + } + end + end + + def transaction_product_id(transaction) + transaction.respond_to?(:payment) ? transaction.payment.productIdentifier : "all" + end + public # SKProductsRequestDelegate methods @@ -99,7 +132,6 @@ def productsRequest(_, didReceiveResponse:response) unless response.invalidProductIdentifiers.empty? red = "\e[0;31m" color_off = "\e[0m" - puts "#{red}PM::IAP Error - invalid product identifier(s) '#{response.invalidProductIdentifiers.join("', '")}' for application identifier #{NSBundle.mainBundle.infoDictionary['CFBundleIdentifier'].inspect}#{color_off}" end retrieved_iaps_handler(response.products, &self.completion_handlers["retrieve_iaps"]) if self.completion_handlers["retrieve_iaps"] @products_request = nil @@ -132,6 +164,10 @@ def paymentQueue(_, updatedTransactions:transactions) end end + def paymentQueue(_, restoreCompletedTransactionsFailedWithError:error) + iap_callback(:error, error) + end + end end ::PM = ProMotion unless defined?(::PM) diff --git a/spec/purchase_iap_spec.rb b/spec/purchase_iap_spec.rb index 080b324..dbb6031 100644 --- a/spec/purchase_iap_spec.rb +++ b/spec/purchase_iap_spec.rb @@ -16,13 +16,18 @@ def matchingIdentifier; end successful_transaction = mock_transaction.new(SKPaymentTransactionStatePurchased, Struct.new(:code).new(nil), mock_payment.new("successfulproductid")) it "returns success" do + called_callback = false subject = TestIAP.new subject.mock!(:completion_handlers, return: { - "purchase-successfulproductid" => ->(status, transaction) { + "purchase-successfulproductid" => ->(status, data) { status.should === :purchased + data[:transaction].transactionState.should == SKPaymentTransactionStatePurchased + data[:error].code.should.be.nil + called_callback = true }, }) subject.paymentQueue(nil, updatedTransactions:[ successful_transaction ]) + called_callback.should.be.true end end @@ -30,13 +35,18 @@ def matchingIdentifier; end canceled_transaction = mock_transaction.new(SKPaymentTransactionStateFailed, Struct.new(:code).new(SKErrorPaymentCancelled), mock_payment.new("canceledproductid")) it "returns nil error" do + called_callback = false subject = TestIAP.new subject.mock!(:completion_handlers, return: { - "purchase-canceledproductid" => ->(status, transaction) { + "purchase-canceledproductid" => ->(status, data) { status.should == :canceled + data[:transaction].should == canceled_transaction + data[:error].code.should == SKErrorPaymentCancelled + called_callback = true }, }) subject.paymentQueue(nil, updatedTransactions:[ canceled_transaction ]) + called_callback.should.be.true end end @@ -44,13 +54,18 @@ def matchingIdentifier; end invalid_transaction = mock_transaction.new(SKPaymentTransactionStateFailed, Struct.new(:code).new(nil), mock_payment.new("invalidproductid")) it "returns an error" do + called_callback = false subject = TestIAP.new subject.mock!(:completion_handlers, return: { - "purchase-invalidproductid" => ->(status, transaction) { + "purchase-invalidproductid" => ->(status, data) { status.should == :error + data[:transaction].should == invalid_transaction + data[:error].code.should.be.nil + called_callback = true }, }) subject.paymentQueue(nil, updatedTransactions:[ invalid_transaction ]) + called_callback.should.be.true end end diff --git a/spec/restore_iaps_spec.rb b/spec/restore_iaps_spec.rb index 7d7c450..144fc97 100644 --- a/spec/restore_iaps_spec.rb +++ b/spec/restore_iaps_spec.rb @@ -16,13 +16,39 @@ def matchingIdentifier; end restored_transaction = mock_transaction.new(SKPaymentTransactionStateRestored, Struct.new(:code).new(nil), mock_payment.new("restoredproductid")) it "returns success" do + callback_called = false subject = TestIAP.new subject.mock!(:completion_handlers, return: { - "restore-restoredproductid" => ->(status, transaction) { + "restore-restoredproductid" => ->(status, data) { status.should == :restored + data[:product_id].should == "restoredproductid" + data[:error].code.should.be.nil + data[:transaction].transactionState.should == SKPaymentTransactionStateRestored + callback_called = true }, }) subject.paymentQueue(nil, updatedTransactions:[ restored_transaction ]) + callback_called.should.be.true + end + end + + context "error in restore" do + restored_transaction = mock_transaction.new(SKPaymentTransactionStateRestored, Struct.new(:code).new(nil), mock_payment.new("restoredproductid")) + + it "returns success" do + callback_called = false + subject = TestIAP.new + subject.mock!(:completion_handlers, return: { + "restore-all" => ->(status, data) { + status.should == :error + data[:product_id].should.be.nil + data[:error].localizedDescription.should == "Failed to restore" + data[:transaction].should.be.nil + callback_called = true + }, + }) + subject.paymentQueue(nil, restoreCompletedTransactionsFailedWithError:Struct.new(:localizedDescription).new("Failed to restore")) + callback_called.should.be.true end end