lucid_shopify 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +102 -0
- data/lib/lucid_shopify.rb +26 -0
- data/lib/lucid_shopify/activate_charge.rb +28 -0
- data/lib/lucid_shopify/charge.rb +53 -0
- data/lib/lucid_shopify/client.rb +49 -0
- data/lib/lucid_shopify/create_charge.rb +28 -0
- data/lib/lucid_shopify/credentials.rb +42 -0
- data/lib/lucid_shopify/delegate_webhooks.rb +43 -0
- data/lib/lucid_shopify/delete_request.rb +15 -0
- data/lib/lucid_shopify/fetch_access_token.rb +50 -0
- data/lib/lucid_shopify/get_request.rb +16 -0
- data/lib/lucid_shopify/post_request.rb +16 -0
- data/lib/lucid_shopify/put_request.rb +16 -0
- data/lib/lucid_shopify/request.rb +50 -0
- data/lib/lucid_shopify/request_credentials.rb +14 -0
- data/lib/lucid_shopify/response.rb +113 -0
- data/lib/lucid_shopify/result.rb +33 -0
- data/lib/lucid_shopify/send_request.rb +64 -0
- data/lib/lucid_shopify/send_throttled_request.rb +50 -0
- data/lib/lucid_shopify/verify_callback.rb +70 -0
- data/lib/lucid_shopify/verify_webhook.rb +40 -0
- data/lib/lucid_shopify/version.rb +5 -0
- data/lib/lucid_shopify/webhook.rb +28 -0
- data/lib/lucid_shopify/webhooks.rb +82 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e4631f8c0d8631d94e42e0213c32c9e349c2f86648fd0816acb98595f8891b98
|
4
|
+
data.tar.gz: ce3b5019de9bd7b89bb20a01c6bc61ad23f5ea04290f752fdd8ac6121ff637fa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 793fed51a3be7594e48d4a6f2190d8651a30846e50bf13691e547618aa82358bb91b4824d47ca7987df02a15896fef864350db61e3e3c4ad616d920eb4400db0
|
7
|
+
data.tar.gz: bab3fdf7ae40219c614db5c76436392468d92ec766ccdf06773c31572a0b20995ccff7c5b8701cea5a366f22883e188294d1cadb9405ebaaf13f194fa3cd1dc2
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
lucid_shopify
|
2
|
+
=============
|
3
|
+
|
4
|
+
Installation
|
5
|
+
------------
|
6
|
+
|
7
|
+
Add the following lines to your ‘Gemfile’:
|
8
|
+
|
9
|
+
git_source :lucid { |r| "https://github.com/lucidnz/gem-lucid_#{r}.git" }
|
10
|
+
|
11
|
+
gem 'lucid_shopify', lucid: 'shopify'
|
12
|
+
|
13
|
+
|
14
|
+
Usage
|
15
|
+
-----
|
16
|
+
|
17
|
+
### Configure the default API client credentials
|
18
|
+
|
19
|
+
LucidShopify.credentials = LucidShopify::Credentials.new(
|
20
|
+
'...', # api_key
|
21
|
+
'...', # shared_secret
|
22
|
+
'...', # scope
|
23
|
+
'...', # billing_callback_uri
|
24
|
+
'...', # webhook_uri
|
25
|
+
)
|
26
|
+
|
27
|
+
Alternatively, a credentials object may be passed as a keyword
|
28
|
+
argument to any of the classes that make use of it.
|
29
|
+
|
30
|
+
Additionally, each API request requires authorization:
|
31
|
+
|
32
|
+
request_credentials = LucidShopify::RequestCredentials.new(
|
33
|
+
'...', # myshopify_domain
|
34
|
+
'...', # access_token
|
35
|
+
)
|
36
|
+
|
37
|
+
If the access token is omitted, the request will be unauthorized.
|
38
|
+
This is only useful during the OAuth2 process.
|
39
|
+
|
40
|
+
|
41
|
+
### Configure webhooks
|
42
|
+
|
43
|
+
Configure each webhook the app will create (if any):
|
44
|
+
|
45
|
+
LucidShopify.webhooks << {topic: 'orders/create', fields: %w(id tags)}
|
46
|
+
LucidShopify.webhooks << {topic: '...', fields: %w(...)}
|
47
|
+
|
48
|
+
|
49
|
+
### Register webhook handlers
|
50
|
+
|
51
|
+
For each webhook, register one or more handlers:
|
52
|
+
|
53
|
+
delegate_webhooks = LucidShopify::DelegateWebhooks.default
|
54
|
+
|
55
|
+
delegate_webhooks.register('orders/create', OrdersCreateWebhook.new)
|
56
|
+
|
57
|
+
See the inline method documentation for more detail.
|
58
|
+
|
59
|
+
To call/delegate a webhook to its handler for processing, you will likely want
|
60
|
+
to create a worker around something like this:
|
61
|
+
|
62
|
+
webhook = LucidShopify::Webhook.new(myshopify_domain, topic, data)
|
63
|
+
|
64
|
+
delegate_webhooks.(webhook)
|
65
|
+
|
66
|
+
|
67
|
+
### Create and delete webhooks
|
68
|
+
|
69
|
+
Create/delete all configured webhooks (see above):
|
70
|
+
|
71
|
+
webhooks = LucidShopify::Webhooks.new
|
72
|
+
|
73
|
+
webhooks.create_all(request_credentials)
|
74
|
+
webhooks.delete_all(request_credentials)
|
75
|
+
|
76
|
+
Create/delete webhooks manually:
|
77
|
+
|
78
|
+
webhook = {topic: 'orders/create', fields: %w(id tags)}
|
79
|
+
|
80
|
+
webhooks.create(request_credentials, webhook)
|
81
|
+
webhooks.delete(request_credentials, webhook_id)
|
82
|
+
|
83
|
+
|
84
|
+
### Verification
|
85
|
+
|
86
|
+
Verify callback requests with the request params:
|
87
|
+
|
88
|
+
LucidShopify::Verify::Callback.new.(params_hash).success?
|
89
|
+
|
90
|
+
Verify webhook requests with the request data and the HMAC header:
|
91
|
+
|
92
|
+
LucidShopify::Verify::Webhook.new.(data, hmac).success?
|
93
|
+
|
94
|
+
|
95
|
+
### Authorization
|
96
|
+
|
97
|
+
_TODO_
|
98
|
+
|
99
|
+
|
100
|
+
### Make an API request
|
101
|
+
|
102
|
+
_TODO_
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Primarily for Bundler.
|
4
|
+
|
5
|
+
require 'lucid_shopify/activate_charge'
|
6
|
+
require 'lucid_shopify/charge'
|
7
|
+
require 'lucid_shopify/client'
|
8
|
+
require 'lucid_shopify/create_charge'
|
9
|
+
require 'lucid_shopify/credentials'
|
10
|
+
require 'lucid_shopify/delegate_webhooks'
|
11
|
+
require 'lucid_shopify/delete_request'
|
12
|
+
require 'lucid_shopify/fetch_access_token'
|
13
|
+
require 'lucid_shopify/get_request'
|
14
|
+
require 'lucid_shopify/post_request'
|
15
|
+
require 'lucid_shopify/put_request'
|
16
|
+
require 'lucid_shopify/request_credentials'
|
17
|
+
require 'lucid_shopify/request'
|
18
|
+
require 'lucid_shopify/response'
|
19
|
+
require 'lucid_shopify/result'
|
20
|
+
require 'lucid_shopify/send_request'
|
21
|
+
require 'lucid_shopify/send_throttled_request'
|
22
|
+
require 'lucid_shopify/verify_callback'
|
23
|
+
require 'lucid_shopify/verify_webhook'
|
24
|
+
require 'lucid_shopify/version'
|
25
|
+
require 'lucid_shopify/webhook'
|
26
|
+
require 'lucid_shopify/webhooks'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
|
5
|
+
require 'lucid_shopify/client'
|
6
|
+
|
7
|
+
module LucidShopify
|
8
|
+
class ActivateCharge
|
9
|
+
extend Dry::Initializer
|
10
|
+
|
11
|
+
# @return [Client]
|
12
|
+
option :client, default: proc { Client.new }
|
13
|
+
|
14
|
+
#
|
15
|
+
# Activate a recurring application charge.
|
16
|
+
#
|
17
|
+
# @param request_credentials [RequestCredentials]
|
18
|
+
# @param charge [Hash, #to_h] an accepted charge received from Shopify via callback
|
19
|
+
#
|
20
|
+
# @return [Hash] the active charge
|
21
|
+
#
|
22
|
+
def call(request_credentials, charge)
|
23
|
+
data = client.post_json(request_credentials, "recurring_application_charges/#{charge_id}/activate", charge.to_h)
|
24
|
+
|
25
|
+
data['recurring_application_charge']
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
|
5
|
+
require 'lucid_shopify/credentials'
|
6
|
+
|
7
|
+
module LucidShopify
|
8
|
+
#
|
9
|
+
# Provides a convenient way to build the charge hash for {CreateCharge}.
|
10
|
+
#
|
11
|
+
class Charge
|
12
|
+
extend Dry::Initializer
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
param :plan_name
|
16
|
+
# @return [Integer]
|
17
|
+
param :price
|
18
|
+
# @return [Integer] requires price_terms
|
19
|
+
option :price_cap, optional: true
|
20
|
+
# @return [String] requires price_cap
|
21
|
+
option :price_terms, optional: true
|
22
|
+
# @return [Boolean] is this a test charge?
|
23
|
+
option :test, default: proc { false }
|
24
|
+
# @return [Integer]
|
25
|
+
option :trial_days, default: proc { 7 }
|
26
|
+
# @return [Credentials]
|
27
|
+
option :credentials, default: proc { LucidShopify.credentials }
|
28
|
+
|
29
|
+
#
|
30
|
+
# Map to the Shopify API structure.
|
31
|
+
#
|
32
|
+
# @return [Hash]
|
33
|
+
#
|
34
|
+
def to_h
|
35
|
+
{}.tap do |hash|
|
36
|
+
hash[:name] = plan_name
|
37
|
+
hash[:price] = price
|
38
|
+
hash[:capped_amount] = price_cap if usage_based_billing?
|
39
|
+
hash[:terms] = price_terms if usage_based_billing?
|
40
|
+
hash[:return_url] = credentials.billing_callback_uri
|
41
|
+
hash[:test] = test if test
|
42
|
+
hash[:trial_days] = trial_days if trial_days
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# @return [Boolean]
|
48
|
+
#
|
49
|
+
private def usage_based_billing?
|
50
|
+
price_cap && price_terms
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid_shopify/send_request'
|
4
|
+
|
5
|
+
%w(delete get post put).each do |method|
|
6
|
+
require "lucid_shopify/#{method}_request"
|
7
|
+
end
|
8
|
+
|
9
|
+
module LucidShopify
|
10
|
+
class Client
|
11
|
+
#
|
12
|
+
# @param send_request [SendRequest]
|
13
|
+
#
|
14
|
+
def initialize(send_request: SendRequest.new)
|
15
|
+
@send_request = send_request
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [SendRequest]
|
19
|
+
attr_reader :send_request
|
20
|
+
|
21
|
+
#
|
22
|
+
# @see {DeleteRequest#initialize}
|
23
|
+
#
|
24
|
+
def delete(*args)
|
25
|
+
send_request.(DeleteRequest.new(*args))
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# @see {GetRequest#initialize}
|
30
|
+
#
|
31
|
+
def get(*args)
|
32
|
+
send_request.(GetRequest.new(*args))
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# @see {PostRequest#initialize}
|
37
|
+
#
|
38
|
+
def post_json(*args)
|
39
|
+
send_request.(PostRequest.new(*args))
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# @see {PutRequest#initialize}
|
44
|
+
#
|
45
|
+
def put_json(*args)
|
46
|
+
send_request.(PutRequest.new(*args))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
|
5
|
+
require 'lucid_shopify/client'
|
6
|
+
|
7
|
+
module LucidShopify
|
8
|
+
class CreateCharge
|
9
|
+
extend Dry::Initializer
|
10
|
+
|
11
|
+
# @return [Client]
|
12
|
+
option :client, default: proc { Client.new }
|
13
|
+
|
14
|
+
#
|
15
|
+
# Create a new recurring application charge.
|
16
|
+
#
|
17
|
+
# @param request_credentials [RequestCredentials]
|
18
|
+
# @param charge [Hash, #to_h]
|
19
|
+
#
|
20
|
+
# @return [Hash] the pending charge
|
21
|
+
#
|
22
|
+
def call(request_credentials, charge)
|
23
|
+
data = client.post_json(request_credentials, 'recurring_application_charge', charge.to_h)
|
24
|
+
|
25
|
+
data['recurring_application_charge']
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
|
5
|
+
module LucidShopify
|
6
|
+
MissingCredentialsError = Class.new(StandardError)
|
7
|
+
|
8
|
+
class Credentials
|
9
|
+
extend Dry::Initializer
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
param :api_key
|
13
|
+
# @return [String]
|
14
|
+
param :shared_secret
|
15
|
+
# @return [String]
|
16
|
+
param :scope
|
17
|
+
# @return [String]
|
18
|
+
param :billing_callback_uri
|
19
|
+
# @return [String]
|
20
|
+
param :webhook_uri
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class << LucidShopify
|
25
|
+
#
|
26
|
+
# Assign default API credentials.
|
27
|
+
#
|
28
|
+
# @param credentials [LucidShopify::Credentials]
|
29
|
+
#
|
30
|
+
attr_writer :credentials
|
31
|
+
|
32
|
+
#
|
33
|
+
# @return [LucidShopify::Credentials]
|
34
|
+
#
|
35
|
+
# @raise [LucidShopify::MissingCredentialsError] if credentials are unset
|
36
|
+
#
|
37
|
+
def credentials
|
38
|
+
raise MissingCredentialsError unless @credentials
|
39
|
+
|
40
|
+
@credentials
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LucidShopify
|
4
|
+
class DelegateWebhooks
|
5
|
+
class << self
|
6
|
+
#
|
7
|
+
# @return [DelegateWebhooks]
|
8
|
+
#
|
9
|
+
def default
|
10
|
+
@default ||= new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@handlers = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Hash<String, Array<#call>>]
|
19
|
+
attr_reader :handlers
|
20
|
+
|
21
|
+
#
|
22
|
+
# Call each of the handlers registered for the given topic in turn. See
|
23
|
+
# {#register} below for more on webhook handlers.
|
24
|
+
#
|
25
|
+
# @param webhook [LucidShopify::Webhook]
|
26
|
+
#
|
27
|
+
def call(webhook)
|
28
|
+
handlers[webhook.topic]&.each { |handler| handler.(webhook) }
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Register a handler for a webhook topic. The callable handler will be
|
33
|
+
# called with the argument passed to {#call}.
|
34
|
+
#
|
35
|
+
# @param topic [String] e.g. 'orders/create'
|
36
|
+
# @param handler [#call]
|
37
|
+
#
|
38
|
+
def register(topic, handler)
|
39
|
+
handlers[topic] ||= []
|
40
|
+
handlers[topic] << handler
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid_shopify/request'
|
4
|
+
|
5
|
+
module LucidShopify
|
6
|
+
class DeleteRequest < Request
|
7
|
+
#
|
8
|
+
# @param credentials [RequestCredentials]
|
9
|
+
# @param path [String] the endpoint relative to the base URL
|
10
|
+
#
|
11
|
+
def initialize(credentials, path)
|
12
|
+
super(credentials, :delete, path)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
|
5
|
+
require 'lucid_shopify/client'
|
6
|
+
|
7
|
+
module LucidShopify
|
8
|
+
class FetchAccessToken
|
9
|
+
Error = Class.new(StandardError)
|
10
|
+
|
11
|
+
extend Dry::Initializer
|
12
|
+
|
13
|
+
# @return [Client]
|
14
|
+
option :client, default: proc { Client.new }
|
15
|
+
# @return [Credentials]
|
16
|
+
option :credentials, default: proc { LucidShopify.credentials }
|
17
|
+
|
18
|
+
#
|
19
|
+
# Exchange an authorization code for a new Shopify access token.
|
20
|
+
#
|
21
|
+
# @param request_credentials [RequestCredentials]
|
22
|
+
# @param authorization_code [String]
|
23
|
+
#
|
24
|
+
# @return [String] the access token
|
25
|
+
#
|
26
|
+
# @raise [Error] if the response is invalid
|
27
|
+
#
|
28
|
+
def call(request_credentials, authorization_code)
|
29
|
+
data = client.post_json(request_credentials, 'oauth/access_token', post_data(authorization_code))
|
30
|
+
|
31
|
+
raise Error if data['access_token'].nil?
|
32
|
+
raise Error if data['scope'] != credentials.scope
|
33
|
+
|
34
|
+
data['access_token']
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# @param authorization_code [String]
|
39
|
+
#
|
40
|
+
# @return [Hash]
|
41
|
+
#
|
42
|
+
private def post_data(authorization_code)
|
43
|
+
{
|
44
|
+
client_id: credentials.api_key,
|
45
|
+
client_secret: credentials.shared_secret,
|
46
|
+
code: authorization_code,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid_shopify/request'
|
4
|
+
|
5
|
+
module LucidShopify
|
6
|
+
class GetRequest < Request
|
7
|
+
#
|
8
|
+
# @param credentials [RequestCredentials]
|
9
|
+
# @param path [String] the endpoint relative to the base URL
|
10
|
+
# @param params [Hash] the query params
|
11
|
+
#
|
12
|
+
def initialize(credentials, path, params = {})
|
13
|
+
super(credentials, :get, path, params: params)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid_shopify/request'
|
4
|
+
|
5
|
+
module LucidShopify
|
6
|
+
class PostRequest < Request
|
7
|
+
#
|
8
|
+
# @param credentials [RequestCredentials]
|
9
|
+
# @param path [String] the endpoint relative to the base URL
|
10
|
+
# @param json [Hash] the JSON request body
|
11
|
+
#
|
12
|
+
def initialize(credentials, path, json)
|
13
|
+
super(credentials, :post, path, json: json)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid_shopify/request'
|
4
|
+
|
5
|
+
module LucidShopify
|
6
|
+
class PutRequest < Request
|
7
|
+
#
|
8
|
+
# @param credentials [RequestCredentials]
|
9
|
+
# @param path [String] the endpoint relative to the base URL
|
10
|
+
# @param json [Hash] the JSON request body
|
11
|
+
#
|
12
|
+
def initialize(credentials, path, json)
|
13
|
+
super(credentials, :put, path, json: json)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
|
5
|
+
module LucidShopify
|
6
|
+
#
|
7
|
+
# @abstract
|
8
|
+
#
|
9
|
+
class Request
|
10
|
+
extend Dry::Initializer
|
11
|
+
|
12
|
+
# @return [RequestCredentials]
|
13
|
+
param :credentials
|
14
|
+
# @return [Symbol]
|
15
|
+
param :http_method
|
16
|
+
# @return [String] the endpoint relative to the base URL
|
17
|
+
param :path, reader: :private
|
18
|
+
# @return [Hash]
|
19
|
+
param :options, default: proc { {} }
|
20
|
+
|
21
|
+
# @return [Hash]
|
22
|
+
param :http_headers, default: proc { build_headers }
|
23
|
+
# @return [String]
|
24
|
+
param :url, default: proc { build_url }
|
25
|
+
|
26
|
+
#
|
27
|
+
# @return [String]
|
28
|
+
#
|
29
|
+
private def build_url
|
30
|
+
admin_url = "https://#{credentials.myshopify_domain}/admin"
|
31
|
+
|
32
|
+
path = path.sub(/^\//, '')
|
33
|
+
path = path.sub(/\.json$/, '')
|
34
|
+
|
35
|
+
admin_url + '/' + path + '.json'
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# @return [Hash]
|
40
|
+
#
|
41
|
+
private def build_headers
|
42
|
+
access_token = credentials.access_token
|
43
|
+
|
44
|
+
{}.tap do |headers|
|
45
|
+
headers['Accept'] = 'application/json'
|
46
|
+
headers['X-Shopify-Access-token'] = access_token if access_token
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
|
5
|
+
module LucidShopify
|
6
|
+
class RequestCredentials
|
7
|
+
extend Dry::Initializer
|
8
|
+
|
9
|
+
# @return [String]
|
10
|
+
param :myshopify_domain
|
11
|
+
# @return [String, nil] if {nil}, request will be unauthorized
|
12
|
+
param :access_token, optional: true
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module LucidShopify
|
7
|
+
class Response
|
8
|
+
#
|
9
|
+
# @abstract
|
10
|
+
#
|
11
|
+
class Error < StandardError
|
12
|
+
extend Dry::Initializer
|
13
|
+
|
14
|
+
# @return [Request]
|
15
|
+
param :request
|
16
|
+
# @return [Response]
|
17
|
+
param :response
|
18
|
+
|
19
|
+
#
|
20
|
+
# @return [String]
|
21
|
+
#
|
22
|
+
def message
|
23
|
+
"bad response (#{response.status_code})"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ClientError = Class.new(Error)
|
28
|
+
ServerError = Class.new(Error)
|
29
|
+
|
30
|
+
extend Dry::Initializer
|
31
|
+
|
32
|
+
# @return [Request] the original request
|
33
|
+
param :request
|
34
|
+
# @return [Integer]
|
35
|
+
param :status_code
|
36
|
+
# @return [Hash]
|
37
|
+
param :headers
|
38
|
+
# @return [String]
|
39
|
+
param :data
|
40
|
+
# @return [Hash] the parsed response body
|
41
|
+
param :data_hash, default: proc { parse_data }
|
42
|
+
|
43
|
+
#
|
44
|
+
# @return [Hash]
|
45
|
+
#
|
46
|
+
private def parse_data
|
47
|
+
return {} unless json?
|
48
|
+
|
49
|
+
JSON.parse(data)
|
50
|
+
end
|
51
|
+
# private def parse_data(data)
|
52
|
+
# JSON.parse(data)
|
53
|
+
# rescue JSON::ParserError
|
54
|
+
# {}
|
55
|
+
# end
|
56
|
+
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
#
|
60
|
+
private def json?
|
61
|
+
headers['Content-Type'] =~ /application\/json/
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# @return [String]
|
66
|
+
#
|
67
|
+
# @see {#assert!}
|
68
|
+
#
|
69
|
+
def data!
|
70
|
+
assert!.data
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# @return [Hash] the parsed response body
|
75
|
+
#
|
76
|
+
# @see {#assert!}
|
77
|
+
#
|
78
|
+
def data_hash!
|
79
|
+
assert!.data_hash
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# @raise [ClientError] for status 4xx
|
84
|
+
# @raise [ServerError] for status 5xx
|
85
|
+
#
|
86
|
+
# @return [self]
|
87
|
+
#
|
88
|
+
def assert!
|
89
|
+
case status_code
|
90
|
+
when 400..499
|
91
|
+
raise ClientError.new(request, self)
|
92
|
+
when 500..599
|
93
|
+
raise ServerError.new(request, self)
|
94
|
+
end
|
95
|
+
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# @return [Boolean]
|
101
|
+
#
|
102
|
+
def success?
|
103
|
+
status_code.between?(200, 299)
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# @return [Boolean]
|
108
|
+
#
|
109
|
+
def failure?
|
110
|
+
!success?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LucidShopify
|
4
|
+
class Result
|
5
|
+
#
|
6
|
+
# @param value [Object]
|
7
|
+
# @param error [Object]
|
8
|
+
#
|
9
|
+
def initialize(value, error = nil)
|
10
|
+
@value = value
|
11
|
+
@error = error
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Object]
|
15
|
+
attr_reader :value
|
16
|
+
# @return [Object]
|
17
|
+
attr_reader :error
|
18
|
+
|
19
|
+
#
|
20
|
+
# @return [Boolean]
|
21
|
+
#
|
22
|
+
def success?
|
23
|
+
error.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# @return [Boolean]
|
28
|
+
#
|
29
|
+
def failure?
|
30
|
+
!success?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
require 'http'
|
5
|
+
|
6
|
+
require 'lucid_shopify/response'
|
7
|
+
|
8
|
+
module LucidShopify
|
9
|
+
class SendRequest
|
10
|
+
class NetworkError < StandardError
|
11
|
+
extend Dry::Initializer
|
12
|
+
|
13
|
+
# @return [HTTP::Error]
|
14
|
+
param :original_exception
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# @param request [Request]
|
19
|
+
# @param attempts [Integer] additional request attempts on client error
|
20
|
+
#
|
21
|
+
# @return [Hash] the parsed response body
|
22
|
+
#
|
23
|
+
# @raise [NetworkError] if the request failed all attempts
|
24
|
+
# @raise [Response::ClientError] for status 4xx
|
25
|
+
# @raise [Response::ServerError] for status 5xx
|
26
|
+
#
|
27
|
+
def call(request, attempts = default_attempts)
|
28
|
+
req = request
|
29
|
+
res = send(req.http_method, req.url, req.options)
|
30
|
+
res = Response.new(req, res.code, res.headers.to_h, res.to_s)
|
31
|
+
|
32
|
+
res.data_hash!
|
33
|
+
rescue *http_network_errors => e
|
34
|
+
raise NetworkError.new(e), e.message if attempts.zero?
|
35
|
+
|
36
|
+
call(request, attempts - 1)
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# @return [HTTP::Response]
|
41
|
+
#
|
42
|
+
private def send(http_method, url, options)
|
43
|
+
HTTP.headers(request.headers).__send__(http_method, url, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# @return [Integer]
|
48
|
+
#
|
49
|
+
private def default_attempts
|
50
|
+
3
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# @return [Array<Class>]
|
55
|
+
#
|
56
|
+
private def http_network_errors
|
57
|
+
[
|
58
|
+
HTTP::ConnectionError,
|
59
|
+
HTTP::ResponseError,
|
60
|
+
HTTP::TimeoutError,
|
61
|
+
]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid_shopify/send_request'
|
4
|
+
|
5
|
+
module LucidShopify
|
6
|
+
class SendThrottledRequest < SendRequest
|
7
|
+
MINIMUM_INTERVAL = 500 # ms
|
8
|
+
|
9
|
+
#
|
10
|
+
# @see {SendRequest#call}
|
11
|
+
#
|
12
|
+
private def call(*)
|
13
|
+
interval
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Sleep for the difference if time since the last request is less than the
|
20
|
+
# MINIMUM_INTERVAL.
|
21
|
+
#
|
22
|
+
# @note Throttling is only maintained across a single thread.
|
23
|
+
#
|
24
|
+
private def interval
|
25
|
+
if Thread.current[interval_key]
|
26
|
+
(timestamp - Thread.current[interval_key]).tap do |n|
|
27
|
+
sleep(Rational(n, 1000)) if n < MINIMUM_INTERVAL
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Thread.current[interval_key] = timestamp
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# @return [String]
|
36
|
+
#
|
37
|
+
private def interval_key
|
38
|
+
'%s[%s].timestamp' % [self.class, request.credentials.myshopify_domain]
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Time in milliseconds since the UNIX epoch.
|
43
|
+
#
|
44
|
+
# @return [Integer]
|
45
|
+
#
|
46
|
+
private def timestamp
|
47
|
+
(Time.now.to_f * 1000).to_i
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
require 'lucid_shopify/credentials'
|
6
|
+
require 'lucid_shopify/result'
|
7
|
+
|
8
|
+
module LucidShopify
|
9
|
+
class VerifyCallback
|
10
|
+
#
|
11
|
+
# @param credentials [LucidShopify::Credentials]
|
12
|
+
#
|
13
|
+
def initialize(credentials: LucidShopify.credentials)
|
14
|
+
@credentials = credentials
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [LucidShopify::Credentials]
|
18
|
+
attr_reader :credentials
|
19
|
+
|
20
|
+
#
|
21
|
+
# Verify that the callback request originated from Shopify.
|
22
|
+
#
|
23
|
+
# @param params_hash [Hash] the request params
|
24
|
+
#
|
25
|
+
# @return [Result]
|
26
|
+
#
|
27
|
+
def call(params_hash)
|
28
|
+
digest = OpenSSL::Digest::SHA256.new
|
29
|
+
digest = OpenSSL::HMAC.hexdigest(digest, credentials.shared_secret, encoded_params(params_hash))
|
30
|
+
result = digest == params_hash[:hmac]
|
31
|
+
|
32
|
+
Result.new(result, result ? nil : 'invalid request')
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# @param params_hash [Hash]
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
#
|
40
|
+
private def encoded_params(params_hash)
|
41
|
+
params_hash.reject do |k, _|
|
42
|
+
k == :hmac
|
43
|
+
end.map do |k, v|
|
44
|
+
encode_key(k) + '=' + encode_value(v)
|
45
|
+
end.join('&')
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# @param k [String, Symbol]
|
50
|
+
#
|
51
|
+
# @return [String]
|
52
|
+
#
|
53
|
+
private def encode_key(k)
|
54
|
+
k.to_s.gsub(/./) do |chr|
|
55
|
+
{'%' => '%25', '&' => '%26', '=' => '%3D'}[chr] || chr
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# @param v [String]
|
61
|
+
#
|
62
|
+
# @return [String]
|
63
|
+
#
|
64
|
+
private def encode_value(v)
|
65
|
+
v.gsub(/./) do |chr|
|
66
|
+
{'%' => '%25', '&' => '%26'}[chr] || chr
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
require 'lucid_shopify/credentials'
|
7
|
+
require 'lucid_shopify/result'
|
8
|
+
|
9
|
+
module LucidShopify
|
10
|
+
class VerifyWebhook
|
11
|
+
Error = Class.new(StandardError)
|
12
|
+
|
13
|
+
#
|
14
|
+
# @param credentials [LucidShopify::Credentials]
|
15
|
+
#
|
16
|
+
def initialize(credentials: LucidShopify.credentials)
|
17
|
+
@credentials = credentials
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [LucidShopify::Credentials]
|
21
|
+
attr_reader :credentials
|
22
|
+
|
23
|
+
#
|
24
|
+
# Verify that the webhook request originated from Shopify.
|
25
|
+
#
|
26
|
+
# @param data [String] the signed request data
|
27
|
+
# @param hmac [String] the signature
|
28
|
+
#
|
29
|
+
# @return [Result]
|
30
|
+
#
|
31
|
+
def call(data, hmac)
|
32
|
+
digest = OpenSSL::Digest::SHA256.new
|
33
|
+
digest = OpenSSL::HMAC.digest(digest, credentials.shared_secret, data)
|
34
|
+
digest = Base64.encode64(digest).strip
|
35
|
+
result = digest == hmac
|
36
|
+
|
37
|
+
Result.new(result, result ? nil : 'invalid request')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module LucidShopify
|
7
|
+
class Webhook
|
8
|
+
extend Dry::Initializer
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
param :myshopify_domain
|
12
|
+
# @return [String]
|
13
|
+
param :topic
|
14
|
+
# @return [String]
|
15
|
+
param :data
|
16
|
+
# @return [Hash] the parsed request body
|
17
|
+
param :data_hash, default: proc { parse_data }
|
18
|
+
|
19
|
+
#
|
20
|
+
# @return [Hash]
|
21
|
+
#
|
22
|
+
private def parse_data
|
23
|
+
JSON.parse(data)
|
24
|
+
rescue JSON::ParserError
|
25
|
+
{}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-initializer'
|
4
|
+
|
5
|
+
require 'lucid_shopify/client'
|
6
|
+
require 'lucid_shopify/credentials'
|
7
|
+
|
8
|
+
module LucidShopify
|
9
|
+
class Webhooks
|
10
|
+
extend Dry::Initializer
|
11
|
+
|
12
|
+
# @return [Client]
|
13
|
+
option :client, default: proc { Client.new }
|
14
|
+
# @return [Credentials]
|
15
|
+
option :credentials, default: proc { LucidShopify.credentials }
|
16
|
+
|
17
|
+
#
|
18
|
+
# Delete any existing webhooks, then (re)create all webhooks for the shop.
|
19
|
+
#
|
20
|
+
# @param request_credentials [RequestCredentials]
|
21
|
+
#
|
22
|
+
def create_all(request_credentials)
|
23
|
+
delete_all
|
24
|
+
|
25
|
+
LucidShopify.webhooks.map do |webhook|
|
26
|
+
Thread.new { create(request_credentials, webhook) }
|
27
|
+
end.map(&:value)
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Create a webhook.
|
32
|
+
#
|
33
|
+
# @param request_credentials [RequestCredentials]
|
34
|
+
# @param webhook [Hash]
|
35
|
+
#
|
36
|
+
def create(request_credentials, webhook)
|
37
|
+
data = {}
|
38
|
+
data[:address] = credentials.webhook_uri
|
39
|
+
data[:fields] = webhook[:fields] if webhook[:fields]
|
40
|
+
data[:topic] = webhook[:topic]
|
41
|
+
|
42
|
+
client.post_json(request_credentials, 'webhooks', webhook: data)
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Delete any existing webhooks.
|
47
|
+
#
|
48
|
+
# @param request_credentials [RequestCredentials]
|
49
|
+
#
|
50
|
+
def delete_all(request_credentials)
|
51
|
+
webhooks = client.get('webhooks')['webhooks']
|
52
|
+
|
53
|
+
webhooks.map do |webhook|
|
54
|
+
Thread.new { delete(request_credentials, webhook['id']) }
|
55
|
+
end.map(&:value)
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Delete a webhook.
|
60
|
+
#
|
61
|
+
# @param request_credentials [RequestCredentials]
|
62
|
+
# @param id [Integer]
|
63
|
+
#
|
64
|
+
def delete(request_credentials, id)
|
65
|
+
client.delete(request_credentials, "webhooks/#{id}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class << LucidShopify
|
71
|
+
#
|
72
|
+
# Webhooks created for each shop.
|
73
|
+
#
|
74
|
+
# @return [Array<Hash>]
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# LucidShopify.webhooks << {topic: 'orders/create', fields: %w(id)}
|
78
|
+
#
|
79
|
+
def webhooks
|
80
|
+
@webhooks ||= []
|
81
|
+
end
|
82
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lucid_shopify
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kelsey Judson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-03-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubocop
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.52.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.52.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: dry-initializer
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.4'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: http
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
description:
|
70
|
+
email: kelsey@lucid.nz
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- README.md
|
76
|
+
- lib/lucid_shopify.rb
|
77
|
+
- lib/lucid_shopify/activate_charge.rb
|
78
|
+
- lib/lucid_shopify/charge.rb
|
79
|
+
- lib/lucid_shopify/client.rb
|
80
|
+
- lib/lucid_shopify/create_charge.rb
|
81
|
+
- lib/lucid_shopify/credentials.rb
|
82
|
+
- lib/lucid_shopify/delegate_webhooks.rb
|
83
|
+
- lib/lucid_shopify/delete_request.rb
|
84
|
+
- lib/lucid_shopify/fetch_access_token.rb
|
85
|
+
- lib/lucid_shopify/get_request.rb
|
86
|
+
- lib/lucid_shopify/post_request.rb
|
87
|
+
- lib/lucid_shopify/put_request.rb
|
88
|
+
- lib/lucid_shopify/request.rb
|
89
|
+
- lib/lucid_shopify/request_credentials.rb
|
90
|
+
- lib/lucid_shopify/response.rb
|
91
|
+
- lib/lucid_shopify/result.rb
|
92
|
+
- lib/lucid_shopify/send_request.rb
|
93
|
+
- lib/lucid_shopify/send_throttled_request.rb
|
94
|
+
- lib/lucid_shopify/verify_callback.rb
|
95
|
+
- lib/lucid_shopify/verify_webhook.rb
|
96
|
+
- lib/lucid_shopify/version.rb
|
97
|
+
- lib/lucid_shopify/webhook.rb
|
98
|
+
- lib/lucid_shopify/webhooks.rb
|
99
|
+
homepage: https://github.com/lucidnz/gem-lucid_shopify
|
100
|
+
licenses:
|
101
|
+
- ISC
|
102
|
+
metadata: {}
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 2.7.3
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: Shopify client library
|
123
|
+
test_files: []
|