rm_vendor 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/README.md +83 -0
- data/lib/Vendor.rb +27 -0
- data/lib/project/buy.rb +73 -0
- data/lib/project/info.rb +84 -0
- data/lib/project/product.rb +78 -0
- data/lib/project/receipt.rb +42 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YzI2MTI0ZmVkMTIzYjk2Y2IwODg5ZjA0MjhhNDJiNjNiMWI2N2JhMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YmUxYzM1YjhmOWUyZjYxNTNlOGNjNjk3ZTRhYzE0ODM2NGRjMTFkZg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
OGE5NDAxMzZjNDVlYmUzYjA1MTZiOWFlZmVhYzg4Y2IzMzM1ZjdiMTU2ODE2
|
10
|
+
MmQyNjdlMGFhMjM0ZGMzYzEyNDQ1MzVhNzY3ZTJmMzcwYzFlYTRiOTQzZjY2
|
11
|
+
NWEzN2VhYTFlNWJmMzhhZmRjNGZhNTEyOWJiOWExODlhMTlmMDA=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MTcyZWVkNjc3MGNkN2I1OTUzOWZjMWI5NmFmYmUxMzQ0MWYzOGI2NGNmMzU4
|
14
|
+
M2QyOTkwMjYyZjZmYTA5ZjQwNGVmMTAyN2JhM2QzNWVhYTQ2NmU2NjA2Y2U2
|
15
|
+
YTBiZDgwYmI0Y2JhYmNjZTNlOWIxMGY5OWVkMmZjNGUyMWE4YmQ=
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Vendor
|
2
|
+
|
3
|
+
## Installation
|
4
|
+
|
5
|
+
Add this line to your application's Gemfile:
|
6
|
+
|
7
|
+
gem 'rm_vendor'
|
8
|
+
|
9
|
+
And then execute:
|
10
|
+
|
11
|
+
$ bundle
|
12
|
+
|
13
|
+
## USAGE
|
14
|
+
|
15
|
+
#### Initialize.
|
16
|
+
|
17
|
+
Initialize your product with your In App Purchase ID. Vendor will check if the product exists and return it's attributes:
|
18
|
+
|
19
|
+
```Ruby
|
20
|
+
@product = Vendor::Product.new(:id => "com.your.item.id") do |product|
|
21
|
+
p "Product exists: #{product.success}"
|
22
|
+
p "Product response: #{product.response}"
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
You don't need to pass along a block though. If you're sure your product exists, then you can just do this:
|
27
|
+
|
28
|
+
```Ruby
|
29
|
+
@product = Vendor::Product.new(:id => "com.your.item.id")
|
30
|
+
```
|
31
|
+
|
32
|
+
#### Get product info.
|
33
|
+
|
34
|
+
After you've initialized your product, you get its information:
|
35
|
+
|
36
|
+
```Ruby
|
37
|
+
@product.price
|
38
|
+
@product.title
|
39
|
+
@product.description
|
40
|
+
@product.bought?
|
41
|
+
```
|
42
|
+
|
43
|
+
#### Purchase product.
|
44
|
+
|
45
|
+
To purchase a product, simply do:
|
46
|
+
|
47
|
+
```Ruby
|
48
|
+
@product.purchase do |product|
|
49
|
+
p "Purchase successful: #{product.success}"
|
50
|
+
p "Purchase transaction: #{product.transaction}"
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
#### Subscriptions.
|
55
|
+
|
56
|
+
Vendor also works with subscriptions. To initialize a subscription:
|
57
|
+
|
58
|
+
```Ruby
|
59
|
+
@subscription = Vendor::Product.new(:id => "com.your.subscription.id", :secret => "abcdefg12345", :subscription => true) do |subscription|
|
60
|
+
p "Subscription exists: #{subscription.success}"
|
61
|
+
p "Subscription response: #{subscription.response}"
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
To purchase a subscription:
|
66
|
+
|
67
|
+
```Ruby
|
68
|
+
@subscription.purchase do |subscription|
|
69
|
+
p "Subscription successful: #{subscription.success}"
|
70
|
+
p "Subscription transaction: #{subscription.transaction}"
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
To see wether product is registered as a subscription and if user is currently subscribed:
|
75
|
+
|
76
|
+
```Ruby
|
77
|
+
@subscription.subscribed?
|
78
|
+
@subscription.subscription?
|
79
|
+
```
|
80
|
+
|
81
|
+
## Example App.
|
82
|
+
|
83
|
+
You can find an example app inside this gem (inside the app folder).
|
data/lib/Vendor.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bubble-wrap/core'
|
5
|
+
require 'sugarcube-anonymous'
|
6
|
+
require 'sugarcube-legacy'
|
7
|
+
require 'sugarcube-numbers'
|
8
|
+
require 'sugarcube-nsuserdefaults'
|
9
|
+
require 'sugarcube-files'
|
10
|
+
require 'motion-cocoapods'
|
11
|
+
|
12
|
+
unless defined?(Motion::Project::Config)
|
13
|
+
raise "This file must be required within a RubyMotion project Rakefile."
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'motion/project/template/ios'
|
17
|
+
|
18
|
+
lib_dir_path = File.dirname(File.expand_path(__FILE__))
|
19
|
+
Motion::Project::App.setup do |app|
|
20
|
+
app.files.unshift(Dir.glob(File.join(lib_dir_path, "project/**/*.rb")))
|
21
|
+
|
22
|
+
app.frameworks += ['StoreKit']
|
23
|
+
|
24
|
+
app.pods ||= Motion::Project::CocoaPods.new(app)
|
25
|
+
app.pods.pod 'CocoaSecurity', '~> 1.2.1'
|
26
|
+
app.pods.pod 'AFNetworking'
|
27
|
+
end
|
data/lib/project/buy.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Vendor
|
2
|
+
class Buy
|
3
|
+
attr_accessor :params, :block, :request_operation_queue
|
4
|
+
|
5
|
+
def initialize(params)
|
6
|
+
@params = params
|
7
|
+
SKPaymentQueue.defaultQueue.addTransactionObserver(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
def close
|
11
|
+
SKPaymentQueue.defaultQueue.removeTransactionObserver(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
# PUBLIC METHODS
|
17
|
+
def purchase(&block)
|
18
|
+
@block = block
|
19
|
+
SKPaymentQueue.defaultQueue.addPayment(SKPayment.paymentWithProductIdentifier(@params.id))
|
20
|
+
end
|
21
|
+
|
22
|
+
def restore(&block)
|
23
|
+
@block = block
|
24
|
+
SKPaymentQueue.defaultQueue.restoreCompletedTransactions
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
# DELEGATE METHODS
|
30
|
+
def finishTransaction(transaction, success:success)
|
31
|
+
SKPaymentQueue.defaultQueue.finishTransaction(transaction)
|
32
|
+
SKPaymentQueue.defaultQueue.removeTransactionObserver(self)
|
33
|
+
|
34
|
+
if success
|
35
|
+
Vendor::Receipt.new(transaction.transactionReceipt, @params) do |block|
|
36
|
+
result_object = BW::JSON.parse(block.object).to_object
|
37
|
+
valid_receipt = block.success && result_object.status.to_i == 0
|
38
|
+
|
39
|
+
@block.call({success: valid_receipt, transaction: transaction}.to_object) unless @block.blank?
|
40
|
+
end
|
41
|
+
else
|
42
|
+
@block.call({success: success, transaction: transaction}.to_object) unless @block.blank?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def paymentQueue(queue, updatedTransactions:transactions)
|
47
|
+
transactions.each do |transaction|
|
48
|
+
case transaction.transactionState
|
49
|
+
when SKPaymentTransactionStatePurchased
|
50
|
+
completeTransaction(transaction)
|
51
|
+
when SKPaymentTransactionStateFailed
|
52
|
+
failedTransaction(transaction)
|
53
|
+
when SKPaymentTransactionStateRestored
|
54
|
+
restoreTransaction(transaction)
|
55
|
+
else
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def completeTransaction(transaction)
|
61
|
+
finishTransaction(transaction, success:true)
|
62
|
+
end
|
63
|
+
|
64
|
+
def restoreTransaction(transaction)
|
65
|
+
finishTransaction(transaction, success:true)
|
66
|
+
end
|
67
|
+
|
68
|
+
def failedTransaction(transaction)
|
69
|
+
finishTransaction(transaction, success:false)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/lib/project/info.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Vendor
|
2
|
+
class Info
|
3
|
+
attr_accessor :params, :block
|
4
|
+
|
5
|
+
def initialize(params, &block)
|
6
|
+
@params = params
|
7
|
+
@block = block
|
8
|
+
|
9
|
+
# Start product request
|
10
|
+
productsRequest = SKProductsRequest.alloc.initWithProductIdentifiers(NSSet.setWithObject(@params.id))
|
11
|
+
productsRequest.delegate = self
|
12
|
+
productsRequest.start
|
13
|
+
|
14
|
+
# Update receipt if bought and subscription
|
15
|
+
Vendor::Receipt.new(NSUserDefaults["#{@params.id}.receipt_data"], @params) if bought? && subscription?
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
# INFO METHODS
|
21
|
+
def price
|
22
|
+
price = NSUserDefaults["#{@params.id}.price"] || @params.price
|
23
|
+
price.to_f.string_with_style(NSNumberFormatterCurrencyStyle)
|
24
|
+
end
|
25
|
+
|
26
|
+
def title
|
27
|
+
NSUserDefaults["#{@params.id}.localizedTitle"] || @params.title
|
28
|
+
end
|
29
|
+
|
30
|
+
def description
|
31
|
+
NSUserDefaults["#{@params.id}.localizedDescription"] || @params.desc
|
32
|
+
end
|
33
|
+
|
34
|
+
def bought?
|
35
|
+
NSUserDefaults["#{@params.id}.receipt"] != nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def subscription?
|
39
|
+
@params.subscription
|
40
|
+
end
|
41
|
+
|
42
|
+
def subscribed?
|
43
|
+
return false if !subscription?
|
44
|
+
receipt_object = BW::JSON.parse(NSUserDefaults["#{@params.id}.receipt"]).to_object
|
45
|
+
return false if receipt_object.blank? || receipt_object.status!=0
|
46
|
+
|
47
|
+
decoder = CocoaSecurityDecoder.new
|
48
|
+
latest_receipt_data = decoder.base64(receipt_object.latest_receipt)
|
49
|
+
latest_receipt_plist = NSPropertyListSerialization.propertyListWithData(latest_receipt_data, options:NSPropertyListImmutable, format:nil, error:nil)
|
50
|
+
|
51
|
+
purchase_info_data = decoder.base64(latest_receipt_plist.objectForKey("purchase-info"))
|
52
|
+
purchase_info_plist = NSPropertyListSerialization.propertyListWithData(purchase_info_data, options:NSPropertyListImmutable, format:nil, error:nil)
|
53
|
+
purchase_info_plist = NSPropertyListSerialization.propertyListWithData(purchase_info_data, options:NSPropertyListImmutable, format:nil, error:nil)
|
54
|
+
|
55
|
+
expires_date = purchase_info_plist.objectForKey("expires-date")
|
56
|
+
# ap "expires_date: #{expires_date}"
|
57
|
+
expires_calc = expires_date.to_i/1000
|
58
|
+
|
59
|
+
return expires_calc > NSDate.date.timeIntervalSince1970
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
# DELEGATE METHODS
|
65
|
+
def productsRequest(request, didReceiveResponse:response)
|
66
|
+
exists = response.invalidProductIdentifiers.count==0
|
67
|
+
|
68
|
+
# Save needed product info
|
69
|
+
if exists
|
70
|
+
product = response.products.first
|
71
|
+
NSUserDefaults["#{@params.id}.price"] = product.price
|
72
|
+
NSUserDefaults["#{@params.id}.localizedTitle"] = product.localizedTitle
|
73
|
+
NSUserDefaults["#{@params.id}.localizedDescription"] = product.localizedDescription
|
74
|
+
end
|
75
|
+
|
76
|
+
@block.call({success: exists, response: response}.to_object)
|
77
|
+
end
|
78
|
+
|
79
|
+
def request(request, didFailWithError:error)
|
80
|
+
@block.call({success: false, error: error}.to_object)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Vendor
|
2
|
+
class Product
|
3
|
+
attr_reader :params, :block, :exists, :info, :buy
|
4
|
+
|
5
|
+
def initialize(options={}, &block)
|
6
|
+
# Set default options
|
7
|
+
default_options = {
|
8
|
+
:id => "no_id",
|
9
|
+
:secret => "no_secret",
|
10
|
+
:subscription => false,
|
11
|
+
:price => "0.99",
|
12
|
+
:title => "No Title",
|
13
|
+
:desc => "No Description."
|
14
|
+
}
|
15
|
+
|
16
|
+
# Set global params and block
|
17
|
+
options = default_options.merge(options)
|
18
|
+
@params = options.to_object
|
19
|
+
@block = block
|
20
|
+
|
21
|
+
# Raise argument error if id is not included
|
22
|
+
raise ArgumentError, "VENDOR WARNING: You forgot to write in your item id. You can't sell item without an id." if @params.id == "no_id"
|
23
|
+
raise ArgumentError, "VENDOR WARNING: You're missing your subscriptions shared secret. Subscriptions must have a shared secret." if @params.subscription && @params.secret == "no_secret"
|
24
|
+
|
25
|
+
# Update product and set exists variable
|
26
|
+
@info = Vendor::Info.new(@params) do |block|
|
27
|
+
@exists = block.success
|
28
|
+
@block.call(block) unless @block.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Initialize purchase
|
32
|
+
@buy = Vendor::Buy.new(@params)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
# PURCHASE METHODS
|
38
|
+
def purchase(&block)
|
39
|
+
@buy.purchase { |result| block.call(result)}
|
40
|
+
end
|
41
|
+
|
42
|
+
def restore(&block)
|
43
|
+
@buy.restore { |result| block.call(result)}
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
# INFO METHODS
|
49
|
+
def price
|
50
|
+
@info.price
|
51
|
+
end
|
52
|
+
|
53
|
+
def title
|
54
|
+
@info.title
|
55
|
+
end
|
56
|
+
|
57
|
+
def description
|
58
|
+
@info.description
|
59
|
+
end
|
60
|
+
|
61
|
+
def exists?
|
62
|
+
@exists || false
|
63
|
+
end
|
64
|
+
|
65
|
+
def bought?
|
66
|
+
@info.bought?
|
67
|
+
end
|
68
|
+
|
69
|
+
def subscription?
|
70
|
+
@info.subscription?
|
71
|
+
end
|
72
|
+
|
73
|
+
def subscribed?
|
74
|
+
@info.subscribed?
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Vendor
|
2
|
+
class Receipt
|
3
|
+
|
4
|
+
attr_reader :receipt_data, :params, :request_operation_queue, :block
|
5
|
+
|
6
|
+
def initialize(receipt_data, params, &block)
|
7
|
+
@receipt_data = receipt_data
|
8
|
+
@params = params
|
9
|
+
@block = block
|
10
|
+
@request_operation_queue = NSOperationQueue.alloc.init
|
11
|
+
|
12
|
+
check_receipt(receipt_data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def check_receipt(receipt_data)
|
16
|
+
encoder = CocoaSecurityEncoder.new
|
17
|
+
json_data = {"receipt-data" => encoder.base64(receipt_data)}
|
18
|
+
json_data[:password] = @params.secret if @params.secret != "no_secret"
|
19
|
+
server = App.development? ? "sandbox" : "buy"
|
20
|
+
base_url = NSURL.URLWithString("https://#{server}.itunes.apple.com")
|
21
|
+
|
22
|
+
# TODO - Could look nicer. Dunno if AFMotion could be used instead.
|
23
|
+
client = AFHTTPClient.alloc.initWithBaseURL(base_url)
|
24
|
+
client.setDefaultHeader("Accept", value:"application/json")
|
25
|
+
client.registerHTTPOperationClass(AFJSONRequestOperation.class)
|
26
|
+
client.setParameterEncoding(AFJSONParameterEncoding)
|
27
|
+
AFJSONRequestOperation.addAcceptableContentTypes(NSSet.setWithObject("text/plain"))
|
28
|
+
|
29
|
+
request = client.requestWithMethod("POST", path:"verifyReceipt", parameters:json_data)
|
30
|
+
request_operation = client.HTTPRequestOperationWithRequest(request, success: lambda { |operation, response_object|
|
31
|
+
NSUserDefaults["#{@params.id}.receipt_data"] = receipt_data
|
32
|
+
NSUserDefaults["#{@params.id}.receipt"] = response_object
|
33
|
+
@block.call({success: true, object: response_object}.to_object) unless @block.blank?
|
34
|
+
}, failure: lambda {|operation, error|
|
35
|
+
@block.call({success: false, error: error}.to_object) unless @block.blank?
|
36
|
+
})
|
37
|
+
|
38
|
+
@request_operation_queue.addOperation(request_operation)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rm_vendor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Holger Sindbaek
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sugarcube
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bubble-wrap
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: motion-cocoapods
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: A RubyMotion StoreKit Wrapper that allows you to buy, restore and get
|
70
|
+
product info on your in app purchases and subscriptions
|
71
|
+
email:
|
72
|
+
- HolgerSindbaek@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- README.md
|
78
|
+
- lib/project/buy.rb
|
79
|
+
- lib/project/info.rb
|
80
|
+
- lib/project/product.rb
|
81
|
+
- lib/project/receipt.rb
|
82
|
+
- lib/Vendor.rb
|
83
|
+
homepage: https://github.com/holgersindbaek/Vendor
|
84
|
+
licenses:
|
85
|
+
- MIT
|
86
|
+
metadata: {}
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 2.1.10
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: RubyMotion StoreKit Wrapper
|
107
|
+
test_files: []
|