venice 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +26 -22
  3. data/LICENSE +1 -1
  4. data/README.md +60 -25
  5. data/Rakefile +1 -2
  6. data/bin/iap +13 -0
  7. data/coverage/assets/0.10.0/application.css +799 -0
  8. data/coverage/assets/0.10.0/application.js +1707 -0
  9. data/coverage/assets/0.10.0/colorbox/border.png +0 -0
  10. data/coverage/assets/0.10.0/colorbox/controls.png +0 -0
  11. data/coverage/assets/0.10.0/colorbox/loading.gif +0 -0
  12. data/coverage/assets/0.10.0/colorbox/loading_background.png +0 -0
  13. data/coverage/assets/0.10.0/favicon_green.png +0 -0
  14. data/coverage/assets/0.10.0/favicon_red.png +0 -0
  15. data/coverage/assets/0.10.0/favicon_yellow.png +0 -0
  16. data/coverage/assets/0.10.0/loading.gif +0 -0
  17. data/coverage/assets/0.10.0/magnify.png +0 -0
  18. data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  19. data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  20. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  21. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  22. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  23. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  24. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  25. data/coverage/assets/0.10.0/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  26. data/coverage/assets/0.10.0/smoothness/images/ui-icons_222222_256x240.png +0 -0
  27. data/coverage/assets/0.10.0/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  28. data/coverage/assets/0.10.0/smoothness/images/ui-icons_454545_256x240.png +0 -0
  29. data/coverage/assets/0.10.0/smoothness/images/ui-icons_888888_256x240.png +0 -0
  30. data/coverage/assets/0.10.0/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  31. data/coverage/index.html +2152 -0
  32. data/lib/venice.rb +1 -0
  33. data/lib/venice/client.rb +16 -8
  34. data/lib/venice/in_app_receipt.rb +92 -0
  35. data/lib/venice/receipt.rb +62 -58
  36. data/lib/venice/version.rb +1 -1
  37. data/spec/client_spec.rb +73 -6
  38. data/spec/in_app_receipt_spec.rb +58 -0
  39. data/spec/receipt_spec.rb +47 -70
  40. data/spec/spec_helper.rb +9 -1
  41. data/venice.gemspec +2 -2
  42. metadata +59 -35
  43. data/spec/receipt_verification_spec.rb +0 -19
  44. data/venice-0.1.0.gem +0 -0
  45. data/venice-0.1.1.gem +0 -0
data/lib/venice.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  require 'venice/version'
2
2
  require 'venice/client'
3
+ require 'venice/in_app_receipt'
3
4
  require 'venice/receipt'
data/lib/venice/client.rb CHANGED
@@ -29,24 +29,32 @@ module Venice
29
29
  end
30
30
 
31
31
  def verify!(data, options = {})
32
+ @verification_url ||= ITUNES_DEVELOPMENT_RECEIPT_VERIFICATION_ENDPOINT
33
+ @shared_secret = options[:shared_secret] if options[:shared_secret]
34
+
32
35
  json = json_response_from_verifying_data(data)
33
36
  status, receipt_attributes = json['status'].to_i, json['receipt']
37
+ receipt_attributes['original_json_response'] = json if receipt_attributes
34
38
 
35
39
  case status
36
40
  when 0, 21006
37
41
  receipt = Receipt.new(receipt_attributes)
38
42
 
39
- if latest_receipt_attributes = json['latest_receipt_info']
40
- receipt.latest = Receipt.new(latest_receipt_attributes)
41
- end
42
-
43
- if latest_expired_receipt_attributes = json['latest_expired_receipt_info']
44
- receipt.latest_expired = Receipt.new(latest_expired_receipt_attributes)
43
+ # From Apple docs:
44
+ # > Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
45
+ # > The JSON representation of the receipt for the most recent renewal
46
+ if latest_receipt_info_attributes = json['latest_receipt_info']
47
+ # AppStore returns 'latest_receipt_info' even if we use over iOS 6. Besides, its format is an Array.
48
+ receipt.latest_receipt_info = []
49
+ latest_receipt_info_attributes.each do |latest_receipt_info_attribute|
50
+ # latest_receipt_info format is identical with in_app
51
+ receipt.latest_receipt_info << InAppReceipt.new(latest_receipt_info_attribute)
52
+ end
45
53
  end
46
54
 
47
55
  return receipt
48
56
  else
49
- raise Receipt::VerificationError.new(status)
57
+ raise Receipt::VerificationError.new(status, receipt)
50
58
  end
