Skip to content

Commit

Permalink
Add additional method for new webhook handler
Browse files Browse the repository at this point in the history
  • Loading branch information
lizkenyon committed Jan 23, 2024
1 parent 59f1536 commit e951579
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 20 deletions.
44 changes: 38 additions & 6 deletions docs/usage/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,42 @@ If using in the Rails framework, we highly recommend you use the [shopify_app](h

## Create a Webhook Handler

### New webhook handler as of Version 13.5.0
If you want to register for an http webhook you need to implement a webhook handler which the `shopify_api` gem can use to determine how to process your webhook. You can make multiple implementations (one per topic) or you can make one implementation capable of handling all the topics you want to subscribe to. To do this simply make a module or class that includes or extends `ShopifyAPI::Webhooks::WebhookHandler` and implement the `handle_webhook` method which accepts the following named parameters: data: `Hash[Symbol, untyped]`. An example implementation is shown below:

You will also have to define a method called `use_handle_webhook?` which returns a boolean. If this method returns `true` then the `handle_webhook` method will be called when a webhook is received. If this method returns `false` then the `handle` method will be called when a webhook is received. The `handle` method is the old way of handling webhooks and will be deprecated in a future version of the gem. When using this method you will not have access to the `webhook_id` or `api_version` of the webhook.

`data` will have the following keys
- `:topic` - The topic of the webhook
- `:shop` - The shop domain of the webhook
- `:body` - The body of the webhook
- `:webhook_id` - The id of the webhook event to [avoid duplicates](https://shopify.dev/docs/apps/webhooks/best-practices#ignore-duplicates)
- `:api_version` - The api version of the webhook

```ruby
module WebhookHandler
extend ShopifyAPI::Webhooks::Handler

class << self
def handle_webhook(data)
puts "Received webhook! topic: #{data[:topic]} shop: #{data[:shop]} body: #{data[:body]} webhook_id: #{data[:webhook_id]} api_version: #{data[:api_version]"
end
def use_handle_webhook?
true
end
end
end
```
**Note:** As of version 13.5.0 the `handle` method is still available to be used but will be removed in a future version of the gem. It is recommended that you use the `handle_webhook` method instead.
### Webhook Handler for versions 13.4.0 and prior
If you want to register for an http webhook you need to implement a webhook handler which the `shopify_api` gem can use to determine how to process your webhook. You can make multiple implementations (one per topic) or you can make one implementation capable of handling all the topics you want to subscribe to. To do this simply make a module or class that includes or extends `ShopifyAPI::Webhooks::WebhookHandler` and implement the handle method which accepts the following named parameters: topic: `String`, shop: `String`, and body: `Hash[String, untyped]`. An example implementation is shown below:
```ruby
module WebhookHandler
module WebhookHandler
extend ShopifyAPI::Webhooks::Handler
class << self
Expand All @@ -28,18 +60,18 @@ end
The next step is to add all the webhooks you would like to subscribe to for any shop to the webhook registry. To do this you can call `ShopifyAPI::Webhooks::Registry.add_registration` for each webhook you would like to handle. `add_registration` accepts a topic string, a delivery_method symbol (currently supporting `:http`, `:event_bridge`, and `:pub_sub`), a webhook path (the relative path for an http webhook) and a handler. This only needs to be done once when the app is started and we recommend doing this at the same time that you setup `ShopifyAPI::Context`. An example is shown below to register an http webhook:
```ruby
registration = ShopifyAPI::Webhooks::Registry.add_registration(topic: "orders/create",
registration = ShopifyAPI::Webhooks::Registry.add_registration(topic: "orders/create",
delivery_method: :http,
handler: WebhookHandler,
path: 'callback/orders/create')
path: 'callback/orders/create')
```
If you are only interested in particular fields, you can optionally filter the data sent by Shopify by specifying the `fields` parameter. Note that you will still receive a webhook request from Shopify every time the resource is updated, but only the specified fields will be sent:
```ruby
registration = ShopifyAPI::Webhooks::Registry.add_registration(
topic: "orders/create",
delivery_method: :http,
handler: WebhookHandler,
topic: "orders/create",
delivery_method: :http,
handler: WebhookHandler,
path: 'callback/orders/create',
fields: ["number","note"] # this can also be a single comma separated string
)
Expand Down
22 changes: 18 additions & 4 deletions lib/shopify_api/webhooks/handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,29 @@
module ShopifyAPI
module Webhooks
module Handler
include Kernel
extend T::Sig
extend T::Helpers
interface!
abstract!

sig do
abstract.params(topic: String, shop: String, body: T::Hash[String, T.untyped],
webhook_id: T.nilable(String), api_version: T.nilable(String)).void
params(data: T::Hash[Symbol, T.untyped]).void
end
def handle_webhook(data)
if use_handle_webhook?
raise NotImplementedError, "You must implement the `handle_webhook` method in your webhook handler class."
end
end

sig do
abstract.params(topic: String, shop: String, body: T::Hash[String, T.untyped]).void
end
def handle(topic:, shop:, body:); end

sig { returns(T::Boolean) }
def use_handle_webhook?
false
end
def handle(topic:, shop:, body:, webhook_id:, api_version:); end
end
end
end
12 changes: 10 additions & 2 deletions lib/shopify_api/webhooks/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,16 @@ def process(request)
raise Errors::NoWebhookHandler, "No webhook handler found for topic: #{request.topic}."
end

handler.handle(topic: request.topic, shop: request.shop, body: request.parsed_body,
webhook_id: request.webhook_id, api_version: request.api_version)
if handler.use_handle_webhook?
handler.handle_webhook({ topic: request.topic, shop: request.shop, body: request.parsed_body,
webhook_id: request.webhook_id, api_version: request.api_version, })
else
handler.handle(topic: request.topic, shop: request.shop, body: request.parsed_body)
ShopifyAPI::Logger.warn(<<~WARNING)
DEPRECATED: Use ShopifyAPI::Webhooks::Handler#handle_webhook
instead of ShopifyAPI::Webhooks::Handler#handle
WARNING
end
end

private
Expand Down
4 changes: 2 additions & 2 deletions test/test_helpers/fake_webhook_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ def initialize(handler)
@handler = handler
end

def handle(topic:, shop:, body:, webhook_id:, api_version:)
@handler.call(topic, shop, body, webhook_id, api_version)
def handle(topic:, shop:, body:)
@handler.call(topic, shop, body)
end
end
end
24 changes: 24 additions & 0 deletions test/test_helpers/new_fake_webhook_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# typed: false
# frozen_string_literal: true

module TestHelpers
class NewFakeWebhookHandler
include ShopifyAPI::Webhooks::Handler

def initialize(handler)
@handler = handler
end

def handle(topic:, shop:, body:)
@handler.call(topic, shop, body)
end

def handle_webhook(data)
@handler.call(data)
end

def use_handle_webhook?
true
end
end
end
129 changes: 123 additions & 6 deletions test/webhooks/registry_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,33 @@ def test_process
handler_called = false

handler = TestHelpers::FakeWebhookHandler.new(
lambda do |topic, shop, body, webhook_id, api_version|
lambda do |topic, shop, body,|
assert_equal(@topic, topic)
assert_equal(@shop, shop)
assert_equal({}, body)
assert_equal("b1234-eefd-4c9e-9520-049845a02082", webhook_id)
assert_equal("2024-01", api_version)
handler_called = true
end,
)

ShopifyAPI::Webhooks::Registry.add_registration(
topic: @topic, path: "path", delivery_method: :http, handler: handler,
)

ShopifyAPI::Webhooks::Registry.process(@webhook_request)

assert(handler_called)
end

def test_process_new_handler
handler_called = false

handler = TestHelpers::NewFakeWebhookHandler.new(
lambda do |data|
assert_equal(@topic, data[:topic])
assert_equal(@shop, data[:shop])
assert_equal({}, data[:body])
assert_equal("b1234-eefd-4c9e-9520-049845a02082", data[:webhook_id])
assert_equal("2024-01", data[:api_version])
handler_called = true
end,
)
Expand Down Expand Up @@ -325,7 +346,7 @@ def do_registration_test(delivery_method, path, fields: nil, metafield_namespace
delivery_method: delivery_method,
path: path,
handler: TestHelpers::FakeWebhookHandler.new(
lambda do |topic, shop, body, webhook_id, api_version|
lambda do |topic, shop, body|
end,
),
fields: fields,
Expand All @@ -351,7 +372,78 @@ def do_registration_test(delivery_method, path, fields: nil, metafield_namespace
delivery_method: delivery_method,
path: "#{path}-updated",
handler: TestHelpers::FakeWebhookHandler.new(
lambda do |topic, shop, body, webhook_id, api_version|
lambda do |topic, shop, body|
end,
),
)
update_registration_response = ShopifyAPI::Webhooks::Registry.register_all(
session: @session,
)[0]

assert(update_registration_response.success)
assert_equal(queries[delivery_method][:register_update_response], update_registration_response.body)
end

def do_registration_test_new_handler(delivery_method, path, fields: nil, metafield_namespaces: nil)
ShopifyAPI::Webhooks::Registry.clear

check_query_body = { query: queries[delivery_method][:check_query], variables: nil }

stub_request(:post, @url)
.with(body: JSON.dump(check_query_body))
.to_return({ status: 200, body: JSON.dump(queries[delivery_method][:check_empty_response]) })

add_query_type = if fields
:register_add_query_with_fields
elsif metafield_namespaces
:register_add_query_with_metafield_namespaces
else
:register_add_query
end
add_response_type = if fields
:register_add_with_fields_response
elsif metafield_namespaces
:register_add_with_metafield_namespaces_response
else
:register_add_response
end

stub_request(:post, @url)
.with(body: JSON.dump({ query: queries[delivery_method][add_query_type], variables: nil }))
.to_return({ status: 200, body: JSON.dump(queries[delivery_method][add_response_type]) })

ShopifyAPI::Webhooks::Registry.add_registration(
topic: @topic,
delivery_method: delivery_method,
path: path,
handler: TestHelpers::NewFakeWebhookHandler.new(
lambda do |data|
end,
),
fields: fields,
metafield_namespaces: metafield_namespaces,
)
registration_response = ShopifyAPI::Webhooks::Registry.register_all(
session: @session,
)[0]

assert(registration_response.success)
assert_equal(queries[delivery_method][add_response_type], registration_response.body)

stub_request(:post, @url)
.with(body: JSON.dump(check_query_body))
.to_return({ status: 200, body: JSON.dump(queries[delivery_method][:check_existing_response]) })

stub_request(:post, @url)
.with(body: JSON.dump({ query: queries[delivery_method][:register_update_query], variables: nil }))
.to_return({ status: 200, body: JSON.dump(queries[delivery_method][:register_update_response]) })

ShopifyAPI::Webhooks::Registry.add_registration(
topic: @topic,
delivery_method: delivery_method,
path: "#{path}-updated",
handler: TestHelpers::NewFakeWebhookHandler.new(
lambda do |data|
end,
),
)
Expand All @@ -376,7 +468,32 @@ def do_registration_check_error_test(delivery_method, path)
delivery_method: delivery_method,
path: path,
handler: TestHelpers::FakeWebhookHandler.new(
lambda do |topic, shop, body, webhook_id, api_version|
lambda do |topic, shop, body|
end,
),
)

assert_raises(StandardError) do
ShopifyAPI::Webhooks::Registry.register_all(
session: @session,
)
end
end

def do_registration_check_error_test_new_handler(delivery_method, path)
ShopifyAPI::Webhooks::Registry.clear
body = { query: queries[delivery_method][:check_query], variables: nil }

stub_request(:post, @url)
.with(body: JSON.dump(body))
.to_return(status: 304)

ShopifyAPI::Webhooks::Registry.add_registration(
topic: @topic,
delivery_method: delivery_method,
path: path,
handler: TestHelpers::NewFakeWebhookHandler.new(
lambda do |data|
end,
),
)
Expand Down

0 comments on commit e951579

Please sign in to comment.