Articles

Développer une application Shopify avec Ruby on Rails

Shopify logo

Dans cet article, nous allons voir au travers d'un exemple simple comment développer une application Shopify avec Ruby on Rails. Notre application récupère et affiche la liste des commandes passées sur une boutique. Vous remarquerez que l'architecture d'une application Shopify est très différente de cette d'un module Magento ou Prestashop par exemple. Le code source du module Magento est directement intégré au code Magento et le module a un accès complet à la base de données de la boutique. Une application Shopify est hébergée sur un serveur isolé de la boutique, n'a accès qu'à sa propre base de données et communique avec la boutique via l'API Shopify. Ainsi, la première étape est de configurer l'application Ruby on Rails afin qu'elle soit capable de communiquer avec l'API Shopify.

Créez un compte Shopify Parner

Afin d'être en mesure de faire votre demande de clé d'API Shopify, vous devez disposer d'un compte Shopify Partner. Rendez-vous à l'adresse http://shopify.com/partners pour créer votre compte. Créez ensuite une nouvelle application afin d'obtenir une clé d'API et le secret correspondant.

Configuration de l'application

Nous allons nous servir de la très pratique gem shopify_app qui nous fournit la classe SessionsController ainsi que du code permettant d'authentifier notre application à l'API Shopify via Oauth. Ajoutez la gem à votre Gemfile et installez votre projet.

ruby '2.3.3'
source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.6'
# Use posgresql as the database for Active Record
gem 'pg'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc

# Handles Shopify API authentication and API requests
gem 'shopify_app', '~> 7.0.0'

# Pagination library for Rails
gem 'will_paginate', '~> 3.1.0'

# I had this gem in a previous version but cant remember why..
# Remove if everything is working fine
# gem 'sinatra'

#  Simple, efficient background processing for Ruby
gem 'sidekiq'

Il faut maintenant configurer l'application Rails afin soit capable de communiquer correctement avec l'API Shopify. Je vous recommande d'utiliser le générateur fourni par la gem shopify_app qui va créer pour vous différents fichiers nécessaires au bon fonctionnement de votre application.

rails generate shopify_app --api_key <your_api_key> --secret <your_app_secret>

Parmi les fichiers crées par le générateur, nous avons :

Un fichier d'initialisation

Le fichier config/initializer/shopify_app.rb contient la configuration permettant à votre application de s'authentifier à l'API Shopify :

ShopifyApp.configure do |config|
  config.api_key = ENV['SHOPIFY_EXPEDITOR_INET_API_KEY']
  config.secret = ENV['SHOPIFY_EXPEDITOR_INET_API_SECRET']
  config.scope = "read_orders"
  config.embedded_app = true
  config.webhooks = [
    {topic: 'app/uninstalled', address: "#{ENV['WEBHOOK_URL']}app_uninstalled", format: 'json'},
    {topic: 'orders/delete', address: "#{ENV['WEBHOOK_URL']}orders_delete", format: 'json'},
    {topic: 'orders/create', address: "#{ENV['WEBHOOK_URL']}orders_create", format: 'json'},
    {topic: 'orders/updated', address: "#{ENV['WEBHOOK_URL']}orders_updated", format: 'json'}
  ]
end

Dans cette exemple, l'application requiert un accès en lecture aux commandes des boutiques Shopify sur lesquelles l'application est installée. Nous indiquons que l'application sera intégrée à l'administration Shopify, c'est à dire qu'elle sera servie dans une iframe à l'intérieur d'une page d'administration Shopify, donnant ainsi l'illusion d'être partie intégrante de Shopify. Finalement, l'application demande à être avertie lors de certains événements concernant les commandes ou lorsque l'application est désinstallée d'une boutique.

Un fichier de configuration des routes

Le fichier config/routes.rb a été mis à jour afin d'inclure les routes de l'engine ShopifyApp :

Rails.application.routes.draw do
  mount ShopifyApp::Engine, at: '/'

  root :to => 'orders#index'
end

En plus des routes fournies par l'engine utilisées lors du processus d'authentification de votre application avec l'API Shopify, nous ajoutons la route vers l'action qui liste les commandes existantes dans la boutique.

Le modèle Shop

Le générateur crée le modèle Shop ainsi que la migration associée. C'est dans la table shops que seront enregistrées les boutiques sur lesquelles l'application a été installée. Pour chaque boutique, la table enregistre aussi le token permettant de s'authentifier à la boutique.

