disco_app 0.4.2
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/Rakefile +37 -0
- data/app/assets/javascripts/disco_app/disco_app.js +7 -0
- data/app/assets/stylesheets/disco_app/bootstrap-custom.scss +55 -0
- data/app/assets/stylesheets/disco_app/bootstrap-variables.scss +12 -0
- data/app/assets/stylesheets/disco_app/disco_app.scss +7 -0
- data/app/controllers/disco_app/app_proxy_controller.rb +41 -0
- data/app/controllers/disco_app/authenticated_controller.rb +44 -0
- data/app/controllers/disco_app/charges_controller.rb +30 -0
- data/app/controllers/disco_app/install_controller.rb +26 -0
- data/app/controllers/disco_app/webhooks_controller.rb +40 -0
- data/app/helpers/disco_app/application_helper.rb +4 -0
- data/app/jobs/disco_app/app_installed_job.rb +41 -0
- data/app/jobs/disco_app/app_uninstalled_job.rb +18 -0
- data/app/jobs/disco_app/shop_job.rb +29 -0
- data/app/jobs/disco_app/shop_update_job.rb +16 -0
- data/app/models/disco_app/shop.rb +39 -0
- data/app/services/disco_app/charges_service.rb +73 -0
- data/app/views/disco_app/charges/activate.html.erb +1 -0
- data/app/views/disco_app/charges/create.html.erb +1 -0
- data/app/views/disco_app/charges/new.html.erb +45 -0
- data/app/views/disco_app/install/installing.html.erb +7 -0
- data/app/views/disco_app/install/uninstalling.html.erb +1 -0
- data/app/views/disco_app/proxy_errors/404.html.erb +1 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20150525162112_add_status_to_shops.rb +5 -0
- data/db/migrate/20150525171422_add_meta_to_shops.rb +11 -0
- data/db/migrate/20150629210346_add_charge_status_to_shop.rb +5 -0
- data/lib/disco_app/engine.rb +6 -0
- data/lib/disco_app/version.rb +3 -0
- data/lib/disco_app.rb +4 -0
- data/lib/generators/disco_app/USAGE +5 -0
- data/lib/generators/disco_app/disco_app_generator.rb +182 -0
- data/lib/generators/disco_app/reactify/reactify_generator.rb +31 -0
- data/lib/generators/disco_app/templates/assets/javascripts/application.js +17 -0
- data/lib/generators/disco_app/templates/assets/stylesheets/application.scss +5 -0
- data/lib/generators/disco_app/templates/config/puma.rb +15 -0
- data/lib/generators/disco_app/templates/controllers/home_controller.rb +7 -0
- data/lib/generators/disco_app/templates/initializers/disco_app.rb +1 -0
- data/lib/generators/disco_app/templates/initializers/shopify_app.rb +6 -0
- data/lib/generators/disco_app/templates/jobs/app_installed_job.rb +2 -0
- data/lib/generators/disco_app/templates/jobs/app_uninstalled_job.rb +2 -0
- data/lib/generators/disco_app/templates/jobs/shop_update_job.rb +2 -0
- data/lib/generators/disco_app/templates/models/shop.rb +3 -0
- data/lib/generators/disco_app/templates/root/Procfile +2 -0
- data/lib/generators/disco_app/templates/views/home/index.html.erb +2 -0
- data/lib/generators/disco_app/templates/views/layouts/application.html.erb +18 -0
- data/lib/generators/disco_app/templates/views/layouts/embedded_app.html.erb +33 -0
- data/lib/generators/disco_app/templates/views/sessions/new.html.erb +26 -0
- data/test/disco_app_test.rb +7 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config/application.rb +26 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/lib/generators/disco_app/disco_app_generator_test.rb +16 -0
- data/test/test_helper.rb +20 -0
- metadata +336 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bfde7107b9cdb0de640f3358353f9c34a4a7daab73625a233970d4be0f914f64
|
|
4
|
+
data.tar.gz: a592683bdc19557edb0024bd2f9266701b51dc053e2b2f6b0ad3d7335411e41f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d19fb76b17ecb36b93cbeee6c2939c0a83f8c028dbb0199f52c941dd9edda4319c1a85b864a167c87562a9394d5bf39fdf6e5fafbb4db39174cccd0646cb0e8e
|
|
7
|
+
data.tar.gz: c98f03e3c2119ed8ea4047371217055caede069cb528a4b741a8149bf8492ba68c58f83b7c8935bb69485353e19b546c3fa35abcfa93598430a5130d516c0a9f
|
data/Rakefile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require 'rdoc/task'
|
|
8
|
+
|
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
11
|
+
rdoc.title = 'DiscoApp'
|
|
12
|
+
rdoc.options << '--line-numbers'
|
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
|
18
|
+
load 'rails/tasks/engine.rake'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
load 'rails/tasks/statistics.rake'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Bundler::GemHelper.install_tasks
|
|
26
|
+
|
|
27
|
+
require 'rake/testtask'
|
|
28
|
+
|
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
|
30
|
+
t.libs << 'lib'
|
|
31
|
+
t.libs << 'test'
|
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
|
33
|
+
t.verbose = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
task default: :test
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* bootstrap-custom.scss
|
|
3
|
+
* Customise the bootstrap components we pull in styles for.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Core variables and mixins
|
|
7
|
+
@import "bootstrap/variables";
|
|
8
|
+
@import "bootstrap/mixins";
|
|
9
|
+
|
|
10
|
+
// Reset and dependencies
|
|
11
|
+
@import "bootstrap/normalize";
|
|
12
|
+
//@import "bootstrap/print";
|
|
13
|
+
//@import "bootstrap/glyphicons";
|
|
14
|
+
|
|
15
|
+
// Core CSS
|
|
16
|
+
@import "bootstrap/scaffolding";
|
|
17
|
+
@import "bootstrap/type";
|
|
18
|
+
//@import "bootstrap/code";
|
|
19
|
+
@import "bootstrap/grid";
|
|
20
|
+
@import "bootstrap/tables";
|
|
21
|
+
@import "bootstrap/forms";
|
|
22
|
+
@import "bootstrap/buttons";
|
|
23
|
+
|
|
24
|
+
// Components
|
|
25
|
+
@import "bootstrap/component-animations";
|
|
26
|
+
@import "bootstrap/dropdowns";
|
|
27
|
+
@import "bootstrap/button-groups";
|
|
28
|
+
@import "bootstrap/input-groups";
|
|
29
|
+
@import "bootstrap/navs";
|
|
30
|
+
@import "bootstrap/navbar";
|
|
31
|
+
//@import "bootstrap/breadcrumbs";
|
|
32
|
+
//@import "bootstrap/pagination";
|
|
33
|
+
//@import "bootstrap/pager";
|
|
34
|
+
@import "bootstrap/labels";
|
|
35
|
+
@import "bootstrap/badges";
|
|
36
|
+
//@import "bootstrap/jumbotron";
|
|
37
|
+
@import "bootstrap/thumbnails";
|
|
38
|
+
@import "bootstrap/alerts";
|
|
39
|
+
@import "bootstrap/progress-bars";
|
|
40
|
+
@import "bootstrap/media";
|
|
41
|
+
@import "bootstrap/list-group";
|
|
42
|
+
@import "bootstrap/panels";
|
|
43
|
+
//@import "bootstrap/responsive-embed";
|
|
44
|
+
//@import "bootstrap/wells";
|
|
45
|
+
@import "bootstrap/close";
|
|
46
|
+
|
|
47
|
+
// Components w/ JavaScript
|
|
48
|
+
//@import "bootstrap/modals";
|
|
49
|
+
//@import "bootstrap/tooltip";
|
|
50
|
+
//@import "bootstrap/popovers";
|
|
51
|
+
//@import "bootstrap/carousel";
|
|
52
|
+
|
|
53
|
+
// Utility classes
|
|
54
|
+
@import "bootstrap/utilities";
|
|
55
|
+
@import "bootstrap/responsive-utilities";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* bootstrap-variables.scss
|
|
3
|
+
* Set custom values for Bootstrap variables.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Grid - change to a 24-column grid for enhanced flexibility.
|
|
7
|
+
$grid-columns: 24;
|
|
8
|
+
$grid-gutter-width: 12px;
|
|
9
|
+
|
|
10
|
+
// Colors
|
|
11
|
+
$brand-primary: #479ccf;
|
|
12
|
+
$brand-danger: #ff5d5d;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
module AppProxyController
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
before_action :verify_proxy_signature
|
|
7
|
+
after_action :add_liquid_header
|
|
8
|
+
|
|
9
|
+
rescue_from ActiveRecord::RecordNotFound do |exception|
|
|
10
|
+
render_error 404
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def verify_proxy_signature
|
|
17
|
+
unless proxy_signature_is_valid?
|
|
18
|
+
head :unauthorized
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def proxy_signature_is_valid?
|
|
23
|
+
return true unless Rails.env.production?
|
|
24
|
+
query_hash = Rack::Utils.parse_query(request.query_string)
|
|
25
|
+
signature = query_hash.delete("signature")
|
|
26
|
+
sorted_params = query_hash.collect{ |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join
|
|
27
|
+
calculated_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha256'), ShopifyApp.configuration.secret, sorted_params)
|
|
28
|
+
signature == calculated_signature
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def add_liquid_header
|
|
32
|
+
response.headers['Content-Type'] = 'application/liquid'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render_error(status)
|
|
36
|
+
add_liquid_header
|
|
37
|
+
render "disco_app/proxy_errors/#{status}", status: status
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
module AuthenticatedController
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
before_action :login_again_if_different_shop
|
|
7
|
+
before_action :shopify_shop
|
|
8
|
+
before_action :verify_status
|
|
9
|
+
around_filter :shopify_session
|
|
10
|
+
layout 'embedded_app'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def shopify_shop
|
|
16
|
+
if shop_session
|
|
17
|
+
@shop = ::Shop.find_by!(shopify_domain: @shop_session.url)
|
|
18
|
+
else
|
|
19
|
+
redirect_to_login
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def verify_status
|
|
24
|
+
if not (@shop.charge_active? or @shop.charge_waived?)
|
|
25
|
+
redirect_if_not_current_path(disco_app.new_charge_path)
|
|
26
|
+
elsif @shop.charge_accepted?
|
|
27
|
+
redirect_if_not_current_path(disco_app.activate_charge_path)
|
|
28
|
+
elsif @shop.never_installed? or @shop.uninstalled?
|
|
29
|
+
redirect_if_not_current_path(disco_app.install_path)
|
|
30
|
+
elsif @shop.awaiting_install? or @shop.installing?
|
|
31
|
+
redirect_if_not_current_path(disco_app.installing_path)
|
|
32
|
+
elsif @shop.awaiting_uninstall? or @shop.uninstalling?
|
|
33
|
+
redirect_if_not_current_path(disco_app.uninstalling_path)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def redirect_if_not_current_path(target)
|
|
38
|
+
if request.path != target
|
|
39
|
+
redirect_to target
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
class ChargesController < ApplicationController
|
|
3
|
+
include DiscoApp::AuthenticatedController
|
|
4
|
+
|
|
5
|
+
skip_before_action :verify_status, only: [:create, :activate]
|
|
6
|
+
|
|
7
|
+
# Display a "pre-charge" page, giving the opportunity to explain why a charge needs to be made.
|
|
8
|
+
def new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Create a new charge for the currently logged in shop, then redirect to the charge's confirmation URL.
|
|
12
|
+
def create
|
|
13
|
+
if (shopify_charge = DiscoApp::ChargesService.create(@shop)).nil?
|
|
14
|
+
redirect_to action: :new and return
|
|
15
|
+
end
|
|
16
|
+
redirect_to shopify_charge.confirmation_url
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Attempt to activate a charge after a user has accepted or declined it. Redirect to the main application's root URL
|
|
20
|
+
# immediately afterwards - if the charge wasn't accepted, the flow will start again.
|
|
21
|
+
def activate
|
|
22
|
+
if (shopify_charge = DiscoApp::ChargesService.get_accepted_charge(@shop, params[:charge_id])).nil?
|
|
23
|
+
redirect_to action: :new and return
|
|
24
|
+
end
|
|
25
|
+
DiscoApp::ChargesService.activate(@shop, shopify_charge)
|
|
26
|
+
redirect_to main_app.root_url
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
class InstallController < ApplicationController
|
|
3
|
+
include DiscoApp::AuthenticatedController
|
|
4
|
+
|
|
5
|
+
# Start the installation process for the current shop, then redirect to the installing screen.
|
|
6
|
+
def install
|
|
7
|
+
::AppInstalledJob.perform_later(@shop.shopify_domain)
|
|
8
|
+
redirect_to action: :installing
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Display an "installing" page.
|
|
12
|
+
def installing
|
|
13
|
+
if @shop.installed?
|
|
14
|
+
redirect_to main_app.root_path
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Display an "uninstalling" page. Should be almost never used.
|
|
19
|
+
def uninstalling
|
|
20
|
+
if @shop.uninstalled?
|
|
21
|
+
redirect_to main_app.root_path
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
class WebhooksController < ActionController::Base
|
|
3
|
+
|
|
4
|
+
before_action :verify_webhook
|
|
5
|
+
|
|
6
|
+
def process_webhook
|
|
7
|
+
# Get the topic and domain for this webhook.
|
|
8
|
+
topic = request.headers['HTTP_X_SHOPIFY_TOPIC']
|
|
9
|
+
domain = request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN']
|
|
10
|
+
|
|
11
|
+
# Try to find a matching background job task for the given topic using class name.
|
|
12
|
+
begin
|
|
13
|
+
job_class = "#{topic}_job".gsub('/', '_').classify.constantize
|
|
14
|
+
rescue NameError
|
|
15
|
+
head :bad_request
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Decode the body data and enqueue the appropriate job.
|
|
19
|
+
data = ActiveSupport::JSON::decode(request.body.read)
|
|
20
|
+
job_class.perform_later(domain, data)
|
|
21
|
+
|
|
22
|
+
render nothing: true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Verify a webhook request.
|
|
28
|
+
def verify_webhook
|
|
29
|
+
data = request.body.read.to_s
|
|
30
|
+
hmac_header = request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']
|
|
31
|
+
digest = OpenSSL::Digest::Digest.new('sha256')
|
|
32
|
+
calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, ShopifyApp.configuration.secret, data)).strip
|
|
33
|
+
unless calculated_hmac == hmac_header
|
|
34
|
+
head :unauthorized
|
|
35
|
+
end
|
|
36
|
+
request.body.rewind
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
class AppInstalledJob < DiscoApp::ShopJob
|
|
3
|
+
|
|
4
|
+
before_enqueue { @shop.awaiting_install! }
|
|
5
|
+
before_perform { @shop.installing! }
|
|
6
|
+
after_perform { @shop.installed! }
|
|
7
|
+
|
|
8
|
+
def perform(domain)
|
|
9
|
+
|
|
10
|
+
# Install webhooks.
|
|
11
|
+
(base_webhook_topics + webhook_topics).each do |topic|
|
|
12
|
+
ShopifyAPI::Webhook.create(topic: topic, address: webhooks_url, format: 'json')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Perform initial update of shop information.
|
|
16
|
+
::ShopUpdateJob.perform_now(domain)
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
protected
|
|
21
|
+
|
|
22
|
+
# Return a list of additional webhook topics to listen for.
|
|
23
|
+
# This method should be overridden in the application.
|
|
24
|
+
def webhook_topics
|
|
25
|
+
[]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Return a list of webhook topics that will always be set up for the application.
|
|
31
|
+
def base_webhook_topics
|
|
32
|
+
[:'app/uninstalled', :'shop/update']
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Return the absolute URL to the webhooks endpoint.
|
|
36
|
+
def webhooks_url
|
|
37
|
+
DiscoApp::Engine.routes.url_helpers.webhooks_url
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
class AppUninstalledJob < DiscoApp::ShopJob
|
|
3
|
+
|
|
4
|
+
before_enqueue { @shop.awaiting_uninstall! }
|
|
5
|
+
before_perform { @shop.uninstalling! }
|
|
6
|
+
after_perform { @shop.uninstalled! }
|
|
7
|
+
|
|
8
|
+
def perform(domain, shop_data)
|
|
9
|
+
|
|
10
|
+
# Mark the shop's charge status as "cancelled" unless charges have been waived.
|
|
11
|
+
unless @shop.charge_waived?
|
|
12
|
+
@shop.charge_cancelled!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# The base class for all jobs that should be performed in the context of a particular Shop's API session. The first
|
|
2
|
+
# argument to any job inheriting from this class must be the domain of the relevant store, so that the appropriate
|
|
3
|
+
# Shop model can be fetched and the temporary API session created.
|
|
4
|
+
|
|
5
|
+
module DiscoApp
|
|
6
|
+
class ShopJob < ActiveJob::Base
|
|
7
|
+
|
|
8
|
+
queue_as :default
|
|
9
|
+
|
|
10
|
+
before_perform { |job| find_shop(job) }
|
|
11
|
+
before_enqueue { |job| find_shop(job) }
|
|
12
|
+
|
|
13
|
+
around_enqueue { |job, block| shop_context(job, block) }
|
|
14
|
+
around_perform { |job, block| shop_context(job, block) }
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def find_shop(job)
|
|
19
|
+
@shop ||= ::Shop.find_by!(shopify_domain: job.arguments.first)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def shop_context(job, block)
|
|
23
|
+
@shop.temp {
|
|
24
|
+
block.call(job.arguments)
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
class ShopUpdateJob < DiscoApp::ShopJob
|
|
3
|
+
|
|
4
|
+
def perform(domain, shop_data = nil)
|
|
5
|
+
# If we weren't provided with shop data (eg from a webhook), fetch it.
|
|
6
|
+
shop_data ||= ActiveSupport::JSON::decode(ShopifyAPI::Shop.current.to_json)
|
|
7
|
+
|
|
8
|
+
# Ensure we can access shop data through symbols.
|
|
9
|
+
shop_data = HashWithIndifferentAccess.new(shop_data)
|
|
10
|
+
|
|
11
|
+
# Update model attributes present in both our model and the data hash.
|
|
12
|
+
@shop.update_attributes(shop_data.except(:id, :created_at).slice(*::Shop.column_names))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
module Shop
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
# Include the base ShopifyApp functionality.
|
|
6
|
+
include ShopifyApp::Shop
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
# Define possible installation statuses as an enum.
|
|
10
|
+
enum status: [:never_installed, :awaiting_install, :installing, :installed, :awaiting_uninstall, :uninstalling, :uninstalled]
|
|
11
|
+
|
|
12
|
+
# Define possible charge statuses as an enum.
|
|
13
|
+
enum charge_status: [:charge_none, :charge_pending, :charge_accepted, :charge_declined, :charge_active, :charge_cancelled, :charge_waived]
|
|
14
|
+
|
|
15
|
+
# Alias 'with_shopify_session' as 'temp', as per our existing conventions.
|
|
16
|
+
alias_method :temp, :with_shopify_session
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Return a hash of attributes that should be used to create a new charge for this shop.
|
|
20
|
+
# This method can be overridden by the inheriting Shop class in order to provide charges
|
|
21
|
+
# customised to a particular shop. Otherwise, the default settings configured in application.rb
|
|
22
|
+
# will be used.
|
|
23
|
+
def new_charge_attributes
|
|
24
|
+
{
|
|
25
|
+
type: Rails.configuration.x.shopify_charges_default_type,
|
|
26
|
+
name: Rails.configuration.x.shopify_app_name,
|
|
27
|
+
price: Rails.configuration.x.shopify_charges_default_price,
|
|
28
|
+
trial_days: Rails.configuration.x.shopify_charges_default_trial_days,
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Update this Shop's charge_status attribute based on the given Shopify charge object.
|
|
33
|
+
def update_charge_status(shopify_charge)
|
|
34
|
+
status_update_method_name = "charge_#{shopify_charge.status}!"
|
|
35
|
+
self.public_send(status_update_method_name) if self.respond_to? status_update_method_name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module DiscoApp
|
|
2
|
+
class ChargesService
|
|
3
|
+
|
|
4
|
+
# Create a new charge for the given Shop using the Shopify API.
|
|
5
|
+
#
|
|
6
|
+
# The attributes of the charge are fetched using the shop's `new_charge_attributes` method, which can be overriden
|
|
7
|
+
# to provide custom charge types for individual shops.
|
|
8
|
+
#
|
|
9
|
+
# Returns the new Shopify charge model on success, nil otherwise.
|
|
10
|
+
def self.create(shop)
|
|
11
|
+
shopify_charge = shop.temp {
|
|
12
|
+
self.charge_api_class(shop).create(self.new_charge_attributes(shop))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# If the charge was successfully created, update the charge status on the shop.
|
|
16
|
+
shop.update_charge_status(shopify_charge) if shopify_charge
|
|
17
|
+
|
|
18
|
+
# Return the charge.
|
|
19
|
+
shopify_charge
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Fetch the specified charge for the given Shop using the Shopify API and check that it has been actioned (either
|
|
23
|
+
# accepted or declined). Updates the shop object's charge status, then returns the charge if it was accepted or
|
|
24
|
+
# nil otherwise.
|
|
25
|
+
def self.get_accepted_charge(shop, charge_id)
|
|
26
|
+
begin
|
|
27
|
+
shopify_charge = shop.temp {
|
|
28
|
+
self.charge_api_class(shop).find(charge_id)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# If the charge was successfully fetched, update the status for the shop accordingly.
|
|
32
|
+
shop.update_charge_status(shopify_charge) if shopify_charge
|
|
33
|
+
|
|
34
|
+
shopify_charge
|
|
35
|
+
rescue
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Attempt to activate the given Shopify charge for the given Shop using the Shopify API.
|
|
41
|
+
# Returns true on successful activation, false otherwise.
|
|
42
|
+
def self.activate(shop, shopify_charge)
|
|
43
|
+
begin
|
|
44
|
+
shop.temp {
|
|
45
|
+
shopify_charge.activate
|
|
46
|
+
}
|
|
47
|
+
shop.charge_active!
|
|
48
|
+
true
|
|
49
|
+
rescue
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Merge the new_charge_attributes returned by the given shop model and merge them with some application-level
|
|
55
|
+
# charge attributes.
|
|
56
|
+
def self.new_charge_attributes(shop)
|
|
57
|
+
shop.new_charge_attributes.merge(
|
|
58
|
+
return_url: DiscoApp::Engine.routes.url_helpers.activate_charge_url,
|
|
59
|
+
test: !Rails.configuration.x.shopify_charges_real,
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get the appropriate Shopify API class for the given shop (either ApplicationCharge or RecurringApplicationCharge).
|
|
64
|
+
def self.charge_api_class(shop)
|
|
65
|
+
if shop.new_charge_attributes[:type] == :one_time
|
|
66
|
+
ShopifyAPI::ApplicationCharge
|
|
67
|
+
else
|
|
68
|
+
ShopifyAPI::RecurringApplicationCharge
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
activate_charge
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
create_charge
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<% provide(:title, 'Thankyou') %>
|
|
2
|
+
|
|
3
|
+
<div class="row">
|
|
4
|
+
<% if @shop.charge_declined? %>
|
|
5
|
+
<div class="alert alert-warning">
|
|
6
|
+
<p>
|
|
7
|
+
Oops! Looks like you declined the charge.
|
|
8
|
+
Unfortunately, you'll have to accept the charge on the next screen in order to continue installing the application.
|
|
9
|
+
</p>
|
|
10
|
+
</div>
|
|
11
|
+
<% elsif @shop.charge_cancelled? %>
|
|
12
|
+
<div class="alert alert-warning">
|
|
13
|
+
<p>
|
|
14
|
+
Your authorized charge for this application has expired.
|
|
15
|
+
This could have occurred if:
|
|
16
|
+
</p>
|
|
17
|
+
<ul>
|
|
18
|
+
<li>You uninstalled and reinstalled the application; or</li>
|
|
19
|
+
<li>Your plan level has changed.</li>
|
|
20
|
+
</ul>
|
|
21
|
+
<p>
|
|
22
|
+
In either case, it's no problem!
|
|
23
|
+
Simply click okay and you'll be asked to authorize a new charge.
|
|
24
|
+
Don't worry - you *wont'* be billed twice.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
<% else %>
|
|
28
|
+
<div class="alert alert-success">
|
|
29
|
+
<p>
|
|
30
|
+
Thanks for installing <%= Rails.configuration.x.shopify_app_name %>!
|
|
31
|
+
</p>
|
|
32
|
+
<p>
|
|
33
|
+
Before we start setting things up, we need you to authorize a charge for the application.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="row">
|
|
40
|
+
<%= form_tag disco_app.create_charge_path, method: 'POST', target: '_parent' do %>
|
|
41
|
+
<div class="form-group">
|
|
42
|
+
<%= submit_tag 'Okay', class: 'form-input' %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uninstalling
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
404 Not Found
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
DiscoApp::Engine.routes.draw do
|
|
2
|
+
|
|
3
|
+
controller :webhooks do
|
|
4
|
+
post 'webhooks' => :process_webhook, as: :webhooks
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
controller :charges do
|
|
8
|
+
get 'charges/new' => :new, as: :new_charge
|
|
9
|
+
post 'charges/create' => :create, as: :create_charge
|
|
10
|
+
get 'charges/activate' => :activate, as: :activate_charge
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
controller :install do
|
|
14
|
+
get 'install' => :install, as: :install
|
|
15
|
+
get 'installing' => :installing, as: :installing
|
|
16
|
+
get 'uninstalling' => :uninstalling, as: :uninstalling
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class AddMetaToShops < ActiveRecord::Migration
|
|
2
|
+
def change
|
|
3
|
+
add_column :shops, :email, :string
|
|
4
|
+
add_column :shops, :country_name, :string
|
|
5
|
+
add_column :shops, :currency, :string
|
|
6
|
+
add_column :shops, :money_format, :string
|
|
7
|
+
add_column :shops, :money_with_currency_format, :string
|
|
8
|
+
add_column :shops, :domain, :string
|
|
9
|
+
add_column :shops, :plan_name, :string
|
|
10
|
+
end
|
|
11
|
+
end
|