51
59
  end
52
60
 
@@ -62,7 +70,7 @@ module Venice
62
70
  uri = URI(@verification_url)
63
71
  http = Net::HTTP.new(uri.host, uri.port)
64
72
  http.use_ssl = true
65
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
73
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
66
74
 
67
75
  request = Net::HTTP::Post.new(uri.request_uri)
68
76
  request['Accept'] = "application/json"
@@ -0,0 +1,92 @@
1
+ require 'time'
2
+
3
+ module Venice
4
+ class InAppReceipt
5
+ # For detailed explanations on these keys/values, see
6
+ # https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW12
7
+
8
+ # The number of items purchased. This value corresponds to the quantity property of
9
+ # the SKPayment object stored in the transaction’s payment property.
10
+ attr_reader :quantity
11
+
12
+ # The product identifier of the item that was purchased. This value corresponds to
13
+ # the productIdentifier property of the SKPayment object stored in the transaction’s
14
+ # payment property.
15
+ attr_reader :product_id
16
+
17
+ # The transaction identifier of the item that was purchased. This value corresponds
18
+ # to the transaction’s transactionIdentifier property.
19
+ attr_reader :transaction_id
20
+
21
+ # The date and time this transaction occurred. This value corresponds to the
22
+ # transaction’s transactionDate property.
23
+ attr_reader :purchased_at
24
+
25
+ # A string that the App Store uses to uniquely identify the application that created
26
+ # the payment transaction. If your server supports multiple applications, you can use
27
+ # this value to differentiate between them. Applications that are executing in the
28
+ # sandbox do not yet have an app-item-id assigned to them, so this key is missing from
29
+ # receipts created by the sandbox.
30
+ attr_reader :app_item_id
31
+
32
+ # An arbitrary number that uniquely identifies a revision of your application. This key
33
+ # is missing in receipts created by the sandbox.
34
+ attr_reader :version_external_identifier
35
+
36
+ # For a transaction that restores a previous transaction, this is the original receipt
37
+ attr_accessor :original
38
+
39
+ # For auto-renewable subscriptions, returns the date the subscription will expire
40
+ attr_reader :expires_at
41
+
42
+ # For a transaction that was canceled by Apple customer support, the time and date of the cancellation.
43
+ attr_reader :cancellation_at
44
+
45
+
46
+ def initialize(attributes = {})
47
+ @quantity = Integer(attributes['quantity']) if attributes['quantity']
48
+ @product_id = attributes['product_id']
49
+ @transaction_id = attributes['transaction_id']
50
+ @purchased_at = DateTime.parse(attributes['purchase_date']) if attributes['purchase_date']
51
+ @app_item_id = attributes['app_item_id']
52
+ @version_external_identifier = attributes['version_external_identifier']
53
+
54
+ # expires_date is in ms since the Epoch, Time.at expects seconds
55
+ @expires_at = Time.at(attributes['expires_date_ms'].to_i / 1000) if attributes['expires_date_ms']
56
+
57
+ # cancellation_date is in ms since the Epoch, Time.at expects seconds
58
+ @cancellation_date = Time.at(attributes['cancellation_date'].to_i / 1000) if attributes['cancellation_date']
59
+
60
+ if attributes['original_transaction_id'] || attributes['original_purchase_date']
61
+ original_attributes = {
62
+ 'transaction_id' => attributes['original_transaction_id'],
63
+ 'purchase_date' => attributes['original_purchase_date']
64
+ }
65
+
66
+ self.original = InAppReceipt.new(original_attributes)
67
+ end
68
+
69
+ end
70
+
71
+ def to_hash
72
+ {
73
+ :quantity => @quantity,
74
+ :product_id => @product_id,
75
+ :transaction_id => @transaction_id,
76
+ :purchase_date => (@purchased_at.httpdate rescue nil),
77
+ :original_transaction_id => (@original.transaction_id rescue nil),
78
+ :original_purchase_date => (@original.purchased_at.httpdate rescue nil),
79
+ :app_item_id => @app_item_id,
80
+ :version_external_identifier => @version_external_identifier,
81
+ :expires_at => (@expires_at.httpdate rescue nil),
82
+ :cancellation_at => (@cancellation_at.httpdate rescue nil)
83
+ }
84
+ end
85
+ alias_method :to_h, :to_hash
86
+
87
+ def to_json
88
+ self.to_hash.to_json
89
+ end
90
+
91
+ end
92
+ end
@@ -2,83 +2,85 @@ require 'time'
2
2
 
