disco_app 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +37 -0
  3. data/app/assets/javascripts/disco_app/disco_app.js +7 -0
  4. data/app/assets/stylesheets/disco_app/bootstrap-custom.scss +55 -0
  5. data/app/assets/stylesheets/disco_app/bootstrap-variables.scss +12 -0
  6. data/app/assets/stylesheets/disco_app/disco_app.scss +7 -0
  7. data/app/controllers/disco_app/app_proxy_controller.rb +41 -0
  8. data/app/controllers/disco_app/authenticated_controller.rb +44 -0
  9. data/app/controllers/disco_app/carrier_request_controller.rb +28 -0
  10. data/app/controllers/disco_app/charges_controller.rb +30 -0
  11. data/app/controllers/disco_app/install_controller.rb +26 -0
  12. data/app/controllers/disco_app/webhooks_controller.rb +40 -0
  13. data/app/helpers/disco_app/application_helper.rb +4 -0
  14. data/app/jobs/disco_app/app_installed_job.rb +41 -0
  15. data/app/jobs/disco_app/app_uninstalled_job.rb +18 -0
  16. data/app/jobs/disco_app/shop_job.rb +29 -0
  17. data/app/jobs/disco_app/shop_update_job.rb +16 -0
  18. data/app/models/disco_app/shop.rb +44 -0
  19. data/app/services/disco_app/charges_service.rb +73 -0
  20. data/app/views/disco_app/charges/activate.html.erb +1 -0
  21. data/app/views/disco_app/charges/create.html.erb +1 -0
  22. data/app/views/disco_app/charges/new.html.erb +45 -0
  23. data/app/views/disco_app/install/installing.html.erb +7 -0
  24. data/app/views/disco_app/install/uninstalling.html.erb +1 -0
  25. data/app/views/disco_app/proxy_errors/404.html.erb +1 -0
  26. data/config/routes.rb +19 -0
  27. data/db/migrate/20150525162112_add_status_to_shops.rb +5 -0
  28. data/db/migrate/20150525171422_add_meta_to_shops.rb +11 -0
  29. data/db/migrate/20150629210346_add_charge_status_to_shop.rb +5 -0
  30. data/db/migrate/20150814214025_add_more_meta_to_shops.rb +16 -0
  31. data/lib/disco_app/engine.rb +6 -0
  32. data/lib/disco_app/version.rb +3 -0
  33. data/lib/disco_app.rb +4 -0
  34. data/lib/generators/disco_app/USAGE +5 -0
  35. data/lib/generators/disco_app/disco_app_generator.rb +182 -0
  36. data/lib/generators/disco_app/reactify/reactify_generator.rb +31 -0
  37. data/lib/generators/disco_app/templates/assets/javascripts/application.js +17 -0
  38. data/lib/generators/disco_app/templates/assets/stylesheets/application.scss +5 -0
  39. data/lib/generators/disco_app/templates/config/puma.rb +15 -0
  40. data/lib/generators/disco_app/templates/controllers/home_controller.rb +7 -0
  41. data/lib/generators/disco_app/templates/initializers/disco_app.rb +1 -0
  42. data/lib/generators/disco_app/templates/initializers/shopify_app.rb +6 -0
  43. data/lib/generators/disco_app/templates/jobs/app_installed_job.rb +2 -0
  44. data/lib/generators/disco_app/templates/jobs/app_uninstalled_job.rb +2 -0
  45. data/lib/generators/disco_app/templates/jobs/shop_update_job.rb +2 -0
  46. data/lib/generators/disco_app/templates/models/shop.rb +3 -0
  47. data/lib/generators/disco_app/templates/root/Procfile +2 -0
  48. data/lib/generators/disco_app/templates/views/home/index.html.erb +2 -0
  49. data/lib/generators/disco_app/templates/views/layouts/application.html.erb +18 -0
  50. data/lib/generators/disco_app/templates/views/layouts/embedded_app.html.erb +33 -0
  51. data/lib/generators/disco_app/templates/views/sessions/new.html.erb +26 -0
  52. data/test/disco_app_test.rb +7 -0
  53. data/test/dummy/README.rdoc +28 -0
  54. data/test/dummy/Rakefile +6 -0
  55. data/test/dummy/app/assets/javascripts/application.js +13 -0
  56. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  57. data/test/dummy/app/controllers/application_controller.rb +5 -0
  58. data/test/dummy/app/helpers/application_helper.rb +2 -0
  59. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  60. data/test/dummy/bin/bundle +3 -0
  61. data/test/dummy/bin/rails +4 -0
  62. data/test/dummy/bin/rake +4 -0
  63. data/test/dummy/bin/setup +29 -0
  64. data/test/dummy/config/application.rb +26 -0
  65. data/test/dummy/config/boot.rb +5 -0
  66. data/test/dummy/config/database.yml +25 -0
  67. data/test/dummy/config/environment.rb +5 -0
  68. data/test/dummy/config/environments/development.rb +41 -0
  69. data/test/dummy/config/environments/production.rb +79 -0
  70. data/test/dummy/config/environments/test.rb +42 -0
  71. data/test/dummy/config/initializers/assets.rb +11 -0
  72. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  73. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  74. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  75. data/test/dummy/config/initializers/inflections.rb +16 -0
  76. data/test/dummy/config/initializers/mime_types.rb +4 -0
  77. data/test/dummy/config/initializers/session_store.rb +3 -0
  78. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  79. data/test/dummy/config/locales/en.yml +23 -0
  80. data/test/dummy/config/routes.rb +4 -0
  81. data/test/dummy/config/secrets.yml +22 -0
  82. data/test/dummy/config.ru +4 -0
  83. data/test/dummy/public/404.html +67 -0
  84. data/test/dummy/public/422.html +67 -0
  85. data/test/dummy/public/500.html +66 -0
  86. data/test/dummy/public/favicon.ico +0 -0
  87. data/test/integration/navigation_test.rb +10 -0
  88. data/test/lib/generators/disco_app/disco_app_generator_test.rb +16 -0
  89. data/test/test_helper.rb +20 -0
  90. metadata +338 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e94f1a57f95b758fd65f751679875a60ca89e1ccd211c3ae4223e12b3962d33b
