O Shopify é uma das plataformas de e-commerce mais populares do mundo, e desenvolver plugins (apps) para ela pode ser muito lucrativo. Neste artigo, vou mostrar como criar um plugin Shopify completo usando Ruby on Rails.
🛍️ O que são Shopify Apps? Os Shopify Apps são aplicações que estendem as funcionalidades da plataforma. Podem ser:
Públicos : Disponíveis na Shopify App StorePrivados : Para uso interno de uma loja específicaPersonalizados : Desenvolvidos sob demanda🚀 Configuração Inicial 1. Criando o Projeto Rails 1
2
3
4
5
# Criar novo projeto Rails
rails new shopify_app --database = postgresql
cd shopify_app
# Adicionar gems necessárias
2. Gemfile Essencial 1
2
3
4
5
6
7
8
9
10
11
12
# Gemfile
gem 'shopify_app' , '~> 21.0'
gem 'shopify_api' , '~> 12.0'
gem 'dotenv-rails'
gem 'sidekiq'
gem 'redis'
gem 'pg'
group :development , :test do
gem 'byebug'
gem 'rspec-rails'
end
3. Configuração do ShopifyApp 1
2
3
4
5
# Instalar gems
bundle install
# Gerar configuração do Shopify
rails generate shopify_app
⚙️ Configuração Básica 1. Variáveis de Ambiente 1
2
3
4
# .env
SHOPIFY_API_KEY = sua_api_key
SHOPIFY_API_SECRET = sua_api_secret
SHOPIFY_APP_URL = https://seu-app.ngrok.io
2. Inicializer do Shopify 1
2
3
4
5
6
7
8
9
10
11
# config/initializers/shopify_app.rb
ShopifyApp . configure do | config |
config . application_name = "Meu Plugin Shopify"
config . api_key = ENV [ 'SHOPIFY_API_KEY' ]
config . secret = ENV [ 'SHOPIFY_API_SECRET' ]
config . scope = "read_products,write_products,read_orders"
config . embedded_app = true
config . after_authenticate_job = false
config . api_version = "2023-10"
config . shop_session_repository = 'Shop'
end
📊 Modelo de Dados 1. Modelo Shop 1
2
3
4
5
6
7
8
9
10
11
# app/models/shop.rb
class Shop < ApplicationRecord
include ShopifyApp :: ShopSessionStorage
validates :shopify_domain , presence: true , uniqueness: true
validates :shopify_token , presence: true
def api_version
ShopifyApp . configuration . api_version
end
end
2. Migration 1
2
3
4
5
6
7
8
9
10
11
12
13
# db/migrate/create_shops.rb
class CreateShops < ActiveRecord :: Migration [ 7.0 ]
def change
create_table :shops do | t |
t . string :shopify_domain , null: false
t . string :shopify_token , null: false
t . datetime :token_expires_at
t . timestamps
end
add_index :shops , :shopify_domain , unique: true
end
end
🎛️ Controllers Principais 1. Controller de Produtos 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# app/controllers/products_controller.rb
class ProductsController < AuthenticatedController
def index
@products = ShopifyAPI :: Product . find ( :all , params: { limit: 50 })
end
def show
@product = ShopifyAPI :: Product . find ( params [ :id ])
end
def create
@product = ShopifyAPI :: Product . new ( product_params )
if @product . save
redirect_to products_path , notice: 'Produto criado com sucesso!'
else
render :new , alert: 'Erro ao criar produto'
end
end
def update
@product = ShopifyAPI :: Product . find ( params [ :id ])
if @product . update_attributes ( product_params )
redirect_to @product , notice: 'Produto atualizado!'
else
render :edit , alert: 'Erro ao atualizar produto'
end
end
private
def product_params
params . require ( :product ). permit ( :title , :body_html , :vendor , :product_type )
end
end
2. Controller Base Autenticado 1
2
3
4
5
6
7
8
9
10
# app/controllers/authenticated_controller.rb
class AuthenticatedController < ApplicationController
include ShopifyApp :: Authenticated
private
def current_shopify_session
@current_shopify_session ||= ShopifyApp :: SessionRepository . retrieve ( session [ :shopify_session_id ])
end
end
1. Configuração de Webhooks 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# config/initializers/shopify_app.rb
ShopifyApp . configure do | config |
# ... outras configurações
config . webhooks = [
{
topic: 'orders/create' ,
address: 'https://seu-app.com/webhooks/orders/create' ,
format: 'json'
},
{
topic: 'products/update' ,
address: 'https://seu-app.com/webhooks/products/update' ,
format: 'json'
}
]
end
2. Controller de Webhooks 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
include ShopifyApp :: WebhookVerification
def orders_create
order_data = JSON . parse ( request . body . read )
ProcessOrderJob . perform_later ( order_data )
head :ok
end
def products_update
product_data = JSON . parse ( request . body . read )
UpdateProductCacheJob . perform_later ( product_data )
head :ok
end
end
3. Job para Processar Webhooks 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
def perform ( order_data )
# Processar dados do pedido
order_id = order_data [ 'id' ]
customer_email = order_data [ 'customer' ][ 'email' ]
total_price = order_data [ 'total_price' ]
# Sua lógica personalizada aqui
Rails . logger . info "Novo pedido recebido: #{ order_id } - #{ total_price } "
# Exemplo: enviar email, atualizar estoque, etc.
OrderNotificationMailer . new_order ( order_data ). deliver_now
end
end
🎨 Interface do Usuário 1. Layout Principal 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title> Meu Plugin Shopify</title>
<meta name= "viewport" content= "width=device-width,initial-scale=1" >
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application' , 'data-turbo-track' : 'reload' %>
<%= javascript_importmap_tags %>
<!-- Polaris CSS -->
<link rel= "stylesheet" href= "https://unpkg.com/@shopify/polaris@latest/build/esm/styles.css" >
</head>
<body>
<div id= "app" >
<%= yield %>
</div>
<!-- Shopify App Bridge -->
<script src= "https://unpkg.com/@shopify/app-bridge@3" ></script>
<script>
const AppBridge = window [ ' app-bridge ' ];
const app = AppBridge . createApp ({
apiKey : ' <%= ShopifyApp . configuration . api_key %> ' ,
host : ' <%= @shop_session . url %> ' ,
});
</script>
</body>
</html>
2. View de Produtos 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- app/views/products/index.html.erb -->
<div class= "Polaris-Page" >
<div class= "Polaris-Page-Header" >
<div class= "Polaris-Page-Header__TitleWrapper" >
<h1 class= "Polaris-DisplayText--sizeLarge" > Produtos</h1>
</div>
<div class= "Polaris-Page-Header__Actions" >
<%= link_to "Novo Produto" , new_product_path ,
class: "Polaris-Button Polaris-Button--primary" %>
</div>
</div>
<div class= "Polaris-Card" >
<div class= "Polaris-ResourceList" >
<% @products . each do | product | %>
<div class= "Polaris-ResourceList__ItemWrapper" >
<div class= "Polaris-ResourceItem" >
<div class= "Polaris-ResourceItem__Content" >
<h3> <%= link_to product . title , product_path ( product ) %> </h3>
<p> Vendor: <%= product . vendor %> </p>
<p> Tipo: <%= product . product_type %> </p>
</div>
<div class= "Polaris-ResourceItem__Actions" >
<%= link_to "Editar" , edit_product_path ( product ),
class: "Polaris-Button" %>
</div>
</div>
</div>
<% end %>
</div>
</div>
</div>
🔧 Funcionalidades Avançadas 1. GraphQL API 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# app/services/shopify_graphql_service.rb
class ShopifyGraphqlService
def initialize ( shop_session )
@session = shop_session
end
def fetch_products_with_variants
query = <<~ GRAPHQL
query getProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
variants(first: 10) {
edges {
node {
id
title
price
inventoryQuantity
}
}
}
}
}
}
}
GRAPHQL
response = ShopifyAPI :: GraphQL . client . query ( query , variables: { first: 50 })
response . data . products . edges . map ( & :node )
end
end
2. Cache Inteligente 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# app/services/product_cache_service.rb
class ProductCacheService
CACHE_TTL = 1 . hour
def self . fetch_products ( shop_domain )
Rails . cache . fetch ( "shop: #{ shop_domain } :products" , expires_in: CACHE_TTL ) do
shop = Shop . find_by ( shopify_domain: shop_domain )
session = ShopifyApp :: SessionRepository . retrieve ( shop . id )
ShopifyAPI :: Base . activate_session ( session )
ShopifyAPI :: Product . find ( :all , params: { limit: 250 })
end
end
def self . invalidate_cache ( shop_domain )
Rails . cache . delete ( "shop: #{ shop_domain } :products" )
end
end
🚀 Deploy em Produção 1. Heroku Deploy 1
2
3
# Procfile
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# .github/workflows/deploy.yml
name : Deploy to Heroku
on :
push :
branches : [ main ]
jobs :
deploy :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v2
- uses : akhileshns/heroku-deploy@v3.12.12
with :
heroku_api_key : $
heroku_app_name : " seu-app-shopify"
heroku_email : " seu-email@exemplo.com"
2. Configurações de Produção 1
2
3
4
5
6
7
8
9
10
11
12
# config/environments/production.rb
Rails . application . configure do
# SSL obrigatório para Shopify
config . force_ssl = true
# Session store seguro
config . session_store :cookie_store ,
key: '_shopify_app_session' ,
secure: true ,
httponly: true ,
same_site: :none
end
📈 Monetização e Store 1. Cobrança por Assinatura 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# app/models/billing_service.rb
class BillingService
def self . create_recurring_charge ( shop , plan_name , price )
charge = ShopifyAPI :: RecurringApplicationCharge . new (
name: plan_name ,
price: price ,
trial_days: 7 ,
test: Rails . env . development?
)
if charge . save
charge . confirmation_url
else
nil
end
end
end
2. Verificação de Pagamento 1
2
3
4
5
6
7
8
9
10
11
12
13
14
# app/controllers/billing_controller.rb
class BillingController < AuthenticatedController
def confirm
charge = ShopifyAPI :: RecurringApplicationCharge . find ( params [ :charge_id ])
if charge . status == 'accepted'
charge . activate
current_shop . update ( billing_active: true )
redirect_to root_path , notice: 'Assinatura ativada!'
else
redirect_to pricing_path , alert: 'Pagamento cancelado'
end
end
end
🎯 Boas Práticas 1. Tratamento de Erros 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app/controllers/concerns/error_handler.rb
module ErrorHandler
extend ActiveSupport :: Concern
included do
rescue_from ShopifyAPI :: ValidationException , with: :handle_validation_error
rescue_from ActiveRecord :: RecordNotFound , with: :handle_not_found
end
private
def handle_validation_error ( exception )
render json: { error: exception . message }, status: :unprocessable_entity
end
def handle_not_found
render json: { error: 'Recurso não encontrado' }, status: :not_found
end
end
2. Rate Limiting 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app/services/shopify_api_service.rb
class ShopifyApiService
MAX_RETRIES = 3
RETRY_DELAY = 2 . seconds
def self . with_retry ( & block )
retries = 0
begin
yield
rescue ShopifyAPI :: LimitExceededException => e
retries += 1
if retries <= MAX_RETRIES
sleep ( RETRY_DELAY * retries )
retry
else
raise e
end
end
end
end
🚀 Conclusão Desenvolver plugins Shopify com Rails é uma excelente oportunidade de negócio. Os pontos principais são:
Configure corretamente as credenciais e webhooksUse Polaris para interface consistenteImplemente billing para monetizaçãoTrate erros e rate limiting adequadamenteTeste thoroughly antes do deployPróximos Passos: Submeter para Shopify App Store Implementar analytics Adicionar testes automatizados Otimizar performance Com mais de 10 anos desenvolvendo com Rails, considero o Shopify uma das melhores plataformas para monetizar habilidades de desenvolvimento web.
Esta postagem está licenciada sob
CC BY 4.0 pelo autor.
{"title"=>"Compartilhar"}