class CreateShops < ActiveRecord::Migration
  def self.up
    create_table :shops  do |t|
      t.string :shopify_domain, null: false
      t.string :shopify_token, null: false
      t.timestamps
    end

    add_index :shops, :shopify_domain, unique: true
  end

  def self.down
    drop_table :shops
  end
end
class Shop < ActiveRecord::Base
  include ShopifyApp::Shop
  include ShopifyApp::SessionStorage
end

Pensez à jouer vos migrations après avoir généré le modèle et le fichier de migration.

Maintenant que vous avons transformé notre application Rails en application Shopify à l'aide de la gem shopify_app et de son générateur, voyons comment nous pouvons récupérer la liste des commandes et les afficher dans notre application.

Création du contrôleur

Le contrôleur OrdersController est en charge de récupérer la liste des commandes. Il hérite du contrôleur ShopifyApp::AuthenticatedController fourni par la gem shopify_app.

La méthode get_current_shop récupère la boutique sur laquelle l'application est en train d'être utilisée. La méthode synchronize que nous allons voir plus loin récupère la liste des commandes via l'API Shopify et les enregistre dans la base de donnée locale à l'application Rails.

class OrdersController < ShopifyApp::AuthenticatedController

  def index
    shop = get_current_shop
    shop.synchronize unless shop.orders_synchronized?
    @orders = shop.orders
    count = @orders.count
    page_limit = 25
    @total_pages = (count.to_f / page_limit.to_f).ceil
    @page = (params[:page].presence || 1).to_i
    @previous_page = "/?page=#{ @page - 1 }&limit=#{page_limit}" if @page > 1
    @next_page = "/?page=#{ @page + 1 }&limit=#{page_limit}" if @page < @total_pages
    @orders = @orders.paginate(page: @page, per_page: page_limit).order('shopify_order_id DESC')
  end

  private

    def get_current_shop
      Shop.find_by shopify_domain: ShopifyAPI::Shop.current.myshopify_domain
    end

end

Création des modèles

En plus du modèle Shop nécessaire à toute application Shopify, nous devons créer le modèle Order qui représente une commande :

class CreateOrders < ActiveRecord::Migration
  def change
    create_table :orders do |t|
      t.string :shopify_order_id, null: false
      t.string :shopify_order_name, default: ''
      t.datetime :shopify_order_created_at
      t.belongs_to :shop, index: true
      t.timestamps
    end

    add_index :orders, :shopify_order_id, unique: true
  end
end
class Order < ActiveRecord::Base
  belongs_to :shop
end

Finalement, voyons la méthode synchronize du modèle Shop qui récupère les commandes via l'API Shopify afin de les enregistrer en base de données :

class Shop < ActiveRecord::Base
  include ShopifyApp::Shop
  include ShopifyApp::SessionStorage

  has_many :orders, dependent: :destroy

  def synchronize
    self.orders.delete_all
    orders = ShopifyAPI::Order.find(:all, params: {status: :any})
    orders.each do |order|
      order = Order.create(
          {
              shopify_order_id: order.id,
              shopify_order_name: order.name,
              shopify_order_created_at: order.created_at,
          })
      self.orders << order
    end
    self.orders_synchronized = true
    self.save
  end

  def orders_synchronized?
    return self.orders_synchronized
  end
end

Création des vues

Nous allons maintenant créer les vues permettant d'afficher la liste des commandes.

Voyons tout d'abord un exemple de layout tirant parti de l'Embedded App SDK permettant d'intégrer votre application Rails directement à l'intérieur de l'interface d'administration Shopify :

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Shopify Embedded Example App</title>
    <%= stylesheet_link_tag 'application' %>
    <%= csrf_meta_tags %>
  </head>

  <body>
    <div class="app-wrapper">
      <div class="app-content">
        <main role="main">
          <%= yield %>
        </main>
      </div>
    </div>

    <%= render 'layouts/flash_messages' %>

    <script src="https://cdn.shopify.com/s/assets/external/app.js?<%= Time.now.strftime('%Y%m%d%H') %>"></script>

    <script type="text/javascript">
      ShopifyApp.init({
        apiKey: "<%= ShopifyApp.configuration.api_key %>",
        shopOrigin: "<%= "https://#{ @shop_session.url }" if @shop_session %>",
        debug: <%= Rails.env.development? ? 'true' : 'false' %>,
        forceRedirect: true
      });
    </script>

    <%= javascript_include_tag 'application', "data-turbolinks-track" => true %>

    <% if content_for?(:javascript) %>
      <div id="ContentForJavascript" data-turbolinks-temporary>
        <%= yield :javascript %>
      </div>
    <% end %>
  </body>