4
+ data.tar.gz: e60095bae476ae30411b7dbc346075ab9bfc6edd4a89e971b40549550094add0
5
+ SHA512:
6
+ metadata.gz: 63d9d433da4bead908a2677d6edc22f71611132c775695bd3b15228d5291ff57a3f61594792df40dd05151a38c2e9b2f88faeac4c75d11b4fbe906c63967709f
7
+ data.tar.gz: 322af952fb19124ffdd22af1ed022ac8afd8c99133247ce863406320ca6b7e822d00e24c83de735ed789296b5dbc7c4a6434268feaebf2ca4efd3e1d8b9d6e8b
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,7 @@
1
+ /**
2
+ * disco_app/disco_app.js
3
+ * Base Javascript for Disco applications.
4
+ * Assumes that jQuery will be loaded in the parent application.js.
5
+ */
6
+ //= require bootstrap-sprockets
7
+ //= require_tree .
@@ -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,7 @@
1
+ /*!
2
+ * disco_app/disco_app.scss
3
+ * Base styles for Disco applications.
4
+ */
5
+ @import 'bootstrap-sprockets';
6
+ @import 'bootstrap-variables';
7
+ @import 'bootstrap-custom';
@@ -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,28 @@
1
+ module DiscoApp
2
+ module CarrierRequestController
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_action :verify_carrier_request_signature
7
+ end
8
+
9
+ private
10
+
11
+ def verify_carrier_request_signature
12
+ unless carrier_request_signature_is_valid?
13
+ head :unauthorized
14
+ end
15
+ end
16
+
17
+ def carrier_request_signature_is_valid?
18
+ return true unless Rails.env.production?
19
+ data = request.body.read.to_s
20
+ hmac_header = request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']
21
+ digest = OpenSSL::Digest::Digest.new('sha256')
22
+ calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, ShopifyApp.configuration.secret, data)).strip
23
+ request.body.rewind
24
+ calculated_hmac == hmac_header
25
+ end
26
+
27
+ end
28
+ 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,4 @@
1
+ module DiscoApp
2
+ module ApplicationHelper
3
+ end
4
+ 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,44 @@
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
+ # Define some useful scopes.
16
+ scope :status, -> (status) { where status: status }
17
+ scope :installed, -> { where status: ShopifySession.statuses[:installed] }
18
+ scope :has_active_shopify_plan, -> { where.not(plan_name: [:cancelled, :frozen]) }
19
+
20
+ # Alias 'with_shopify_session' as 'temp', as per our existing conventions.
21
+ alias_method :temp, :with_shopify_session
22
+ end
23
+
24
+ # Return a hash of attributes that should be used to create a new charge for this shop.
25
+ # This method can be overridden by the inheriting Shop class in order to provide charges
26
+ # customised to a particular shop. Otherwise, the default settings configured in application.rb
27
+ # will be used.
28
+ def new_charge_attributes
29
+ {
30
+ type: Rails.configuration.x.shopify_charges_default_type,
31
+ name: Rails.configuration.x.shopify_app_name,
32
+ price: Rails.configuration.x.shopify_charges_default_price,
33
+ trial_days: Rails.configuration.x.shopify_charges_default_trial_days,
34
+ }
35
+ end
36
+
37
+ # Update this Shop's charge_status attribute based on the given Shopify charge object.
38
+ def update_charge_status(shopify_charge)
39
+ status_update_method_name = "charge_#{shopify_charge.status}!"
40
+ self.public_send(status_update_method_name) if self.respond_to? status_update_method_name
41
+ end
42
+
43
+ end
44
+ 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,7 @@
1
+ <% content_for :extra_head do %>
2
+ <meta http-equiv="refresh" content="5">
3
+ <% end %>
4
+
5
+ <p>
6
+ Installing, please wait...
7
+ </p>
@@ -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,5 @@
1
+ class AddStatusToShops < ActiveRecord::Migration
2
+ def change
3
+ add_column :shops, :status, :integer, default: 0
4
+ end
5
+ end