-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #247 from discolabs/develop
Release 0.16.1
- Loading branch information
Showing
39 changed files
with
996 additions
and
132 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -430,6 +430,170 @@ listen for the `app/uninstalled` and `shop/update` webhook topics. | |
|
||
To check which webhooks are registered by your app run `shop.with_api_context{ShopifyAPI::Webhook.find(:all)}` from your console, where `shop = DiscoApp::Shop.find(your_shop_id)`. | ||
|
||
### Shopify Flow | ||
The gem provides support for [Shopify Flow Connectors][], allowing applications | ||
built with this framework to define and send triggers and receive and process | ||
actions. Each trigger that's created or action that's received is stored in the | ||
database as `DiscoApp::Flow::Trigger` and `DiscoApp::Flow::Action` models | ||
respectively. | ||
|
||
Triggers and actions are processed asynchronously as background jobs. The | ||
success or failure of a trigger or action is stored in a `status` attribute in | ||
the models. If a trigger or action fails for any reason, the reported reasons | ||
for failure are stored in a `processing_errors` attribute. | ||
|
||
Applications that are sending a lot of triggers, or receiving a lot of actions, | ||
may want to clear out the trigger and action database tables periodically. | ||
|
||
[Shopify Flow Connectors]: https://help.shopify.com/en/api/embedded-apps/app-extensions/flow | ||
|
||
#### Triggers | ||
Shopify Flow Triggers are events that happen inside a Shopify app that can be | ||
used inside Shopify Flow to start workflows. There's no special configuration | ||
that you need to undertake to start using Flow triggers with a Disco App - | ||
assuming that you've [defined a trigger][] in your application's configuration | ||
from the Shopify Partner dashboard, you can fire that trigger with the | ||
following code: | ||
|
||
```ruby | ||
DiscoApp::Flow::CreateTrigger.call( | ||
shop: @shop, | ||
title: 'Customer became a VIP', | ||
resource_name: 'Customer Jane Doe', | ||
resource_url: 'https://store.myshopify.com/admin/customers/734299256292', | ||
properties: { | ||
'Customer email' => '[email protected]' | ||
} | ||
) | ||
``` | ||
|
||
Upon execution, a new `DiscoApp::Flow::Trigger` model will be persisted and a | ||
background job enqueued to send the trigger information to the relevant Shopify | ||
store's GraphQL API endpoint. | ||
|
||
The arguments passed to the `CreateTrigger` method are: | ||
|
||
- `shop`: The relevant `DiscoApp::Shop` instance the trigger relates to; | ||
- `title`: The title of the trigger. This must exactly match the title of the | ||
trigger as defined from the Shopify Partner dashboard; | ||
- `resource_name`: A short description of the object the trigger relates to. | ||
This is used by the Shopify Flow app to display workflow event history to | ||
store owners; | ||
- `resource_url`: A URL that can be followed by a store owner to view more | ||
information about the object the trigger relates to; | ||
- `properties`: A payload hash containing data about the trigger event that can | ||
be used by merchants within their workflows. The presence and data types of | ||
the values in this hash must exactly match those configured for the relevant | ||
trigger in the Shopify Partner dashboard. | ||
|
||
[defined a trigger]: https://help.shopify.com/en/api/embedded-apps/app-extensions/flow/create-triggers | ||
|
||
#### Actions | ||
Shopify Flow Actions are the operations a Shopify application can perform as | ||
part of a workflow. Like Triggers, [Actions must be defined][] within the | ||
Shopify Partner Dashboard configuration page for the application. The Disco App | ||
gem provides an `DiscoApp::Flow::ActionsController`, which serves a similar | ||
function to the `DiscoApp::WebhooksController` - it receives and verifies | ||
incoming requests from Shopify before handing them off for processing. | ||
|
||
Unlike webhook processing, incoming actions are persisted to the database in | ||
the form of a `DiscoApp::Flow::Action` model before being processed. When | ||
attempting to process an action, Disco App will attempt to find, instantiate | ||
and call a service object with the same name as the `action_id` of the | ||
relevant action. The `action_id` is determined by the URL used by Shopify to | ||
send the action payload. | ||
|
||
To take an example, an action may be configured in the Shopify Dashboard with | ||
the following attributes: | ||
|
||
- Action title: `Email customer`; | ||
- Action description: `Send an email to a customer`; | ||
- HTTPS request URL: `https://example.discolabs.com/flow/action/email_customer`. | ||
|
||
When Shopify sends a request for this action, the `action_id` of the persisted | ||
action model will be `email_customer` (derived from the request URL). When | ||
trying to process this action, Disco App will attempt to look for either an | ||
`EmailCustomer` or `Flow::Actions::EmailCustomer` service object class within | ||
the current application. If found, the `call` method will be called on that | ||
object with the relevant `DiscoApp::Shop` instance and the provided action | ||
properties hash being passed as keyword arguments - essentially, something like | ||
this: | ||
|
||
```ruby | ||
Flow::Actions::EmailCustomer.call(shop: action.shop, properties: action.properties) | ||
``` | ||
|
||
In this way Disco App expects applications using Shopify Flow actions to define | ||
service objects to process those actions using a typical Disco interactor | ||
pattern. | ||
|
||
[Actions must be defined]: https://help.shopify.com/en/api/embedded-apps/app-extensions/flow/create-actions | ||
|
||
#### Configuration | ||
Strictly speaking, the only two things that need to be done inside application | ||
code to support Shopify Flow Actions and Triggers are: | ||
|
||
1. Call `DiscoApp::Flow::CreateTrigger` anywhere in your code where a trigger | ||
should be fired; | ||
2. Create a `Flow::Actions::ActionName` service object class for each action | ||
you'd like your application to be able to process. | ||
|
||
Assuming you've configured your application's Flow integration correctly from | ||
the Shopify Partner dashboard, the sending of triggers and receiving of actions | ||
should then "just work". | ||
|
||
However, to help maintain an overview of the actions and triggers supported by | ||
your application with its codebase, it's recommended to maintain two additional | ||
initializers in your application's configuration that describe them. These | ||
files should then be treated as the source of truth for your application's | ||
actions and triggers, and should be referenced when setting up or updating your | ||
application's Flow configuration from the Partner Dashboard. | ||
|
||
Examples of each initializer follow. | ||
|
||
```ruby | ||
# config/initializers/disco_app_flow_actions.rb | ||
DiscoApp.configure do |config| | ||
config.flow_actions = { | ||
email_customer: { | ||
title: 'Email customer', | ||
description: 'Send an email to a customer', | ||
properties: [ | ||
{ | ||
name: 'customer_email', | ||
label: 'Customer email', | ||
help_text: 'The email address of the customer.', | ||
type: :email, | ||
required: true | ||
} | ||
] | ||
} | ||
} | ||
end | ||
``` | ||
|
||
```ruby | ||
# config/initializers/disco_app_flow_triggers.rb | ||
DiscoApp.configure do |config| | ||
config.flow_triggers = { | ||
customer_became_a_vip: { | ||
title: 'Customer became a VIP', | ||
description: 'A customer successfully qualified for VIP status.', | ||
properties: [ | ||
{ | ||
name: 'Customer email', | ||
description: 'The email address of the customer.', | ||
type: :email | ||
} | ||
] | ||
} | ||
} | ||
end | ||
``` | ||
|
||
In future versions of Disco App, the creation of triggers and the processing of | ||
actions may be validated against the schema defined in these initializers. | ||
|
||
### Asset Rendering | ||
It's a pretty common pattern for apps to want to render and update Shopify | ||
assets (Javascript, stylesheets, Liquid snippets etc) whenever a store owner | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
0.16.0 | ||
0.16.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
require 'rest-client' | ||
|
||
## | ||
# This file defines a very simple GraphQL API client to support a single type | ||
# of GraphQL API call for a Shopify store - sending a Shopify Flow trigger. | ||
# | ||
# We use this simple approach rather than using an existing GraphQL client | ||
# library such as https://github.com/github/graphql-client (either standalone | ||
# or as bundled with the Shopify API gem) for a couple of reasons: | ||
# | ||
# - These libraries tend to presume that a single client instance is | ||
# instantiated once and then reused across the application, which isn't the | ||
# case when we're making API calls once per trigger for each background | ||
# job. | ||
# - These libraries make an API call to fetch the Shopify GraphQL schema on | ||
# initialisation. The schema is very large, so the API call takes a number | ||
# of seconds to complete and when parsed consumes a large amount of memory. | ||
# - These libraries do not natively work well with the idea of a dynamic API | ||
# endpoint (ie, changing the request URL frequently), which is required | ||
# when making many requests to different Shopify stores. | ||
# | ||
module DiscoApp | ||
class GraphqlClient | ||
|
||
def initialize(shop) | ||
@shop = shop | ||
end | ||
|
||
## | ||
# Fire a Shopify Flow Trigger. | ||
# Returns a tuple {Boolean, Array} representing {success, errors}. | ||
def create_flow_trigger(title, resource_name, resource_url, properties) | ||
body = { | ||
trigger_title: title, | ||
resources: [ | ||
{ | ||
name: resource_name, | ||
url: resource_url | ||
} | ||
], | ||
properties: properties | ||
} | ||
|
||
# The double .to_json.to_json below looks odd but is required to properly escape the JSON hash | ||
# when inserting it into the GraphQL mutation call. | ||
response = execute(%Q( | ||
mutation { | ||
flowTriggerReceive(body: #{body.to_json.to_json}) { | ||
userErrors { | ||
field, | ||
message | ||
} | ||
} | ||
} | ||
)) | ||
|
||
errors = response.dig(:data, :flowTriggerReceive, :userErrors) | ||
[errors.empty?, errors] | ||
end | ||
|
||
private | ||
|
||
def execute(query) | ||
response = RestClient::Request.execute( | ||
method: :post, | ||
headers: headers, | ||
url: url, | ||
payload: { query: query }.to_json | ||
) | ||
JSON.parse(response.body).with_indifferent_access | ||
end | ||
|
||
def headers | ||
{ | ||
'Content-Type' => 'application/json', | ||
'X-Shopify-Access-Token' => @shop.shopify_token | ||
} | ||
end | ||
|
||
def url | ||
"https://#{@shop.shopify_domain}/admin/api/graphql.json" | ||
end | ||
|
||
end | ||
end |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
module DiscoApp | ||
module Flow | ||
class ActionsController < ActionController::Base | ||
include DiscoApp::Flow::Concerns::ActionsController | ||
end | ||
end | ||
end |
47 changes: 47 additions & 0 deletions
47
app/controllers/disco_app/flow/concerns/actions_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
module DiscoApp | ||
module Flow | ||
module Concerns | ||
module ActionsController | ||
|
||
extend ActiveSupport::Concern | ||
|
||
included do | ||
before_action :verify_flow_action | ||
before_action :find_shop | ||
protect_from_forgery with: :null_session | ||
end | ||
|
||
def create_flow_action | ||
DiscoApp::Flow::CreateAction.call( | ||
shop: @shop, | ||
action_id: params[:id], | ||
action_run_id: params[:action_run_id], | ||
properties: params[:properties] | ||
) | ||
|
||
head :ok | ||
end | ||
|
||
private | ||
|
||
def verify_flow_action | ||
unless flow_action_is_valid? | ||
head :unauthorized | ||
end | ||
request.body.rewind | ||
end | ||
|
||
# Shopify Flow action endpoints use the same verification method as webhooks, which is why we reuse this | ||
# service method here. | ||
def flow_action_is_valid? | ||
DiscoApp::WebhookService.is_valid_hmac?(request.body.read.to_s, ShopifyApp.configuration.secret, request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']) | ||
end | ||
|
||
def find_shop | ||
@shop = DiscoApp::Shop.find_by_shopify_domain!(params[:shopify_domain]) | ||
end | ||
|
||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.