Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support filter AppStore subscriptions per products #40

Merged
merged 1 commit into from
Nov 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ For **subscription verification**, Apple also returns a list of the user's purch
verifier.verify_subscription(your_receipt_data, your_secret) # => ReceiptCollection or VerificationFailure
```

If you want to restrict the subscription verification to some specific products, pass their ids as an array:

```ruby
# ... create your verifier as above
product_ids = ['sk_product_id_1', 'sk_product_id_2'...]
verifier.verify_subscription(your_receipt_data, your_secret, product_ids) # => ReceiptCollection or VerificationFailure
```

Please see the class documentation for [`CandyCheck::AppStore::ReceiptCollection`](http://www.rubydoc.info/github/jnbt/candy_check/master/CandyCheck/AppStore/ReceiptCollection) for further details.

### PlayStore
Expand Down
2 changes: 1 addition & 1 deletion lib/candy_check/app_store/receipt_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class ReceiptCollection

# Initializes a new instance which bases on a JSON result
# from Apple's verification server
# @param attributes [Array<Hash>]
# @param attributes [Array<Hash>] raw data from Apple's server
def initialize(attributes)
@receipts = attributes.map { |r| Receipt.new(r) }
end
Expand Down
26 changes: 25 additions & 1 deletion lib/candy_check/app_store/subscription_verification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,44 @@ module AppStore
# Verifies a latest_receipt_info block against a verification server.
# The call return either an {ReceiptCollection} or a {VerificationFailure}
class SubscriptionVerification < CandyCheck::AppStore::Verification
# Builds a fresh verification run
# @param endpoint_url [String] the verification URL to use
# @param receipt_data [String] the raw data to be verified
# @param secret [String] optional: shared secret
# @param product_ids [Array<String>] optional: select specific products
def initialize(
endpoint_url,
receipt_data,
secret = nil,
product_ids = nil
)
super(endpoint_url, receipt_data, secret)
@product_ids = product_ids
end

# Performs the verification against the remote server
# @return [ReceiptCollection] if successful
# @return [VerificationFailure] otherwise
def call!
verify!
if valid?
ReceiptCollection.new(@response['latest_receipt_info'])
build_collection(@response['latest_receipt_info'])
else
VerificationFailure.fetch(@response['status'])
end
end

private

def build_collection(latest_receipt_info)
unless @product_ids.nil?
latest_receipt_info = latest_receipt_info.select do |info|
@product_ids.include?(info['product_id'])
end
end
ReceiptCollection.new(latest_receipt_info)
end

def valid?
status_is_ok = @response['status'] == STATUS_OK
@response && status_is_ok && @response['latest_receipt_info']
Expand Down
2 changes: 1 addition & 1 deletion lib/candy_check/app_store/verification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Verification
# Builds a fresh verification run
# @param endpoint_url [String] the verification URL to use
# @param receipt_data [String] the raw data to be verified
# @param secret [String] the optional shared secret
# @param secret [String] optional: shared secret
def initialize(endpoint_url, receipt_data, secret = nil)
@endpoint_url = endpoint_url
@receipt_data = receipt_data
Expand Down
22 changes: 11 additions & 11 deletions lib/candy_check/app_store/verifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,33 @@ def initialize(config)
# @return [Receipt] if successful
# @return [VerificationFailure] otherwise
def verify(receipt_data, secret = nil)
@verifier = Verification
fetch_receipt_information(receipt_data, secret)
fetch_receipt_information(Verification, [receipt_data, secret])
end

# Calls a subscription verification for the given input
# @param receipt_data [String] the raw data to be verified
# @param secret [string] the optional shared secret
# @param secret [String] optional: shared secret
# @param product_ids [Array<String>] optional: products to filter
# @return [ReceiptCollection] if successful
# @return [Verification] otherwise
def verify_subscription(receipt_data, secret = nil)
@verifier = SubscriptionVerification
fetch_receipt_information(receipt_data, secret)
def verify_subscription(receipt_data, secret = nil, product_ids = nil)
args = [receipt_data, secret, product_ids]
fetch_receipt_information(SubscriptionVerification, args)
end

private

def fetch_receipt_information(receipt_data, secret = nil)
def fetch_receipt_information(verifier_class, args)
default_endpoint, opposite_endpoint = endpoints
result = call_for(default_endpoint, receipt_data, secret)
result = call_for(verifier_class, args.dup.unshift(default_endpoint))
if should_retry?(result)
return call_for(opposite_endpoint, receipt_data, secret)
return call_for(verifier_class, args.dup.unshift(opposite_endpoint))
end
result
end

def call_for(endpoint_url, receipt_data, secret)
@verifier.new(endpoint_url, receipt_data, secret).call!
def call_for(verifier_class, args)
verifier_class.new(*args).call!
end

def should_retry?(result)
Expand Down
33 changes: 33 additions & 0 deletions spec/app_store/subscription_verification_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,45 @@
result = subject.call!
result.must_be_instance_of CandyCheck::AppStore::ReceiptCollection
result.receipts.must_be_instance_of Array
result.receipts.size.must_equal(2)
last = result.receipts.last
last.must_be_instance_of CandyCheck::AppStore::Receipt
last.item_id.must_equal('some_other_id')
end
end

describe 'filtered product_ids' do
subject do
CandyCheck::AppStore::SubscriptionVerification.new(
endpoint,
data,
secret,
product_ids
)
end
let(:product_ids) { ['product_1'] }

it 'returns only filtered reciepts when specifc product_ids are reqested' do
response = {
'status' => 0,
'latest_receipt_info' => [
{ 'item_id' => 'some_id', 'product_id' => 'product_1' },
{ 'item_id' => 'some_other_id', 'product_id' => 'product_1' },
{ 'item_id' => 'some_id', 'product_id' => 'product_2' }
]
}
with_mocked_response(response) do
result = subject.call!
result.must_be_instance_of CandyCheck::AppStore::ReceiptCollection
result.receipts.must_be_instance_of Array
result.receipts.size.must_equal(2)
last = result.receipts.last
last.must_be_instance_of CandyCheck::AppStore::Receipt
last.item_id.must_equal('some_other_id')
end
end
end

private

DummyClient = Struct.new(:response) do
Expand Down
29 changes: 24 additions & 5 deletions spec/app_store/verifier_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@
subject.verify_subscription(
data, secret
).must_be_same_as receipt_collection
assert_recorded([production_endpoint, data, secret])
assert_recorded([production_endpoint, data, secret, nil])
end
end

it 'only uses production endpoint for normal failures' do
failure = get_failure(21_000)
with_mocked_verifier(failure) do
subject.verify_subscription(data, secret).must_be_same_as failure
assert_recorded([production_endpoint, data, secret])
assert_recorded([production_endpoint, data, secret, nil])
end
end

Expand All @@ -106,11 +106,21 @@
with_mocked_verifier(failure, receipt) do
subject.verify_subscription(data, secret).must_be_same_as receipt
assert_recorded(
[production_endpoint, data, secret],
[sandbox_endpoint, data, secret]
[production_endpoint, data, secret, nil],
[sandbox_endpoint, data, secret, nil]
)
end
end

it 'passed the product_ids' do
product_ids = ['product_1']
with_mocked_verifier(receipt_collection) do
subject.verify_subscription(
data, secret, product_ids
).must_be_same_as receipt_collection
assert_recorded([production_endpoint, data, secret, product_ids])
end
end
end

private
Expand All @@ -134,8 +144,17 @@ def get_failure(code)
CandyCheck::AppStore::VerificationFailure.fetch(code)
end

DummyAppStoreVerification = Struct.new(:endpoint, :data, :secret) do
class DummyAppStoreVerification
attr_reader :endpoint, :data, :secret, :product_ids
attr_accessor :results

def initialize(endpoint, data, secret, product_ids = nil)
@endpoint = endpoint
@data = data
@secret = secret
@product_ids = product_ids
end

def call!
results.shift
end
Expand Down