3
3
  module Venice
4
4
  class Receipt
5
- # The number of items purchased. This value corresponds to the quantity property of the SKPayment object stored in the transaction’s payment property.
6
- attr_reader :quantity
5
+ # For detailed explanations on these keys/values, see
6
+ # https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
7
7
 
8
- # The product identifier of the item that was purchased. This value corresponds to the productIdentifier property of the SKPayment object stored in the transaction’s payment property.
9
- attr_reader :product_id
8
+ # The app’s bundle identifier.
9
+ attr_reader :bundle_id
10
10
 
11
- # The transaction identifier of the item that was purchased. This value corresponds to the transaction’s transactionIdentifier property.
12
- attr_reader :transaction_id
11
+ # The app’s version number.
12
+ attr_reader :application_version
13
13
 
14
- # The date and time this transaction occurred. This value corresponds to the transaction’s transactionDate property.
15
- attr_reader :purchase_date
14
+ # The receipt for an in-app purchase.
15
+ attr_reader :in_app
16
16
 
17
- # A string that the App Store uses to uniquely identify the application that created the payment transaction. If your server supports multiple applications, you can use this value to differentiate between them. Applications that are executing in the sandbox do not yet have an app-item-id assigned to them, so this key is missing from receipts created by the sandbox.
18
- attr_reader :app_item_id
17
+ # The version of the app that was originally purchased.
18
+ attr_reader :original_application_version
19
19
 
20
- # An arbitrary number that uniquely identifies a revision of your application. This key is missing in receipts created by the sandbox.
21
- attr_reader :version_external_identifier
20
+ # The original purchase date
21
+ attr_reader :original_purchase_date
22
22
 
23
- # The bundle identifier for the application.
24
- attr_reader :bid
25
-
26
- # A version number for the application.
27
- attr_reader :bvrs
23
+ # The date that the app receipt expires.
24
+ attr_reader :expires_at
28
25
 
29
- # For a transaction that restores a previous transaction, this is the original receipt
30
- attr_accessor :original
26
+ # Non-Documented receipt keys/values
27
+ attr_reader :receipt_type
28
+ attr_reader :adam_id
29
+ attr_reader :download_id
30
+ attr_reader :requested_at
31
31
 
32
- # For an active subscription was renewed with transaction that took place after the receipt your server sent to the App Store, this is the latest receipt.
33
- attr_accessor :latest
32
+ # Original json response from AppStore
33
+ attr_reader :original_json_response
34
34
 
35
- # For an expired auto-renewable subscription, this contains the receipt details for the latest expired receipt
36
- attr_accessor :latest_expired
37
35
 
38
- # For auto-renewable subscriptions, returns the date the subscription will expire
39
- attr_reader :expires_at
36
+ attr_accessor :latest_receipt_info
40
37
 
41
38
  def initialize(attributes = {})
42
- @quantity = Integer(attributes['quantity']) if attributes['quantity']
43
- @product_id = attributes['product_id']
44
- @transaction_id = attributes['transaction_id']
45
- @purchase_date = DateTime.parse(attributes['purchase_date']) if attributes['purchase_date']
46
- @app_item_id = attributes['app_item_id']
47
- @version_external_identifier = attributes['version_external_identifier']
48
- @bid = attributes['bid']
49
- @bvrs = attributes['bvrs']
50
-
51
- # expires_date is in ms since the Epoch, Time.at expects seconds
52
- @expires_at = Time.at(attributes['expires_date'].to_i / 1000) if attributes['expires_date']
53
-
54
- if attributes['original_transaction_id'] || attributes['original_purchase_date']
55
- original_attributes = {
56
- 'transaction_id' => attributes['original_transaction_id'],
57
- 'purchase_date' => attributes['original_purchase_date']
58
- }
59
-
60
- self.original = Receipt.new(original_attributes)
39
+ @original_json_response = attributes['original_json_response']
40
+
41
+ @bundle_id = attributes['bundle_id']
42
+ @application_version = attributes['application_version']
43
+ @original_application_version = attributes['original_application_version']
44
+ if attributes['original_purchase_date']
45
+ @original_purchase_date = DateTime.parse(attributes['original_purchase_date'])
46
+ end
47
+ if attributes['expiration_date']
48
+ @expires_at = Time.at(attributes['expiration_date'].to_i / 1000).to_datetime
61
49
  end
