sinatra-embedded-shopify-app 0.5.16
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/.circleci/config.yml +15 -0
- data/.github/dependabot.yml +14 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG +96 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +118 -0
- data/LICENSE +20 -0
- data/README.md +172 -0
- data/bin/ci.sh +10 -0
- data/bin/sinatra-embedded-shopify-app-generator +85 -0
- data/example/.gitignore +3 -0
- data/example/.ruby-version +1 -0
- data/example/Gemfile +28 -0
- data/example/Gemfile.lock +126 -0
- data/example/Procfile +1 -0
- data/example/README.md +4 -0
- data/example/Rakefile +19 -0
- data/example/config/database.yml +12 -0
- data/example/config.ru +3 -0
- data/example/db/migrate/20140413221328_create_shops.rb +14 -0
- data/example/db/migrate/20140414042317_add_index_to_shops.rb +11 -0
- data/example/db/schema.rb +20 -0
- data/example/db/seeds.rb +3 -0
- data/example/public/icon.png +0 -0
- data/example/public/legend.gif +0 -0
- data/example/src/app.rb +57 -0
- data/example/test/app_test.rb +81 -0
- data/example/test/test_helper.rb +31 -0
- data/example/views/_flash_messages.erb +25 -0
- data/example/views/_top_bar.erb +10 -0
- data/example/views/home.erb +11 -0
- data/example/views/layouts/application.erb +26 -0
- data/example/views/login.erb +42 -0
- data/lib/sinatra/sinatra-embedded-shopify-app.rb +275 -0
- data/sinatra-embedded-shopify-app.gemspec +32 -0
- metadata +272 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
sinatra-embedded-shopify-app (0.5.16)
|
5
|
+
activesupport
|
6
|
+
omniauth (~> 2.0, >= 2.0.4)
|
7
|
+
omniauth-shopify-oauth2 (~> 2.3, >= 2.3.2)
|
8
|
+
shopify_api (= 9.5.1)
|
9
|
+
sinatra (>= 2.2, < 3.1)
|
10
|
+
sinatra-activerecord (~> 2.0.9)
|
11
|
+
|
12
|
+
GEM
|
13
|
+
remote: https://rubygems.org/
|
14
|
+
specs:
|
15
|
+
activemodel (7.0.4.3)
|
16
|
+
activesupport (= 7.0.4.3)
|
17
|
+
activemodel-serializers-xml (1.0.2)
|
18
|
+
activemodel (> 5.x)
|
19
|
+
activesupport (> 5.x)
|
20
|
+
builder (~> 3.1)
|
21
|
+
activerecord (7.0.4.3)
|
22
|
+
activemodel (= 7.0.4.3)
|
23
|
+
activesupport (= 7.0.4.3)
|
24
|
+
activeresource (6.0.0)
|
25
|
+
activemodel (>= 6.0)
|
26
|
+
activemodel-serializers-xml (~> 1.0)
|
27
|
+
activesupport (>= 6.0)
|
28
|
+
activesupport (7.0.4.3)
|
29
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
30
|
+
i18n (>= 1.6, < 2)
|
31
|
+
minitest (>= 5.1)
|
32
|
+
tzinfo (~> 2.0)
|
33
|
+
builder (3.2.4)
|
34
|
+
byebug (11.1.3)
|
35
|
+
concurrent-ruby (1.2.2)
|
36
|
+
fakeweb (1.3.0)
|
37
|
+
faraday (2.7.4)
|
38
|
+
faraday-net_http (>= 2.0, < 3.1)
|
39
|
+
ruby2_keywords (>= 0.0.4)
|
40
|
+
faraday-net_http (3.0.2)
|
41
|
+
graphql (2.0.21)
|
42
|
+
graphql-client (0.18.0)
|
43
|
+
activesupport (>= 3.0)
|
44
|
+
graphql
|
45
|
+
hashie (5.0.0)
|
46
|
+
i18n (1.12.0)
|
47
|
+
concurrent-ruby (~> 1.0)
|
48
|
+
jwt (2.7.0)
|
49
|
+
mini_portile2 (2.8.2)
|
50
|
+
minitest (5.18.0)
|
51
|
+
mocha (1.15.0)
|
52
|
+
multi_xml (0.6.0)
|
53
|
+
mustermann (2.0.2)
|
54
|
+
ruby2_keywords (~> 0.0.1)
|
55
|
+
oauth2 (2.0.9)
|
56
|
+
faraday (>= 0.17.3, < 3.0)
|
57
|
+
jwt (>= 1.0, < 3.0)
|
58
|
+
multi_xml (~> 0.5)
|
59
|
+
rack (>= 1.2, < 4)
|
60
|
+
snaky_hash (~> 2.0)
|
61
|
+
version_gem (~> 1.1)
|
62
|
+
omniauth (2.1.1)
|
63
|
+
hashie (>= 3.4.6)
|
64
|
+
rack (>= 2.2.3)
|
65
|
+
rack-protection
|
66
|
+
omniauth-oauth2 (1.8.0)
|
67
|
+
oauth2 (>= 1.4, < 3)
|
68
|
+
omniauth (~> 2.0)
|
69
|
+
omniauth-shopify-oauth2 (2.3.2)
|
70
|
+
activesupport
|
71
|
+
omniauth-oauth2 (~> 1.5)
|
72
|
+
pg (1.4.3)
|
73
|
+
rack (2.2.6.4)
|
74
|
+
rack-protection (2.2.4)
|
75
|
+
rack
|
76
|
+
rack-test (2.0.2)
|
77
|
+
rack (>= 1.3)
|
78
|
+
rake (13.0.6)
|
79
|
+
ruby2_keywords (0.0.5)
|
80
|
+
shopify_api (9.5.1)
|
81
|
+
activeresource (>= 4.1.0)
|
82
|
+
graphql-client
|
83
|
+
rack
|
84
|
+
sinatra (2.2.4)
|
85
|
+
mustermann (~> 2.0)
|
86
|
+
rack (~> 2.2)
|
87
|
+
rack-protection (= 2.2.4)
|
88
|
+
tilt (~> 2.0)
|
89
|
+
sinatra-activerecord (2.0.26)
|
90
|
+
activerecord (>= 4.1)
|
91
|
+
sinatra (>= 1.0)
|
92
|
+
sinatra-flash (0.3.0)
|
93
|
+
sinatra (>= 1.0.0)
|
94
|
+
snaky_hash (2.0.1)
|
95
|
+
hashie
|
96
|
+
version_gem (~> 1.1, >= 1.1.1)
|
97
|
+
sqlite3 (1.5.1)
|
98
|
+
mini_portile2 (~> 2.8.0)
|
99
|
+
sqlite3 (1.5.1-x86_64-linux)
|
100
|
+
tilt (2.0.11)
|
101
|
+
tzinfo (2.0.6)
|
102
|
+
concurrent-ruby (~> 1.0)
|
103
|
+
version_gem (1.1.2)
|
104
|
+
|
105
|
+
PLATFORMS
|
106
|
+
ruby
|
107
|
+
x86_64-linux
|
108
|
+
|
109
|
+
DEPENDENCIES
|
110
|
+
byebug
|
111
|
+
fakeweb
|
112
|
+
minitest
|
113
|
+
mocha
|
114
|
+
pg
|
115
|
+
rack-test
|
116
|
+
rake (>= 12.3.3)
|
117
|
+
sinatra-activerecord
|
118
|
+
sinatra-embedded-shopify-app!
|
119
|
+
sinatra-flash
|
120
|
+
sqlite3
|
121
|
+
|
122
|
+
RUBY VERSION
|
123
|
+
ruby 2.7.7p221
|
124
|
+
|
125
|
+
BUNDLED WITH
|
126
|
+
2.4.12
|
data/example/Procfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
web: bundle exec rackup config.ru -p $PORT
|
data/example/README.md
ADDED
data/example/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'sinatra/activerecord/rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require './src/app'
|
4
|
+
|
5
|
+
namespace :test do
|
6
|
+
task :prepare do
|
7
|
+
`RACK_ENV=test rake db:create`
|
8
|
+
`RACK_ENV=test rake db:migrate`
|
9
|
+
`RACK_ENV=test SECRET=secret rake db:seed`
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
task :test do
|
14
|
+
Rake::TestTask.new do |t|
|
15
|
+
t.pattern = 'test/*_test.rb'
|
16
|
+
t.libs << 'test'
|
17
|
+
t.verbose = true
|
18
|
+
end
|
19
|
+
end
|
data/example/config.ru
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
4
|
+
#
|
5
|
+
# This file is the source Rails uses to define your schema when running `bin/rails
|
6
|
+
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
9
|
+
# migrations use external dependencies or application code.
|
10
|
+
#
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
12
|
+
|
13
|
+
ActiveRecord::Schema[7.0].define(version: 2014_04_14_042317) do
|
14
|
+
create_table "shops", force: :cascade do |t|
|
15
|
+
t.string "shopify_domain"
|
16
|
+
t.string "shopify_token"
|
17
|
+
t.index ["shopify_domain"], name: "index_shops_on_shopify_domain"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
data/example/db/seeds.rb
ADDED
Binary file
|
Binary file
|
data/example/src/app.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra/sinatra-embedded-shopify-app'
|
4
|
+
require 'sinatra/flash'
|
5
|
+
|
6
|
+
# SinatraApp
|
7
|
+
class SinatraApp < Sinatra::Base
|
8
|
+
register Sinatra::Shopify
|
9
|
+
register Sinatra::Flash
|
10
|
+
|
11
|
+
# set the scope that your app needs, read more here:
|
12
|
+
# http://docs.shopify.com/api/tutorials/oauth
|
13
|
+
set :scope, 'read_products, read_orders'
|
14
|
+
|
15
|
+
# Your App's Home page
|
16
|
+
# this is a simple example that fetches some products
|
17
|
+
# from Shopify and displays them inside your app
|
18
|
+
get '/' do
|
19
|
+
shopify_session do |shop_name|
|
20
|
+
@shop = ShopifyAPI::Shop.current
|
21
|
+
@products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
|
22
|
+
erb :home
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# this endpoint recieves the uninstall webhook
|
27
|
+
# and cleans up data, add to this endpoint as your app
|
28
|
+
# stores more data.
|
29
|
+
shopify_webhook '/uninstall' do |shop_name, params|
|
30
|
+
Shop.find_by(shopify_domain: shop_name).destroy
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# This method gets called when your app is installed.
|
36
|
+
# setup any webhooks or services you need on Shopify
|
37
|
+
# inside here.
|
38
|
+
def after_shopify_auth
|
39
|
+
# shopify_session do
|
40
|
+
# create an uninstall webhook, this webhook gets sent
|
41
|
+
# when your app is uninstalled from a shop. It is good
|
42
|
+
# practice to clean up any data from a shop when they
|
43
|
+
# uninstall your app:
|
44
|
+
|
45
|
+
# uninstall_webhook = ShopifyAPI::Webhook.new(
|
46
|
+
# topic: 'app/uninstalled',
|
47
|
+
# address: "#{base_url}/uninstall",
|
48
|
+
# format: 'json'
|
49
|
+
# )
|
50
|
+
# begin
|
51
|
+
# uninstall_webhook.save!
|
52
|
+
# rescue => e
|
53
|
+
# raise unless uninstall_webhook.persisted?
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require './src/app'
|
3
|
+
|
4
|
+
class MockShop
|
5
|
+
def initialize(shop_name)
|
6
|
+
@shop_name = shop_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def myshopify_domain
|
10
|
+
@shop_name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class AppTest < Minitest::Test
|
15
|
+
def app
|
16
|
+
SinatraApp
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup
|
20
|
+
@shop_name = 'testshop.myshopify.com'
|
21
|
+
@shopify_shop = MockShop.new(@shop_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_root_with_session
|
25
|
+
set_session
|
26
|
+
api_url = "https://testshop.myshopify.com/admin/api/#{app.settings.api_version}"
|
27
|
+
fake "#{api_url}/shop.json", body: {myshopify_domain: @shop_name}.to_json
|
28
|
+
fake "#{api_url}/products.json?limit=10", body: '{}'
|
29
|
+
get '/'
|
30
|
+
assert last_response.ok?
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_root_with_session_activates_api
|
34
|
+
set_session
|
35
|
+
SinatraApp.any_instance.expects(:activate_shopify_api).with(@shop_name, 'token')
|
36
|
+
ShopifyAPI::Shop.expects(:current).returns(@shopify_shop)
|
37
|
+
ShopifyAPI::Product.expects(:find).returns([])
|
38
|
+
get '/'
|
39
|
+
assert last_response.ok?
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_root_without_session_redirects_to_install
|
43
|
+
get '/'
|
44
|
+
assert_equal 302, last_response.status
|
45
|
+
assert_equal 'http://example.org/login', last_response.location
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_root_with_shop_redirects_to_auth
|
49
|
+
get '/?shop=othertestshop.myshopify.com'
|
50
|
+
assert_equal 'http://example.org/login?shop=othertestshop.myshopify.com', last_response.location
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_root_with_session_and_new_shop_redirects_to_auth
|
54
|
+
set_session
|
55
|
+
get '/?shop=othertestshop.myshopify.com'
|
56
|
+
assert_equal 'http://example.org/login?shop=othertestshop.myshopify.com', last_response.location
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_root_rescues_UnauthorizedAccess_clears_session_and_redirects
|
60
|
+
set_session
|
61
|
+
SinatraApp.any_instance.expects(:activate_shopify_api).with(@shop_name, 'token')
|
62
|
+
SinatraApp.any_instance.expects(:clear_session)
|
63
|
+
ShopifyAPI::Shop.expects(:current).raises(ActiveResource::UnauthorizedAccess.new('UnauthorizedAccess'))
|
64
|
+
get '/'
|
65
|
+
assert_equal 302, last_response.status
|
66
|
+
assert_equal 'http://example.org/', last_response.location
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_uninstall_webhook_endpoint
|
70
|
+
SinatraApp.any_instance.expects(:verify_shopify_webhook).returns(true)
|
71
|
+
Shop.any_instance.expects(:destroy)
|
72
|
+
post '/uninstall', '{}', 'HTTP_X_SHOPIFY_SHOP_DOMAIN' => @shop_name
|
73
|
+
assert last_response.ok?
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def set_session(shop = 'testshop.myshopify.com', token = 'token')
|
79
|
+
SinatraApp.any_instance.stubs(:session).returns(shopify: { shop: shop, token: token })
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
$VERBOSE = nil
|
2
|
+
|
3
|
+
ENV['RACK_ENV'] = 'test'
|
4
|
+
ENV['SECRET'] = 'secret'
|
5
|
+
|
6
|
+
require 'minitest/autorun'
|
7
|
+
require 'rack/test'
|
8
|
+
require 'mocha/minitest'
|
9
|
+
require 'fakeweb'
|
10
|
+
|
11
|
+
FakeWeb.allow_net_connect = false
|
12
|
+
|
13
|
+
module Helpers
|
14
|
+
include Rack::Test::Methods
|
15
|
+
|
16
|
+
def load_fixture(name)
|
17
|
+
File.read("./test/fixtures/#{name}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def fake(url, options = {})
|
21
|
+
method = options.delete(:method) || :get
|
22
|
+
body = options.delete(:body) || '{}'
|
23
|
+
format = options.delete(:format) || :json
|
24
|
+
|
25
|
+
FakeWeb.register_uri(method, url, { body: body, status: 200, content_type: "application/#{format}" }.merge(options))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Minitest::Test
|
30
|
+
include Helpers
|
31
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<script type="text/javascript">
|
2
|
+
var AppBridge = window['app-bridge'];
|
3
|
+
|
4
|
+
var actions = AppBridge.actions;
|
5
|
+
var Toast = actions.Toast;
|
6
|
+
|
7
|
+
<% if flash[:notice] %>
|
8
|
+
var notice = Toast.create(app, {
|
9
|
+
message: "<%= flash[:notice] %>",
|
10
|
+
duration: 5000
|
11
|
+
});
|
12
|
+
|
13
|
+
notice.dispatch(Toast.Action.SHOW);
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
<% if flash[:error] %>
|
17
|
+
var notice = Toast.create(app, {
|
18
|
+
message: "<%= flash[:error] %>",
|
19
|
+
duration: 5000,
|
20
|
+
isError: true,
|
21
|
+
});
|
22
|
+
|
23
|
+
notice.dispatch(Toast.Action.SHOW);
|
24
|
+
<% end %>
|
25
|
+
</script>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<h3>Shopify Sinatra App</h3>
|
2
|
+
<p>Welcome!</p>
|
3
|
+
|
4
|
+
<h3>Products</h3>
|
5
|
+
<ul>
|
6
|
+
<% @products.each do |product| %>
|
7
|
+
<li><a href=<%="https://#{@shop.myshopify_domain}/admin/products/#{product.id}"%> target="_blank"> <%= product.id %> </a></li>
|
8
|
+
<% end %>
|
9
|
+
</ul>
|
10
|
+
|
11
|
+
<a href="/logout">Logout</a>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<script src="https://unpkg.com/@shopify/app-bridge@3"></script>
|
5
|
+
<script type="text/javascript">
|
6
|
+
var AppBridge = window['app-bridge'];
|
7
|
+
|
8
|
+
var host = '<%= shop_host %>';
|
9
|
+
var apiKey = '<%= SinatraApp.settings.api_key %>';
|
10
|
+
|
11
|
+
var app = AppBridge.createApp({
|
12
|
+
apiKey: apiKey,
|
13
|
+
host: host
|
14
|
+
});
|
15
|
+
</script>
|
16
|
+
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-7s5uDGW3AHqw6xtJmNNtr+OBRJUlgkNJEo78P4b0yRw= sha512-nNo+yCHEyn0smMxSswnf/OnX6/KwJuZTlNZBjauKhTK0c+zT+q5JOCx0UFhXQ6rJR9jg6Es8gPuD2uZcYDLqSw==" crossorigin="anonymous">
|
17
|
+
</head>
|
18
|
+
|
19
|
+
<body>
|
20
|
+
<div class="container">
|
21
|
+
<%= erb :'_top_bar', layout: false, locals: locals %>
|
22
|
+
<%= erb :'_flash_messages', layout: false, locals: locals %>
|
23
|
+
<%= yield %>
|
24
|
+
</div>
|
25
|
+
</body>
|
26
|
+
</html>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-7s5uDGW3AHqw6xtJmNNtr+OBRJUlgkNJEo78P4b0yRw= sha512-nNo+yCHEyn0smMxSswnf/OnX6/KwJuZTlNZBjauKhTK0c+zT+q5JOCx0UFhXQ6rJR9jg6Es8gPuD2uZcYDLqSw==" crossorigin="anonymous">
|
2
|
+
|
3
|
+
<style>
|
4
|
+
body {
|
5
|
+
background: #fff url(<%="#{base_url}/legend.gif"%>) no-repeat fixed right 130px;
|
6
|
+
}
|
7
|
+
|
8
|
+
.form-large input {
|
9
|
+
font-size: 18px;
|
10
|
+
padding: 8px;
|
11
|
+
height: auto;
|
12
|
+
line-height: normal;
|
13
|
+
}
|
14
|
+
|
15
|
+
.form-wide {
|
16
|
+
width: 480px;
|
17
|
+
}
|
18
|
+
</style>
|
19
|
+
|
20
|
+
<body>
|
21
|
+
<div style="margin-top: 100px"></div>
|
22
|
+
|
23
|
+
<div class="container">
|
24
|
+
<h1>Shopify Sinatra App</h1>
|
25
|
+
<h2><small>This app requires you to login to start using it.</small></h2>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<div style="margin-top: 30px"></div>
|
29
|
+
|
30
|
+
<div class="container">
|
31
|
+
<form id="login" role="form" class="form-large" action="/auth/shopify" method="post">
|
32
|
+
<input type="hidden" name="authenticity_token" value="<%= env['rack.session'][:csrf] %>" />
|
33
|
+
<div class="input-group form-wide">
|
34
|
+
<input class="form-control" name="shop" value="<%= params['shop'] %>" placeholder="your-shop-url.myshopify.com">
|
35
|
+
<span class="input-group-btn">
|
36
|
+
<input class="btn" type="submit" value="Login" />
|
37
|
+
</span>
|
38
|
+
</div>
|
39
|
+
</form>
|
40
|
+
</div>
|
41
|
+
</body>
|
42
|
+
|