shopify-sinatra-app 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ecb9d81878d71aa112a589d76932f041c53e1d9a
4
+ data.tar.gz: 81d6eb212418d81c89fbad68595d3b6fdcde01b9
5
+ SHA512:
6
+ metadata.gz: a9738522aa12eec39f3b87267a0a72af1b01788dcdc0acba8280b83b53fabb28ae71dad9ef9a19329fd62f273ebf4abd014f9f07fbb7db3ee9dc61ba5997bad7
7
+ data.tar.gz: 176f8fc34e159c4f01ea4eb9f5f779a6bc4ff29a85fab83562c8d78c01bb2448a9848cea1484b3960cb3bad5002208705bc622a518b8b152df82418e8fe5e644
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,29 @@
1
+ source 'https://rubygems.org'
2
+ ruby "2.0.0"
3
+
4
+ gem 'sinatra'
5
+ gem 'sinatra-redis'
6
+ gem 'sinatra-activerecord'
7
+ gem 'sinatra-twitter-bootstrap', require: 'sinatra/twitter-bootstrap'
8
+ gem 'rack-flash3', require: 'rack-flash'
9
+ gem 'activesupport'
10
+ gem 'attr_encrypted'
11
+ gem 'foreman'
12
+ gem 'rake'
13
+
14
+ gem 'resque', '~> 1.22.0'
15
+
16
+ gem 'omniauth-shopify-oauth2'
17
+ gem 'shopify_api'
18
+
19
+ group :production do
20
+ gem 'pg'
21
+ end
22
+
23
+ group :development do
24
+ gem 'sqlite3'
25
+ gem 'rack-test'
26
+ gem 'fakeweb'
27
+ gem 'mocha', require: false
28
+ gem 'byebug'
29
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,137 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ activemodel (4.1.1)
5
+ activesupport (= 4.1.1)
6
+ builder (~> 3.1)
7
+ activerecord (4.1.1)
8
+ activemodel (= 4.1.1)
9
+ activesupport (= 4.1.1)
10
+ arel (~> 5.0.0)
11
+ activeresource (4.0.0)
12
+ activemodel (~> 4.0)
13
+ activesupport (~> 4.0)
14
+ rails-observers (~> 0.1.1)
15
+ activesupport (4.1.1)
16
+ i18n (~> 0.6, >= 0.6.9)
17
+ json (~> 1.7, >= 1.7.7)
18
+ minitest (~> 5.1)
19
+ thread_safe (~> 0.1)
20
+ tzinfo (~> 1.1)
21
+ arel (5.0.1.20140414130214)
22
+ attr_encrypted (1.3.2)
23
+ encryptor (>= 1.3.0)
24
+ builder (3.2.2)
25
+ byebug (3.1.2)
26
+ columnize (~> 0.8)
27
+ debugger-linecache (~> 1.2)
28
+ coderay (1.1.0)
29
+ columnize (0.8.9)
30
+ debugger-linecache (1.2.0)
31
+ dotenv (0.7.0)
32
+ encryptor (1.3.0)
33
+ fakeweb (1.3.0)
34
+ faraday (0.9.0)
35
+ multipart-post (>= 1.2, < 3)
36
+ foreman (0.64.0)
37
+ dotenv (~> 0.7.0)
38
+ thor (>= 0.13.6)
39
+ hashie (2.1.1)
40
+ i18n (0.6.9)
41
+ json (1.8.1)
42
+ jwt (1.0.0)
43
+ metaclass (0.0.4)
44
+ method_source (0.8.2)
45
+ minitest (5.3.4)
46
+ mocha (1.1.0)
47
+ metaclass (~> 0.0.1)
48
+ multi_json (1.10.1)
49
+ multi_xml (0.5.5)
50
+ multipart-post (2.0.0)
51
+ oauth2 (0.9.4)
52
+ faraday (>= 0.8, < 0.10)
53
+ jwt (~> 1.0)
54
+ multi_json (~> 1.3)
55
+ multi_xml (~> 0.5)
56
+ rack (~> 1.2)
57
+ omniauth (1.2.1)
58
+ hashie (>= 1.2, < 3)
59
+ rack (~> 1.0)
60
+ omniauth-oauth2 (1.1.2)
61
+ faraday (>= 0.8, < 0.10)
62
+ multi_json (~> 1.3)
63
+ oauth2 (~> 0.9.3)
64
+ omniauth (~> 1.2)
65
+ omniauth-shopify-oauth2 (1.1.6)
66
+ omniauth-oauth2 (~> 1.1.1)
67
+ pg (0.17.1)
68
+ pry (0.9.12.6)
69
+ coderay (~> 1.0)
70
+ method_source (~> 0.8)
71
+ slop (~> 3.4)
72
+ rack (1.5.2)
73
+ rack-flash3 (1.0.5)
74
+ rack
75
+ rack-protection (1.5.3)
76
+ rack
77
+ rack-test (0.6.2)
78
+ rack (>= 1.0)
79
+ rails-observers (0.1.2)
80
+ activemodel (~> 4.0)
81
+ rake (10.3.2)
82
+ redis (3.0.7)
83
+ redis-namespace (1.4.1)
84
+ redis (~> 3.0.4)
85
+ resque (1.22.0)
86
+ multi_json (~> 1.0)
87
+ redis-namespace (~> 1.0)
88
+ sinatra (>= 0.9.2)
89
+ vegas (~> 0.1.2)
90
+ shopify_api (3.2.4)
91
+ activeresource (>= 3.0.0)
92
+ pry (>= 0.9.12.6)
93
+ thor (~> 0.18.1)
94
+ sinatra (1.4.5)
95
+ rack (~> 1.4)
96
+ rack-protection (~> 1.4)
97
+ tilt (~> 1.3, >= 1.3.4)
98
+ sinatra-activerecord (2.0.2)
99
+ activerecord (>= 3.2)
100
+ sinatra (~> 1.0)
101
+ sinatra-redis (0.3.0)
102
+ redis
103
+ sinatra (>= 0.9.4)
104
+ sinatra-twitter-bootstrap (2.3.3)
105
+ slop (3.5.0)
106
+ sqlite3 (1.3.9)
107
+ thor (0.18.1)
108
+ thread_safe (0.3.4)
109
+ tilt (1.4.1)
110
+ tzinfo (1.2.1)
111
+ thread_safe (~> 0.1)
112
+ vegas (0.1.11)
113
+ rack (>= 1.0.0)
114
+
115
+ PLATFORMS
116
+ ruby
117
+
118
+ DEPENDENCIES
119
+ activesupport
120
+ attr_encrypted
121
+ byebug
122
+ fakeweb
123
+ foreman
124
+ json
125
+ mocha
126
+ omniauth-shopify-oauth2
127
+ pg
128
+ rack-flash3
129
+ rack-test
130
+ rake
131
+ resque (~> 1.22.0)
132
+ shopify_api
133
+ sinatra
134
+ sinatra-activerecord
135
+ sinatra-redis
136
+ sinatra-twitter-bootstrap
137
+ sqlite3
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 Kevin Hughes
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ shopify-sinatra-app
2
+ ===================
3
+
4
+ "A classy shopify app"
5
+
6
+
7
+ Getting Started
8
+ ---------------
9
+
10
+ Install the gem:
11
+
12
+ ```
13
+ gem install shopify-sinatra-app
14
+ ```
15
+
16
+ or build from source
17
+
18
+ ```
19
+ gem build shopify-sinatra-app.gemspec
20
+ gem install shopify-sinatra-app-X.X.X.gem
21
+ ```
22
+
23
+ To create a new app use the generator:
24
+
25
+ ```
26
+ shopify-sinatra-app-generator new <your new app name>
27
+ ```
28
+
29
+ This will create a new skeleton shopify-sinatra-app. The generator will create several default files for you rather than having them bundled in the sinatra extension - its worthwhile to read this section to understand what each of these files is for.
30
+
31
+ `config/app.yml` --> Some important config information is contained in this file.
32
+ * scope: The scope of your app (what your app can access from the Shopify store once installed, e.g. read_products), this will be read by your app and used when your app is installed.
33
+
34
+ * uninstall_webhook: Initially an uninstall webhook is also defined in this file, although without a proper url, this file is a good place to define objects that need to be created on Shopify when your app is installed like webhooks, fulfillment and carrier services.
35
+
36
+ `config/database.yml` --> The database config for active record. Initially this is setup to use sqlite3 for development and testing which you may want to change to mimic your production database.
37
+
38
+ `config.ru` --> Rackup file - describes how to run a rack based app
39
+
40
+ `Gemfile` --> manages the dependencies of the app
41
+
42
+ `lib/app.rb` --> This file is the skeleton app file. More details on how to use the methods provided by this extension are given in the following section.
43
+
44
+ `Procfile` --> Specific for deploying to Heroku, this file tells heroku how to run the app
45
+
46
+ `public/icon.png` --> This icon file is used by the Shopify Embedded App SKD and is shown in the menu bar of your embedded app
47
+
48
+ `Rakefile` --> includes some helper methods etc for running and managing the app. Standard for ruby based projects
49
+
50
+ `views/layouts/appliction.erb` --> This is the layout file that all templates will use unless otherwise specified. It sets up some defaults for using the Shopify Embedded App SDK and Twitter Bootstrap for styling
51
+
52
+ `views/_top_bar.erb` --> This is a partial view that describes the top bar inside a Shopify Embedded App. It also has some code to *forward* flash messages to the Embedded App SKD
53
+
54
+ `views/*` --> The other views used by the app. You'll probably make a lot of changes to home.erb and install.erb to customize the experience for your app
55
+
56
+
57
+ ### Setting the app to use your Shopify API credentials
58
+
59
+ You'll need to create a Shopify Partner Account and a new application. You can make an account [here](http://www.shopify.ca/partners) and see this [tutorial](http://docs.shopify.com/api/the-basics/getting-started) for creating a new application.
60
+
61
+ After creating your new application you need to create a `.env` file and add the following lines:
62
+
63
+ ```
64
+ SHOPIFY_API_KEY=<your api key>
65
+ SHOPIFY_SHARED_SECRET=<your shared secret>
66
+ SECRET=<generate a random string to encrypt credentials with>
67
+ ```
68
+
69
+
70
+ Shopify::Methods
71
+ ----------------
72
+
73
+ `shopify_session` - The main method of the framework, most of your routes will use this method to acquire a valid shopify session and then perform api calls to Shopfiy. The method simply takes a block of code and makes the shop_name available to you after activating an api session. Here is an example endpoint that displays products:
74
+
75
+ ```ruby
76
+ get '/products.json' do
77
+ shopify_session do |shop_name|
78
+ products = ShopifyAPI::Product.all(limit: 5)
79
+ products.to_json
80
+ end
81
+ end
82
+ ```
83
+
84
+ `webhook_session` - This method is for an endpoint that recieves a webhook from Shopify. Webhooks are a great way to keep your app in sync with a shop's data without polling. You can read more about webhooks [here](http://docs.shopify.com/api/tutorials/using-webhooks). This method also takes a block of code and makes the shop and webhook data as a hash available (note only works for json webhooks, don't use xml). Here is an example that listens to a order creation webhook:
85
+
86
+ ```ruby
87
+ post '/order.json' do
88
+ webhook_session do |shop, params|
89
+ # do something with the data
90
+ end
91
+ end
92
+ ```
93
+
94
+ `webhook_job` - Its impossible to control the flow of webhooks to your app from Shopify especially if a larger store installs your app or if a shop has a flash sale. To prevent your app from getting overloaded with webhook requests it is usually a good idea to process webhooks in a background queue and return a 200 to Shopify immediately. This method provides this functionality using redis and resque. This method takes the name of a job class whose perform method expects a `shop_name` and the webhook data as a hash. The session method is useful for prototpying and experimenting but production apps should use `webhook_job`. Here is an example:
95
+
96
+ ```ruby
97
+ post '/order.json' do
98
+ webhook_job(OrderWebhookJob)
99
+ end
100
+
101
+ class OrderWebhookJob
102
+ @queue = :default
103
+
104
+ def self.perform(shop_name, params)
105
+ # do something with the data
106
+ end
107
+ end
108
+ ```
109
+
110
+ `install` - This is a private method provided with the framework that gets called when the app is authorized for the first time. You should fill this method in with anything you need to initialize on install, for example webhooks and services on shopify or any other database models you have created specific to a shop.
111
+
112
+ `uninstall` - This method gets called when your app recieves an uninstall webhook from shopify. You should override this method in your class and do any appropriate clean up when the app is removed from a shop.
113
+
114
+ `logout` - This method clears the current session data in the app
115
+
116
+ `current_shop` - Returns the name of the current shop
117
+
118
+ `base_url` - This returns the url of the app
119
+
120
+
121
+ Deploying
122
+ ---------
123
+
124
+ This template was created with deploying to Heroku in mind. Heroku is a great cloud based app hosting provider that makes it incredibly easy to get an application into a product environment. To create a new heroku application download the [Heroku Toolbelt](https://devcenter.heroku.com/articles/quickstart) and create a new application:
125
+
126
+ ```
127
+ heroku apps:create <your new app name>
128
+ ```
129
+
130
+ You will also need to add the following (free) add-ons to your new Heroku app:
131
+
132
+ ```
133
+ heroku addons:add heroku-postgresql
134
+ heroku addons:add rediscloud
135
+ ```
136
+
137
+ and make sure you have at least 1 dyno for web and resque:
138
+
139
+ ```
140
+ heroku scale web=1 resque=1
141
+ ```
142
+
143
+ Note - if you are not using any background queue for processing webhooks then you do not need the redis add-on or the resque dyno so you can set it to 0.
144
+
145
+ Contributing
146
+ ------------
147
+
148
+ PRs welcome!
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fileutils'
3
+
4
+ working_dir = Dir.pwd
5
+
6
+ spec = Gem::Specification.find_by_name("shopify-sinatra-app")
7
+ gem_root = spec.gem_dir
8
+ gem_lib = gem_root + "/lib"
9
+
10
+ generator_dir = gem_lib + "/generator"
11
+
12
+ if ARGV.length < 2
13
+ puts "Usage:\n./shopify-sinatra-app new <app_name>"
14
+ else
15
+ app_name = ARGV[1]
16
+ app_dir = working_dir + "/" + app_name
17
+
18
+ begin
19
+ Dir.mkdir(app_dir)
20
+
21
+ FileUtils.cp_r(generator_dir + "/config", app_dir + "/config")
22
+ FileUtils.cp_r(generator_dir + "/db", app_dir + "/db")
23
+ FileUtils.cp_r(generator_dir + "/lib", app_dir + "/lib")
24
+ FileUtils.cp_r(generator_dir + "/public", app_dir + "/public")
25
+ FileUtils.cp_r(generator_dir + "/views", app_dir + "/views")
26
+
27
+ FileUtils.cp(generator_dir + "/config.ru", app_dir + "/config.ru")
28
+ FileUtils.cp(generator_dir + "/Procfile", app_dir + "/Procfile")
29
+ FileUtils.cp(generator_dir + "/Rakefile", app_dir + "/Rakefile")
30
+ FileUtils.cp(generator_dir + "/Gemfile", app_dir + "/Gemfile")
31
+ FileUtils.cp(generator_dir + "/README.md", app_dir + "/README.md")
32
+
33
+ Dir.chdir(app_dir)
34
+
35
+ pipe = IO.popen("bundle install")
36
+ while (line = pipe.gets)
37
+ print line
38
+ end
39
+
40
+ pipe = IO.popen("bundle exec rake db:migrate")
41
+ while (line = pipe.gets)
42
+ print line
43
+ end
44
+
45
+ rescue Errno::EEXIST => e
46
+ puts "App directory alread exists, pick a new app name or delete the existing folder"
47
+ end
48
+ end
@@ -0,0 +1,30 @@
1
+ source 'https://rubygems.org'
2
+ ruby "2.0.0"
3
+
4
+ gem 'sinatra'
5
+ gem 'sinatra-redis'
6
+ gem 'sinatra-activerecord'
7
+ gem 'sinatra-twitter-bootstrap', require: 'sinatra/twitter-bootstrap'
8
+ gem 'rack-flash3', require: 'rack-flash'
9
+ gem 'activesupport'
10
+ gem 'attr_encrypted'
11
+ gem 'foreman'
12
+ gem 'rake'
13
+
14
+ gem 'resque', '~> 1.22.0'
15
+
16
+ gem 'shopify-sinatra-app'
17
+ gem 'omniauth-shopify-oauth2'
18
+ gem 'shopify_api'
19
+
20
+ group :production do
21
+ gem 'pg'
22
+ end
23
+
24
+ group :development do
25
+ gem 'sqlite3'
26
+ gem 'rack-test'
27
+ gem 'fakeweb'
28
+ gem 'mocha', require: false
29
+ gem 'byebug'
30
+ end
@@ -0,0 +1 @@
1
+ web: bundle exec rackup config.ru -p $PORT
@@ -0,0 +1,4 @@
1
+ New Shopify-Sinatra-App
2
+ =======================
3
+
4
+ Fill me in!
@@ -0,0 +1,48 @@
1
+ require 'sinatra/activerecord/rake'
2
+ require 'resque/tasks'
3
+ require 'rake/testtask'
4
+ require './lib/app'
5
+
6
+ task :server do
7
+ pipe = IO.popen("bundle exec rackup config.ru -p 4567")
8
+ while (line = pipe.gets)
9
+ print line
10
+ end
11
+ end
12
+
13
+ task :deploy do
14
+ pipe = IO.popen("git push heroku master --force")
15
+ while (line = pipe.gets)
16
+ print line
17
+ end
18
+ end
19
+
20
+ task :clear do
21
+ Rake::Task["clear_products"].invoke
22
+ Rake::Task["clear_charities"].invoke
23
+ Rake::Task["clear_shops"].invoke
24
+ end
25
+
26
+ task :clear_shops do
27
+ Shop.delete_all
28
+ end
29
+
30
+ task :clear_charities do
31
+ Charity.delete_all
32
+ end
33
+
34
+ task :clear_products do
35
+ Product.delete_all
36
+ end
37
+
38
+ task :creds2heroku do
39
+ Bundler.with_clean_env {
40
+ api_key = `sed -n '1p' .env`
41
+ shared_secret = `sed -n '2p' .env`
42
+ secret = `sed -n '3p' .env`
43
+
44
+ `heroku config:set #{api_key}`
45
+ `heroku config:set #{shared_secret}`
46
+ `heroku config:set #{secret}`
47
+ }
48
+ end
@@ -0,0 +1,6 @@
1
+ scope: 'read_products, read_orders'
2
+
3
+ uninstall_webhook:
4
+ topic: "app/uninstalled"
5
+ address: "your apps url"
6
+ format: "json"
@@ -0,0 +1,9 @@
1
+ development:
2
+ adapter: sqlite3
3
+ database: db/shopify_app.sqlite3
4
+ pool: 5
5
+
6
+ test:
7
+ adapter: sqlite3
8
+ database: "db/test.sqlite3"
9
+ pool: 8
@@ -0,0 +1,2 @@
1
+ require './lib/app'
2
+ SinatraApp.run!
File without changes
@@ -0,0 +1,12 @@
1
+ class CreateShops < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :shops do |t|
4
+ t.string :name
5
+ t.string :token_encrypted
6
+ end
7
+ end
8
+
9
+ def self.down
10
+ drop_table :shops
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ class AddIndexToShops < ActiveRecord::Migration
2
+ def self.up
3
+ add_index :shops, :name
4
+ end
5
+
6
+ def self.down
7
+ remove_index :shops, :name
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ require 'sinatra/shopify-sinatra-app'
2
+
3
+ class SinatraApp < Sinatra::Base
4
+ register Sinatra::Shopify
5
+
6
+ get '/' do
7
+ # your app's Home page
8
+ shopify_session do |shop_name|
9
+ @shop = Shop.find_by(:name => shop_name)
10
+ @products = ShopifyAPI::Product.all(limit: 5)
11
+ erb :home
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def install
18
+ # setup any webhooks or services you need when your app is installed
19
+ shopify_session do |shop_name|
20
+ params = YAML.load(File.read("config/app.yml"))
21
+
22
+ # create the uninstall webhook
23
+ uninstall_webhook = ShopifyAPI::Webhook.new(params["uninstall_webhook"])
24
+ unless ShopifyAPI::Webhook.find(:all).include?(uninstall_webhook)
25
+ uninstall_webhook.save
26
+ end
27
+ end
28
+ redirect '/'
29
+ end
30
+
31
+ def uninstall
32
+ # remove data for a shop when they uninstall your app
33
+ webhook_session do |shop, params|
34
+ shop.destroy
35
+ end
36
+ end
37
+
38
+ end
Binary file
@@ -0,0 +1,17 @@
1
+ <script type="text/javascript">
2
+
3
+ ShopifyApp.ready(function(){
4
+ ShopifyApp.Bar.initialize({
5
+ icon: 'icon.png'
6
+ });
7
+
8
+ <% if flash[:notice] %>
9
+ ShopifyApp.flashNotice("<%= flash[:notice] %>");
10
+ <% end %>
11
+
12
+ <% if flash[:error] %>
13
+ ShopifyApp.flashError("<%= flash[:error] %>");
14
+ <% end %>
15
+
16
+ });
17
+ </script>
@@ -0,0 +1,17 @@
1
+ <%= erb :"_top_bar", :layout => false, :locals => locals %>
2
+
3
+ <h3>Shopify Sinatra App</h3>
4
+ <p>Welcome!</p>
5
+
6
+ <h3>Products</h3>
7
+ <table class="table table-striped table-hover">
8
+ <tbody>
9
+ <% @products.each do |product| %>
10
+ <tr>
11
+ <td>
12
+ <a href=<%="https://#{@shop.name}/admin/products/#{product.product_id}"%> target="_blank"> <%= product.product_id %> </a>
13
+ </td>
14
+ </tr>
15
+ <% end %>
16
+ </tbody>
17
+ </table>
@@ -0,0 +1,9 @@
1
+ <h1>Shopify Sinatra App</h1>
2
+
3
+ <h2>Install This App</h2>
4
+ <h2><small>This app requires you to login to start using it.</small></h2>
5
+
6
+ <form action="/login" method="post">
7
+ <input type="url" name="shop" placeholder="Shop URL">
8
+ <input type="submit" value="Install App" />
9
+ </form>
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <script src="https://cdn.shopify.com/s/assets/external/app.js"></script>
5
+ <script type="text/javascript">
6
+ ShopifyApp.init({
7
+ apiKey: "<%= SinatraApp.settings.api_key %>",
8
+ shopOrigin: "<%= current_shop_url %>",
9
+ debug: true
10
+ });
11
+ </script>
12
+
13
+ <%= bootstrap_assets %>
14
+ </head>
15
+
16
+ <body>
17
+ <div class="container">
18
+ <%= yield %>
19
+ </div>
20
+ </body>
21
+ </html>
@@ -0,0 +1,248 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/redis'
3
+ require 'sinatra/activerecord'
4
+ require 'sinatra/twitter-bootstrap'
5
+
6
+ require 'resque'
7
+ require 'rack-flash'
8
+ require 'attr_encrypted'
9
+ require 'active_support/all'
10
+
11
+ require 'shopify_api'
12
+ require 'omniauth-shopify-oauth2'
13
+
14
+ module Sinatra
15
+ module Shopify
16
+
17
+ module Methods
18
+ def install
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def uninstall
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def logout
27
+ session[:shopify] = nil
28
+ end
29
+
30
+ def base_url
31
+ @base_url ||= "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}"
32
+ end
33
+
34
+ def current_shop
35
+ session[:shopify][:shop] if session.has_key?(:shopify)
36
+ end
37
+
38
+ def current_shop_url
39
+ "https://#{current_shop}" if current_shop
40
+ end
41
+
42
+ def shopify_session(&blk)
43
+ if !session.has_key?(:shopify)
44
+ get_session
45
+ elsif params[:shop].present? && session[:shopify][:shop] != sanitize_shop_param(params)
46
+ logout
47
+ get_session
48
+ else
49
+ shop_name = session[:shopify][:shop]
50
+ token = session[:shopify][:token]
51
+
52
+ api_session = ShopifyAPI::Session.new(shop_name, token)
53
+ ShopifyAPI::Base.activate_session(api_session)
54
+
55
+ yield shop_name
56
+ end
57
+ end
58
+
59
+ def webhook_session(&blk)
60
+ return unless verify_shopify_webhook
61
+
62
+ shop_name = request.env['HTTP_X_SHOPIFY_SHOP_DOMAIN']
63
+ shop = Shop.find_by(:name => shop_name)
64
+
65
+ if shop.present?
66
+ params = ActiveSupport::JSON.decode(request.body.read.to_s)
67
+ api_session = ShopifyAPI::Session.new(shop_name, shop.token)
68
+ ShopifyAPI::Base.activate_session(api_session)
69
+
70
+ yield shop, params
71
+
72
+ status 200
73
+ end
74
+ end
75
+
76
+ def webhook_job(jobKlass)
77
+ return unless verify_shopify_webhook
78
+
79
+ shop_name = request.env['HTTP_X_SHOPIFY_SHOP_DOMAIN']
80
+ params = ActiveSupport::JSON.decode(request.body.read.to_s)
81
+
82
+ Resque.enqueue(jobKlass, shop_name, params)
83
+
84
+ status 200
85
+ end
86
+
87
+ private
88
+
89
+ def get_session
90
+ shop_name = sanitize_shop_param(params)
91
+ shop = Shop.find_by(:name => shop_name)
92
+
93
+ return_to = request.env["sinatra.route"].split(' ').last
94
+
95
+ if shop.present?
96
+ session[:shopify] ||= {}
97
+ session[:shopify][:shop] = shop.name
98
+ session[:shopify][:token] = shop.token
99
+ redirect return_to
100
+ else
101
+ authenticate(return_to)
102
+ end
103
+ end
104
+
105
+ def authenticate(return_to = '/')
106
+ if shop_name = sanitize_shop_param(params)
107
+ redirect_url = "/auth/shopify?shop=#{shop_name}&return_to=#{base_url}#{return_to}"
108
+ fullpage_redirect_to redirect_url
109
+ else
110
+ redirect "/install"
111
+ end
112
+ end
113
+
114
+ def fullpage_redirect_to(redirect_url)
115
+ @fullpage_redirect_to = redirect_url
116
+
117
+ erb "<script type='text/javascript'>
118
+ window.top.location.href = '<%= @fullpage_redirect_to %>';
119
+ </script>", :layout => false
120
+ end
121
+
122
+ def sanitize_shop_param(params)
123
+ return unless params[:shop].present?
124
+ name = params[:shop].to_s.strip
125
+ name += '.myshopify.com' if !name.include?("myshopify.com") && !name.include?(".")
126
+ name.gsub!('https://', '')
127
+ name.gsub!('http://', '')
128
+
129
+ u = URI("http://#{name}")
130
+ u.host.ends_with?(".myshopify.com") ? u.host : nil
131
+ end
132
+
133
+ def verify_shopify_webhook
134
+ data = request.body.read.to_s
135
+ digest = OpenSSL::Digest::Digest.new('sha256')
136
+ calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, app.settings.shared_secret, data)).strip
137
+ request.body.rewind
138
+
139
+ calculated_hmac == request.env['HTTP_X_SHOPIFY_HMAC_SHA256']
140
+ end
141
+ end
142
+
143
+ def self.registered(app)
144
+ app.helpers Shopify::Methods
145
+ app.register Sinatra::ActiveRecordExtension
146
+ app.register Sinatra::Twitter::Bootstrap::Assets
147
+
148
+ app.set :database_file, File.expand_path("config/database.yml")
149
+ app.set :views, File.expand_path("views")
150
+ app.set :public_folder, File.expand_path("public")
151
+ app.set :erb, :layout => :'layouts/application'
152
+ app.set :protection, :except => :frame_options
153
+
154
+ app.enable :sessions
155
+ app.enable :inline_templates
156
+
157
+ app.set :scope, YAML.load(File.read(File.expand_path("config/app.yml")))["scope"]
158
+
159
+ app.set :api_key, ENV['SHOPIFY_API_KEY']
160
+ app.set :shared_secret, ENV['SHOPIFY_SHARED_SECRET']
161
+ app.set :secret, ENV['SECRET']
162
+
163
+ app.use Rack::Flash, :sweep => true
164
+ app.use Rack::MethodOverride
165
+ app.use Rack::Session::Cookie, :key => '#{base_url}.session',
166
+ :path => '/',
167
+ :secret => app.settings.secret,
168
+ :expire_after => 2592000
169
+
170
+ app.set :redis_url, ENV["REDISCLOUD_URL"] || "redis://localhost:6379/"
171
+ redis_uri = URI.parse(app.settings.redis_url)
172
+ Resque.redis = ::Redis.new(:host => redis_uri.host,
173
+ :port => redis_uri.port,
174
+ :password => redis_uri.password)
175
+ Resque.redis.namespace = "resque"
176
+ app.set :redis, app.settings.redis_url
177
+
178
+ app.use OmniAuth::Builder do
179
+ provider :shopify,
180
+ app.settings.api_key,
181
+ app.settings.shared_secret,
182
+
183
+ :scope => app.settings.scope,
184
+
185
+ :setup => lambda { |env|
186
+ params = Rack::Utils.parse_query(env['QUERY_STRING'])
187
+ site_url = "https://#{params['shop']}"
188
+ env['omniauth.strategy'].options[:client_options][:site] = site_url
189
+ }
190
+ end
191
+
192
+ ShopifyAPI::Session.setup({:api_key => app.settings.api_key,
193
+ :secret => app.settings.shared_secret})
194
+
195
+ app.get '/install' do
196
+ erb :install, :layout => false
197
+ end
198
+
199
+ # endpoint for the app/uninstall webhook
200
+ app.post '/uninstall.json' do
201
+ uninstall
202
+ end
203
+
204
+ app.post '/login' do
205
+ authenticate
206
+ end
207
+
208
+ app.get '/logout' do
209
+ logout
210
+ redirect '/install'
211
+ end
212
+
213
+ app.get '/auth/shopify/callback' do
214
+ shop_name = params["shop"]
215
+ token = request.env['omniauth.auth']['credentials']['token']
216
+
217
+ session[:shopify] ||= {}
218
+ session[:shopify][:shop] = shop_name
219
+ session[:shopify][:token] = token
220
+
221
+ if Shop.where(:name => shop_name).blank?
222
+ Shop.create(:name => shop_name, :token => token)
223
+ install
224
+ end
225
+
226
+ return_to = env['omniauth.params']['return_to']
227
+ redirect return_to
228
+ end
229
+
230
+ app.get '/auth/failure' do
231
+ erb "<h1>Authentication Failed:</h1>
232
+ <h3>message:<h3> <pre>#{params}</pre>", :layout => false
233
+ end
234
+ end
235
+ end
236
+
237
+ class Shop < ActiveRecord::Base
238
+
239
+ def self.secret
240
+ @secret ||= ENV['SECRET']
241
+ end
242
+
243
+ attr_encrypted :token, :key => secret, :attribute => 'token_encrypted'
244
+ validates_presence_of :name, :token
245
+ end
246
+
247
+ register Shopify
248
+ end
@@ -0,0 +1,34 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'shopify-sinatra-app'
3
+ s.version = '0.0.1'
4
+
5
+ s.summary = "A classy shopify app"
6
+ s.description = "A Sinatra extension for building Shopify Apps. Akin to the shopify_app gem but for Sinatra"
7
+
8
+ s.authors = ["Kevin Hughes"]
9
+ s.email = "kevin.hughes@shopify.com"
10
+ s.homepage = "https://github.com/pickle27/shopify-sinatra-app/"
11
+ s.license = 'MIT'
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.executables << 'shopify-sinatra-app-generator'
15
+
16
+ s.add_runtime_dependency 'sinatra'
17
+ s.add_runtime_dependency 'sinatra-redis'
18
+ s.add_runtime_dependency 'sinatra-activerecord'
19
+ s.add_runtime_dependency 'sinatra-twitter-bootstrap'
20
+ s.add_runtime_dependency 'rack-flash3'
21
+ s.add_runtime_dependency 'activesupport'
22
+ s.add_runtime_dependency 'attr_encrypted'
23
+
24
+ s.add_runtime_dependency 'resque', '~> 1.22.0'
25
+
26
+ s.add_runtime_dependency 'shopify_api'
27
+ s.add_runtime_dependency 'omniauth-shopify-oauth2'
28
+
29
+ s.add_development_dependency 'rake'
30
+ s.add_development_dependency 'sqlite3'
31
+ s.add_development_dependency 'rack-test'
32
+ s.add_development_dependency 'fakeweb'
33
+ s.add_development_dependency 'mocha'
34
+ end
metadata ADDED
@@ -0,0 +1,279 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shopify-sinatra-app
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kevin Hughes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sinatra
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sinatra-redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sinatra-activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra-twitter-bootstrap
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack-flash3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: attr_encrypted
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: resque
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 1.22.0
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 1.22.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: shopify_api
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: omniauth-shopify-oauth2
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rake
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: sqlite3
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rack-test
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: fakeweb
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: mocha
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ description: A Sinatra extension for building Shopify Apps. Akin to the shopify_app
224
+ gem but for Sinatra
225
+ email: kevin.hughes@shopify.com
226
+ executables:
227
+ - shopify-sinatra-app-generator
228
+ extensions: []
229
+ extra_rdoc_files: []
230
+ files:
231
+ - ".gitignore"
232
+ - Gemfile
233
+ - Gemfile.lock
234
+ - LICENSE
235
+ - README.md
236
+ - bin/shopify-sinatra-app-generator
237
+ - lib/generator/Gemfile
238
+ - lib/generator/Procfile
239
+ - lib/generator/README.md
240
+ - lib/generator/Rakefile
241
+ - lib/generator/config.ru
242
+ - lib/generator/config/app.yml
243
+ - lib/generator/config/database.yml
244
+ - lib/generator/db/.keep
245
+ - lib/generator/db/migrate/20140413221328_create_shops.rb
246
+ - lib/generator/db/migrate/20140414042317_add_index_to_shops.rb
247
+ - lib/generator/lib/app.rb
248
+ - lib/generator/public/icon.png
249
+ - lib/generator/views/_top_bar.erb
250
+ - lib/generator/views/home.erb
251
+ - lib/generator/views/install.erb
252
+ - lib/generator/views/layouts/application.erb
253
+ - lib/sinatra/shopify-sinatra-app.rb
254
+ - shopify-sinatra-app.gemspec
255
+ homepage: https://github.com/pickle27/shopify-sinatra-app/
256
+ licenses:
257
+ - MIT
258
+ metadata: {}
259
+ post_install_message:
260
+ rdoc_options: []
261
+ require_paths:
262
+ - lib
263
+ required_ruby_version: !ruby/object:Gem::Requirement
264
+ requirements:
265
+ - - ">="
266
+ - !ruby/object:Gem::Version
267
+ version: '0'
268
+ required_rubygems_version: !ruby/object:Gem::Requirement
269
+ requirements:
270
+ - - ">="
271
+ - !ruby/object:Gem::Version
272
+ version: '0'
273
+ requirements: []
274
+ rubyforge_project:
275
+ rubygems_version: 2.0.14
276
+ signing_key:
277
+ specification_version: 4
278
+ summary: A classy shopify app
279
+ test_files: []