50
+
51
+ @receipt_type = attributes['receipt_type']
52
+ @adam_id = attributes['adam_id']
53
+ @download_id = attributes['download_id']
54
+ @requested_at = DateTime.parse(attributes['request_date']) if attributes['request_date']
55
+
56
+ @in_app = []
57
+ if attributes['in_app']
58
+ attributes['in_app'].each do |in_app_purchase_attributes|
59
+ @in_app << InAppReceipt.new(in_app_purchase_attributes)
60
+ end
61
+ end
62
+
62
63
  end
63
64
 
64
- def to_h
65
+ def to_hash
65
66
  {
66
- :quantity => @quantity,
67
- :product_id => @product_id,
68
- :transaction_id => @transaction_id,
69
- :purchase_date => (@purchase_date.httpdate rescue nil),
70
- :original_transaction_id => (@original.transaction_id rescue nil),
71
- :original_purchase_date => (@original.purchase_date.httpdate rescue nil),
72
- :app_item_id => @app_item_id,
73
- :version_external_identifier => @version_external_identifier,
74
- :bid => @bid,
75
- :bvrs => @bvrs,
76
- :expires_at => (@expires_at.httpdate rescue nil)
67
+ :bundle_id => @bundle_id,
68
+ :application_version => @application_version,
69
+ :original_application_version => @original_application_version,
70
+ :original_purchase_date => (@original_purchase_date.httpdate rescue nil),
71
+ :expires_at => (@expires_at.httpdate rescue nil),
72
+ :receipt_type => @receipt_type,
73
+ :adam_id => @adam_id,
74
+ :download_id => @download_id,
75
+ :requested_at => (@requested_at.httpdate rescue nil),
76
+ :in_app => @in_app.map{|iap| iap.to_h },
77
+ :latest_receipt_info => @latest_receipt_info
77
78
  }
78
79
  end
80
+ alias_method :to_h, :to_hash
79
81
 
80
82
  def to_json
81
- self.to_h.to_json
83
+ self.to_hash.to_json
82
84
  end
83
85
 
84
86
  class << self
@@ -111,9 +113,11 @@ module Venice
111
113
 
112
114
  class VerificationError < StandardError
113
115
  attr_accessor :code
116
+ attr_accessor :receipt
114
117
 
115
- def initialize(code)
118
+ def initialize(code, receipt)
116
119
  @code = Integer(code)
120
+ @receipt = receipt
117
121
  end
118
122
 
119
123
  def message
@@ -1,3 +1,3 @@
1
1
  module Venice
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/spec/client_spec.rb CHANGED
@@ -8,10 +8,14 @@ describe Venice::Client do
8
8
  context "no shared_secret" do
9
9
  before do
10
10
  client.shared_secret = nil
11
+ Venice::Receipt.stub :new
11
12
  end
12
13
 
13
14
  it "should only include the receipt_data" do
14
- client.should_receive(:perform_post).with('receipt-data' => receipt_data)
15
+ Net::HTTP.any_instance.should_receive(:request) do |post|
16
+ post.body.should eq({'receipt-data' => receipt_data}.to_json)
17
+ post
18
+ end
15
19
  client.verify! receipt_data
16
20
  end
17
21
  end
@@ -20,14 +24,77 @@ describe Venice::Client do
20
24
  let(:secret) { "shhhhhh" }
21
25
 
22
26
  before do
23
- client.shared_secret = secret
27
+ Venice::Receipt.stub :new
24
28
  end
25
29
 