</html>

L'Embedded App SDK permet entre autres choses d'ajouter des boutons ou d'afficher des alertes et des modals directement dans l'interface de Shopify. C'est pourquoi le SDK a besoin de s'authentifier à l'aide de la clé d'API Shopify.

Voyons maintenant la vue listant les commandes récupérées par le contrôleur :

<% content_for :javascript do %>
  <script type="text/javascript">
    ShopifyApp.ready(function(){
      ShopifyApp.Bar.initialize({
        icon: "<%= asset_path('favicon.ico') %>",
        pagination: {
          previous: <%= (@previous_page.present? ? {href: @previous_page} : nil).to_json.html_safe %>,
          next: <%= (@next_page.present? ? {href: @next_page} : nil).to_json.html_safe %>
        }
      });
    });
  </script>
<% end %>

<div class="section">
  <div class="section-content">
    <div class="section-row">
      <div class="section-listing">
        <div class="section-options">
          <ul class="section-tabs">
            <li class="active"><a href="#top">All Orders</a></li>
          </ul>
          <div class="section-content">
            <div class="section-row">
              <% if @orders.any? %>
                <table class="table-section">
                  <thead>
                  <tr>
                    <th class="select-col">
                      <div class="btn default btn-select-all ico-down">
                        <input id="select-all" class="checkbox" type="checkbox" value="" name="select-all">
                        <span class="checkbox-styled"></span>
                      </div>
                    </th>
                    <th class="sortable">Order</th>
                    <th class="sortable">Date</th>
                  </tr>
                  </thead>
                  <tbody>
                    <% @orders.each do |order| %>
                        <tr>
                          <td>
                            <input class="checkbox select-order-checkbox" type="checkbox" value="<%= order.id %>">
                            <span class="checkbox-styled"></span>
                          </td>
                          <td><%= link_to order.shopify_order_name, "https://#{@shop_session.url}/admin/orders/#{order.shopify_order_id}", target: "_top" %></td>
                          <td><%= format_date order.shopify_order_created_at %></td>
                        </tr>
                    <% end %>
                  </tbody>
                </table>
              <% else %>
                  <div>There is no order.</div>
              <% end %>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Remarquez que la vue ajoute des boutons de pagination via le Embedded App SDK.

Création d'un job

Voyons un exemple de job appelé par l'API Shopify lors d'un événement sur une commande comme demandé via l'instruction webhooks dans le fichier d'initialisation shopify_app.rb. Le système de webhooks permet de garder les commandes de notre application Rails synchronisées avec les commandes de la boutique Shopify. Par exemple, notre application demande à être informée lors de la création d'une nouvelle commande afin qu'elle puisse elle-même créer cette commande dans sa base de données. Etudions donc la classe OrdersCreateJob :

class OrdersCreateJob < ActiveJob::Base
  def perform(shop_domain:, webhook:)
    shop = Shop.find_by(shopify_domain: shop_domain)

    shop.with_shopify_session do

      order =
          {
              shopify_order_id: webhook[:id],
              shopify_order_name: webhook[:order_number],
              shopify_order_created_at: webhook[:created_at]
          }
      order = Order.where(shopify_order_id: order[:shopify_order_id]).first_or_create(order)
      shop.orders << order
    end
  end
end

Dernière recommandation

Je vous recommande vivement de styler votre application Shopify à l'aide de la librairie frontend Shopify Embedded App Frontend Framework. Cette librairie fournit le code CSS et Javascript vous permettant de reproduire l'interface utilisateur Shopify, vous faciliterez ainsi grandement la vie de vos utilisateurs qui sont déjà habitués à utiliser cette interface.

Conclusion

L'exemple présenté dans cet article est bien entendu très limité et n'apporte aucune valeur ajoutée par rapport à ce que propose déjà l'interface d'administration Shopify. Cependant, vous disposez maintenant de tous les outils vous permettant de créer une application Shopify aussi évoluée que vous le désirez avec Ruby on Rails.