26
- it "should include the secret in the post" do
27
- client.should_receive(:perform_post).with('receipt-data' => receipt_data, 'password' => secret)
28
- client.verify! receipt_data
30
+ context "set secret manually" do
31
+ before do
32
+ client.shared_secret = secret
33
+ end
34
+
35
+ it "should include the secret in the post" do
36
+ Net::HTTP.any_instance.should_receive(:request) do |post|
37
+ post.body.should eq({'receipt-data' => receipt_data, 'password' => secret}.to_json)
38
+ post
39
+ end
40
+ client.verify! receipt_data
41
+ end
42
+ end
43
+
44
+ context "set secret when verification" do
45
+ let(:options) { {shared_secret: secret} }
46
+
47
+ it "should include the secret in the post" do
48
+ Net::HTTP.any_instance.should_receive(:request) do |post|
49
+ post.body.should eq({'receipt-data' => receipt_data, 'password' => secret}.to_json)
50
+ post
51
+ end
52
+ client.verify! receipt_data, options
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ context "with a latest receipt info attribute" do
59
+ before do
60
+ client.stub(:json_response_from_verifying_data).and_return(response)
61
+ end
62
+
63
+ let(:response) do
64
+ {
65
+ 'status' => 0,
66
+ 'receipt' => {},
67
+ 'latest_receipt' => "<encoded string>",
68
+ 'latest_receipt_info' => [
69
+ {
70
+ "original_purchase_date_pst" => "2012-12-30 09:39:24 America/Los_Angeles",
71
+ "unique_identifier" => "0000b01147b8",
72
+ "original_transaction_id" => "1000000061051565",
73
+ "expires_date" => "1365114731000",
74
+ "transaction_id" => "1000000070104252",
75
+ "quantity" => "1",
76
+ "product_id" => "com.ficklebits.nsscreencast.monthly_sub",
77
+ "original_purchase_date_ms" => "1356889164000",
78
+ "bid" => "com.ficklebits.nsscreencast",
79
+ "web_order_line_item_id" => "1000000026812043",
80
+ "bvrs" => "0.1",
81
+ "expires_date_formatted" => "2013-04-04 22:32:11 Etc/GMT",
82
+ "purchase_date" => "2013-04-04 22:27:11 Etc/GMT",
83
+ "purchase_date_ms" => "1365114431000",
84
+ "expires_date_formatted_pst" => "2013-04-04 15:32:11 America/Los_Angeles",
85
+ "purchase_date_pst" => "2013-04-04 15:27:11 America/Los_Angeles",
86
+ "original_purchase_date" => "2012-12-30 17:39:24 Etc/GMT",
87
+ "item_id" => "590265423"
88
+ }
89
+ ]
90
+ }
91
+ end
92
+
93
+ it "should create a latest receipt" do
94
+ receipt = client.verify! 'asdf'
95
+ receipt.latest_receipt_info.should_not be_nil
29
96
  end
30
97
  end
98
+
31
99
  end
32
100
  end
33
-
@@ -0,0 +1,58 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Venice::InAppReceipt do
4
+
5
+ describe ".new" do
6
+
7
+ let :attributes do
8
+ {
9
+ "quantity" => 1,
10
+ "product_id" => "com.foo.product1",
11
+ "transaction_id" => "1000000070107235",
12
+ "purchase_date" => "2014-05-28 14:47:53 Etc/GMT",
13
+ "purchase_date_ms" => "1401288473000",
14
+ "purchase_date_pst" => "2014-05-28 07:47:53 America/Los_Angeles",
15
+ "original_transaction_id" => "140xxx867509",
16
+ "original_purchase_date" => "2014-05-28 14:47:53 Etc/GMT",
17
+ "original_purchase_date_ms" => "1401288473000",
18
+ "original_purchase_date_pst" => "2014-05-28 07:47:53 America/Los_Angeles",
19
+ "is_trial_period" => false,
20
+ "version_external_identifier" => "123",
21
+ "app_item_id" => 'com.foo.app1',
22
+ "expires_date" => "2014-06-28 07:47:53 America/Los_Angeles",
23
+ "expires_date_ms" => "1403941673000"
24
+ }
25
+ end
26
+
27
+ subject(:in_app_receipt) do
28
+ Venice::InAppReceipt.new attributes
29
+ end
30
+
31
+ its(:quantity) { 1 }
32
+ its(:product_id) { "com.foo.product1" }
33
+ its(:transaction_id) { "1000000070107235" }
34
+ its(:purchased_at) { should be_instance_of DateTime }
35
+ its(:app_item_id) { 'com.foo.app1' }
36
+ its(:version_external_identifier) { "123" }
37
+ its(:original) { should be_instance_of Venice::InAppReceipt }
38
+ its(:expires_at) { should be_instance_of Time }
39
+
40
+ it "should parse the 'original' attributes" do
41
+ subject.original.should be_instance_of Venice::InAppReceipt
42
+ subject.original.transaction_id.should == "140xxx867509"
43
+ subject.original.purchased_at.should be_instance_of DateTime
44
+ end
45
+
46
+ it "should output a hash with attributes" do
47
+ in_app_receipt.to_h.should include(:quantity => 1,
48
+ :product_id => "com.foo.product1",
49
+ :transaction_id => "1000000070107235",
50
+ :purchase_date => "Wed, 28 May 2014 14:47:53 GMT",
51
+ :original_purchase_date => "Wed, 28 May 2014 14:47:53 GMT"
52
+ )
53
+ end
54
+
55
+
56
+ end
57
+
58
+ end