disco_app 0.8.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +37 -0
- data/app/assets/images/disco_app/icon.svg +1 -0
- data/app/assets/javascripts/disco_app/components/filterable_shop_list.js.jsx +60 -0
- data/app/assets/javascripts/disco_app/components/shop_filter_tab.js.jsx +34 -0
- data/app/assets/javascripts/disco_app/components/shop_filter_tabs.js.jsx +21 -0
- data/app/assets/javascripts/disco_app/components/shop_list.js.jsx +144 -0
- data/app/assets/javascripts/disco_app/components/shop_row.js.jsx +27 -0
- data/app/assets/javascripts/disco_app/components/shopify_admin_link.js.jsx +29 -0
- data/app/assets/javascripts/disco_app/components.js +5 -0
- data/app/assets/javascripts/disco_app/disco_app.js +7 -0
- data/app/assets/javascripts/disco_app/frame.js +152 -0
- data/app/assets/javascripts/disco_app/shopify-turbolinks.js +7 -0
- data/app/assets/stylesheets/disco_app/bootstrap/_custom.scss +54 -0
- data/app/assets/stylesheets/disco_app/bootstrap/_variables.scss +872 -0
- data/app/assets/stylesheets/disco_app/disco/_buttons.scss +31 -0
- data/app/assets/stylesheets/disco_app/disco/_cards.scss +52 -0
- data/app/assets/stylesheets/disco_app/disco/_forms.scss +23 -0
- data/app/assets/stylesheets/disco_app/disco/_grid.scss +58 -0
- data/app/assets/stylesheets/disco_app/disco/_sections.scss +61 -0
- data/app/assets/stylesheets/disco_app/disco/_tables.scss +57 -0
- data/app/assets/stylesheets/disco_app/disco/_tabs.scss +61 -0
- data/app/assets/stylesheets/disco_app/disco/_type.scss +39 -0
- data/app/assets/stylesheets/disco_app/disco/mixins/_flexbox.scss +394 -0
- data/app/assets/stylesheets/disco_app/disco_app.scss +16 -0
- data/app/assets/stylesheets/disco_app/frame/_buttons.scss +54 -0
- data/app/assets/stylesheets/disco_app/frame/_forms.scss +26 -0
- data/app/assets/stylesheets/disco_app/frame/_layout.scss +77 -0
- data/app/assets/stylesheets/disco_app/frame/_type.scss +32 -0
- data/app/assets/stylesheets/disco_app/frame.scss +9 -0
- data/app/controllers/disco_app/admin/app_settings_controller.rb +3 -0
- data/app/controllers/disco_app/admin/application_controller.rb +3 -0
- data/app/controllers/disco_app/admin/concerns/app_settings_controller.rb +24 -0
- data/app/controllers/disco_app/admin/concerns/authenticated_controller.rb +20 -0
- data/app/controllers/disco_app/admin/concerns/plans_controller.rb +51 -0
- data/app/controllers/disco_app/admin/concerns/shops_controller.rb +7 -0
- data/app/controllers/disco_app/admin/plans_controller.rb +3 -0
- data/app/controllers/disco_app/admin/resources/shops_controller.rb +3 -0
- data/app/controllers/disco_app/admin/shops_controller.rb +3 -0
- data/app/controllers/disco_app/charges_controller.rb +47 -0
- data/app/controllers/disco_app/concerns/app_proxy_controller.rb +40 -0
- data/app/controllers/disco_app/concerns/authenticated_controller.rb +56 -0
- data/app/controllers/disco_app/concerns/carrier_request_controller.rb +21 -0
- data/app/controllers/disco_app/frame_controller.rb +9 -0
- data/app/controllers/disco_app/install_controller.rb +27 -0
- data/app/controllers/disco_app/subscriptions_controller.rb +32 -0
- data/app/controllers/disco_app/webhooks_controller.rb +46 -0
- data/app/controllers/sessions_controller.rb +28 -0
- data/app/helpers/disco_app/application_helper.rb +28 -0
- data/app/jobs/disco_app/app_installed_job.rb +3 -0
- data/app/jobs/disco_app/app_uninstalled_job.rb +3 -0
- data/app/jobs/disco_app/concerns/app_installed_job.rb +39 -0
- data/app/jobs/disco_app/concerns/app_uninstalled_job.rb +20 -0
- data/app/jobs/disco_app/concerns/shop_update_job.rb +16 -0
- data/app/jobs/disco_app/concerns/subscription_changed_job.rb +7 -0
- data/app/jobs/disco_app/concerns/synchronise_carrier_service_job.rb +52 -0
- data/app/jobs/disco_app/concerns/synchronise_webhooks_job.rb +61 -0
- data/app/jobs/disco_app/shop_job.rb +27 -0
- data/app/jobs/disco_app/shop_update_job.rb +3 -0
- data/app/jobs/disco_app/subscription_changed_job.rb +3 -0
- data/app/jobs/disco_app/synchronise_carrier_service_job.rb +3 -0
- data/app/jobs/disco_app/synchronise_webhooks_job.rb +3 -0
- data/app/models/disco_app/app_settings.rb +3 -0
- data/app/models/disco_app/application_charge.rb +18 -0
- data/app/models/disco_app/concerns/app_settings.rb +7 -0
- data/app/models/disco_app/concerns/plan.rb +26 -0
- data/app/models/disco_app/concerns/plan_code.rb +15 -0
- data/app/models/disco_app/concerns/shop.rb +80 -0
- data/app/models/disco_app/concerns/subscription.rb +48 -0
- data/app/models/disco_app/concerns/synchronises.rb +39 -0
- data/app/models/disco_app/plan.rb +3 -0
- data/app/models/disco_app/plan_code.rb +3 -0
- data/app/models/disco_app/recurring_application_charge.rb +18 -0
- data/app/models/disco_app/session_storage.rb +18 -0
- data/app/models/disco_app/shop.rb +3 -0
- data/app/models/disco_app/subscription.rb +3 -0
- data/app/resources/disco_app/admin/resources/concerns/shop_resource.rb +45 -0
- data/app/resources/disco_app/admin/resources/shop_resource.rb +4 -0
- data/app/services/disco_app/carrier_request_service.rb +15 -0
- data/app/services/disco_app/charges_service.rb +81 -0
- data/app/services/disco_app/proxy_service.rb +17 -0
- data/app/services/disco_app/subscription_service.rb +46 -0
- data/app/services/disco_app/webhook_service.rb +30 -0
- data/app/views/disco_app/admin/app_settings/edit.html.erb +5 -0
- data/app/views/disco_app/admin/plans/_form.html.erb +75 -0
- data/app/views/disco_app/admin/plans/edit.html.erb +7 -0
- data/app/views/disco_app/admin/plans/index.html.erb +41 -0
- data/app/views/disco_app/admin/plans/new.html.erb +7 -0
- data/app/views/disco_app/admin/shops/index.html.erb +12 -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 +12 -0
- data/app/views/disco_app/frame/frame.html.erb +36 -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/app/views/disco_app/shared/_card.html.erb +14 -0
- data/app/views/disco_app/shared/_section.html.erb +17 -0
- data/app/views/disco_app/subscriptions/new.html.erb +25 -0
- data/app/views/layouts/admin/_navbar.html.erb +25 -0
- data/app/views/layouts/admin.html.erb +23 -0
- data/app/views/layouts/application.html.erb +18 -0
- data/app/views/layouts/embedded_app.html.erb +41 -0
- data/app/views/layouts/embedded_app_modal.html.erb +17 -0
- data/app/views/sessions/new.html.erb +26 -0
- data/config/routes.rb +46 -0
- data/db/migrate/20150525000000_create_shops_if_not_existent.rb +15 -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/db/migrate/20150814214025_add_more_meta_to_shops.rb +15 -0
- data/db/migrate/20151017231302_create_disco_app_plans.rb +13 -0
- data/db/migrate/20151017232027_create_disco_app_subscriptions.rb +15 -0
- data/db/migrate/20151017234409_move_shop_to_disco_app_engine.rb +5 -0
- data/db/migrate/20160112233706_create_disco_app_sessions.rb +12 -0
- data/db/migrate/20160113194418_add_shop_id_to_disco_app_sessions.rb +6 -0
- data/db/migrate/20160223111044_create_disco_app_settings.rb +8 -0
- data/db/migrate/20160301223215_update_plans.rb +22 -0
- data/db/migrate/20160301224558_update_subscriptions.rb +13 -0
- data/db/migrate/20160302104816_create_disco_app_recurring_application_charges.rb +14 -0
- data/db/migrate/20160302105259_create_disco_app_application_charges.rb +14 -0
- data/db/migrate/20160302134728_drop_charge_status_from_shops.rb +5 -0
- data/db/migrate/20160302142941_add_shopify_attributes_to_charges.rb +8 -0
- data/db/migrate/20160331093148_create_disco_app_plan_codes.rb +14 -0
- data/db/migrate/20160401044420_add_status_to_plan_codes.rb +5 -0
- data/db/migrate/20160401045551_add_amount_and_plan_code_to_disco_app_subscriptions.rb +7 -0
- data/db/migrate/20160425205211_add_source_to_disco_app_subscriptions.rb +5 -0
- data/db/migrate/20160426033520_add_trial_period_days_to_disco_app_subscriptions.rb +5 -0
- data/lib/disco_app/configuration.rb +39 -0
- data/lib/disco_app/constants.rb +4 -0
- data/lib/disco_app/engine.rb +26 -0
- data/lib/disco_app/session.rb +14 -0
- data/lib/disco_app/support/file_fixtures.rb +23 -0
- data/lib/disco_app/test_help.rb +11 -0
- data/lib/disco_app/version.rb +3 -0
- data/lib/disco_app.rb +7 -0
- data/lib/generators/disco_app/USAGE +5 -0
- data/lib/generators/disco_app/adminify/adminify_generator.rb +35 -0
- data/lib/generators/disco_app/disco_app_generator.rb +164 -0
- data/lib/generators/disco_app/mailify/mailify_generator.rb +54 -0
- data/lib/generators/disco_app/monitorify/monitorify_generator.rb +28 -0
- data/lib/generators/disco_app/monitorify/templates/config/newrelic.yml +26 -0
- data/lib/generators/disco_app/monitorify/templates/initializers/rollbar.rb +12 -0
- data/lib/generators/disco_app/reactify/reactify_generator.rb +45 -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 +19 -0
- data/lib/generators/disco_app/templates/initializers/session_store.rb +2 -0
- data/lib/generators/disco_app/templates/initializers/shopify_app.rb +7 -0
- data/lib/generators/disco_app/templates/initializers/shopify_session_repository.rb +7 -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/tasks/carrier_service.rake +10 -0
- data/lib/tasks/sessions.rake +9 -0
- data/lib/tasks/start.rake +3 -0
- data/lib/tasks/webhooks.rake +10 -0
- data/test/controllers/disco_app/admin/shops_controller_test.rb +54 -0
- data/test/controllers/disco_app/charges_controller_test.rb +92 -0
- data/test/controllers/disco_app/install_controller_test.rb +50 -0
- data/test/controllers/disco_app/subscriptions_controller_test.rb +68 -0
- data/test/controllers/disco_app/webhooks_controller_test.rb +58 -0
- data/test/controllers/home_controller_test.rb +92 -0
- data/test/controllers/proxy_controller_test.rb +42 -0
- data/test/disco_app_test.rb +7 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +17 -0
- data/test/dummy/app/assets/stylesheets/application.scss +5 -0
- data/test/dummy/app/controllers/application_controller.rb +6 -0
- data/test/dummy/app/controllers/disco_app/admin/shops_controller.rb +8 -0
- data/test/dummy/app/controllers/home_controller.rb +7 -0
- data/test/dummy/app/controllers/proxy_controller.rb +8 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/jobs/disco_app/app_installed_job.rb +16 -0
- data/test/dummy/app/jobs/disco_app/app_uninstalled_job.rb +11 -0
- data/test/dummy/app/jobs/products_create_job.rb +7 -0
- data/test/dummy/app/jobs/products_delete_job.rb +7 -0
- data/test/dummy/app/jobs/products_update_job.rb +7 -0
- data/test/dummy/app/models/disco_app/shop.rb +15 -0
- data/test/dummy/app/models/product.rb +6 -0
- data/test/dummy/app/views/home/index.html.erb +2 -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 +38 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.codeship.yml +23 -0
- data/test/dummy/config/database.yml +20 -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 +85 -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/disco_app.rb +19 -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/omniauth.rb +9 -0
- data/test/dummy/config/initializers/session_store.rb +2 -0
- data/test/dummy/config/initializers/shopify_app.rb +7 -0
- data/test/dummy/config/initializers/shopify_session_repository.rb +7 -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 +10 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/migrate/20160307182229_create_products.rb +11 -0
- data/test/dummy/db/schema.rb +140 -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/fixtures/api/widget_store/charges/activate_application_charge_request.json +16 -0
- data/test/fixtures/api/widget_store/charges/activate_application_charge_response.json +1 -0
- data/test/fixtures/api/widget_store/charges/activate_recurring_application_charge_request.json +20 -0
- data/test/fixtures/api/widget_store/charges/activate_recurring_application_charge_response.json +1 -0
- data/test/fixtures/api/widget_store/charges/create_application_charge_request.json +9 -0
- data/test/fixtures/api/widget_store/charges/create_application_charge_response.json +16 -0
- data/test/fixtures/api/widget_store/charges/create_recurring_application_charge_request.json +9 -0
- data/test/fixtures/api/widget_store/charges/create_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/charges/create_second_recurring_application_charge_request.json +9 -0
- data/test/fixtures/api/widget_store/charges/create_second_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/charges/get_accepted_application_charge_response.json +16 -0
- data/test/fixtures/api/widget_store/charges/get_accepted_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/charges/get_declined_application_charge_response.json +16 -0
- data/test/fixtures/api/widget_store/charges/get_declined_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/charges/get_pending_application_charge_response.json +16 -0
- data/test/fixtures/api/widget_store/charges/get_pending_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/shop.json +46 -0
- data/test/fixtures/api/widget_store/webhooks.json +1 -0
- data/test/fixtures/disco_app/application_charges.yml +11 -0
- data/test/fixtures/disco_app/plan_codes.yml +13 -0
- data/test/fixtures/disco_app/plans.yml +37 -0
- data/test/fixtures/disco_app/recurring_application_charges.yml +11 -0
- data/test/fixtures/disco_app/shops.yml +10 -0
- data/test/fixtures/disco_app/subscriptions.yml +21 -0
- data/test/fixtures/products.yml +4 -0
- data/test/fixtures/webhooks/app_uninstalled.json +46 -0
- data/test/fixtures/webhooks/product_created.json +167 -0
- data/test/fixtures/webhooks/product_deleted.json +3 -0
- data/test/fixtures/webhooks/product_updated.json +167 -0
- data/test/integration/synchronises_test.rb +55 -0
- data/test/jobs/disco_app/app_installed_job_test.rb +55 -0
- data/test/jobs/disco_app/app_uninstalled_job_test.rb +30 -0
- data/test/models/disco_app/plan_test.rb +5 -0
- data/test/models/disco_app/session_test.rb +31 -0
- data/test/models/disco_app/shop_test.rb +27 -0
- data/test/services/disco_app/charges_service_test.rb +105 -0
- data/test/services/disco_app/subscription_service_test.rb +60 -0
- data/test/support/test_file_fixtures.rb +29 -0
- data/test/support/test_shopify_api.rb +16 -0
- data/test/test_helper.rb +52 -0
- metadata +663 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module DiscoApp::Admin::Concerns::PlansController
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action :find_plan, only: [:edit, :update]
|
6
|
+
end
|
7
|
+
|
8
|
+
def index
|
9
|
+
@plans = DiscoApp::Plan.all
|
10
|
+
end
|
11
|
+
|
12
|
+
def new
|
13
|
+
@plan = DiscoApp::Plan.new
|
14
|
+
@plan.plan_codes.build
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
@plan = DiscoApp::Plan.new(plan_params)
|
19
|
+
if @plan.save
|
20
|
+
redirect_to admin_plans_path
|
21
|
+
else
|
22
|
+
render 'new'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def edit
|
27
|
+
@plan.plan_codes.build
|
28
|
+
end
|
29
|
+
|
30
|
+
def update
|
31
|
+
if @plan.update_attributes(plan_params)
|
32
|
+
redirect_to admin_plans_path
|
33
|
+
else
|
34
|
+
render 'edit'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def find_plan
|
41
|
+
@plan = DiscoApp::Plan.find(params[:id])
|
42
|
+
end
|
43
|
+
|
44
|
+
def plan_params
|
45
|
+
params.require(:plan).permit(
|
46
|
+
:name, :status, :plan_type, :trial_period_days, :amount,
|
47
|
+
:plan_codes_attributes => [:code, :trial_period_days, :amount]
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class DiscoApp::ChargesController < ApplicationController
|
2
|
+
include DiscoApp::Concerns::AuthenticatedController
|
3
|
+
|
4
|
+
skip_before_action :check_active_charge
|
5
|
+
before_action :find_subscription
|
6
|
+
|
7
|
+
# Display a "pre-charge" page, giving the opportunity to explain why a charge
|
8
|
+
# needs to be made.
|
9
|
+
def new
|
10
|
+
end
|
11
|
+
|
12
|
+
# Attempt to create a new charge for the logged in shop and selected
|
13
|
+
# subscription. If successful, redirect to the (external) charge confirmation
|
14
|
+
# URL. If it fails, redirect back to the new charge page.
|
15
|
+
def create
|
16
|
+
if(charge = DiscoApp::ChargesService.create(@shop, @subscription)).nil?
|
17
|
+
redirect_to action: :new
|
18
|
+
else
|
19
|
+
redirect_to charge.confirmation_url
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Attempt to activate a charge after a user has accepted or declined it.
|
24
|
+
# Redirect to the main application's root URL immediately afterwards - if the
|
25
|
+
# charge wasn't accepted, the flow will start again.
|
26
|
+
def activate
|
27
|
+
# First attempt to find a matching charge.
|
28
|
+
if(charge = @subscription.charges.find_by(id: params[:id], shopify_id: params[:charge_id])).nil?
|
29
|
+
redirect_to action: :new and return
|
30
|
+
end
|
31
|
+
if DiscoApp::ChargesService.activate(@shop, charge)
|
32
|
+
redirect_to main_app.root_url
|
33
|
+
else
|
34
|
+
redirect_to action: :new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def find_subscription
|
41
|
+
@subscription = @shop.subscriptions.find_by_id!(params[:subscription_id])
|
42
|
+
unless @subscription.requires_active_charge? and not @subscription.active_charge?
|
43
|
+
redirect_to main_app.root_url
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module DiscoApp::Concerns::AppProxyController
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action :verify_proxy_signature
|
6
|
+
before_action :shopify_shop
|
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 if Rails.env.development? and DiscoApp.configuration.skip_proxy_verification?
|
24
|
+
DiscoApp::ProxyService.proxy_signature_is_valid?(request.query_string, ShopifyApp.configuration.secret)
|
25
|
+
end
|
26
|
+
|
27
|
+
def shopify_shop
|
28
|
+
@shop = DiscoApp::Shop.find_by_shopify_domain!(params[:shop])
|
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
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module DiscoApp::Concerns::AuthenticatedController
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action :login_again_if_different_shop
|
6
|
+
before_action :shopify_shop
|
7
|
+
before_action :check_installed
|
8
|
+
before_action :check_current_subscription
|
9
|
+
before_action :check_active_charge
|
10
|
+
around_filter :shopify_session
|
11
|
+
layout 'embedded_app'
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def shopify_shop
|
17
|
+
if shop_session
|
18
|
+
@shop = DiscoApp::Shop.find_by!(shopify_domain: @shop_session.url)
|
19
|
+
else
|
20
|
+
redirect_to_login
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def check_installed
|
25
|
+
if @shop.awaiting_install? or @shop.installing?
|
26
|
+
redirect_if_not_current_path disco_app.installing_path
|
27
|
+
return
|
28
|
+
end
|
29
|
+
if @shop.awaiting_uninstall? or @shop.uninstalling?
|
30
|
+
redirect_if_not_current_path disco_app.uninstalling_path
|
31
|
+
return
|
32
|
+
end
|
33
|
+
unless @shop.installed?
|
34
|
+
redirect_if_not_current_path disco_app.install_path
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_current_subscription
|
39
|
+
unless @shop.current_subscription?
|
40
|
+
redirect_if_not_current_path disco_app.new_subscription_path
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_active_charge
|
45
|
+
if @shop.current_subscription? and @shop.current_subscription.requires_active_charge? and not @shop.current_subscription.active_charge?
|
46
|
+
redirect_if_not_current_path disco_app.new_subscription_charge_path(@shop.current_subscription)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def redirect_if_not_current_path(target)
|
51
|
+
if request.path != target
|
52
|
+
redirect_to target
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module DiscoApp::Concerns::CarrierRequestController
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action :verify_carrier_request
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def verify_carrier_request
|
11
|
+
unless carrier_request_signature_is_valid?
|
12
|
+
head :unauthorized
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def carrier_request_signature_is_valid?
|
17
|
+
return true if Rails.env.development? and DiscoApp.configuration.skip_carrier_request_verification?
|
18
|
+
DiscoApp::CarrierRequestService.is_valid_hmac?(request.body.read.to_s, ShopifyApp.configuration.secret, request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'])
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class DiscoApp::InstallController < ApplicationController
|
2
|
+
include DiscoApp::Concerns::AuthenticatedController
|
3
|
+
|
4
|
+
skip_before_action :check_current_subscription
|
5
|
+
skip_before_action :check_active_charge
|
6
|
+
|
7
|
+
# Start the installation process for the current shop, then redirect to the installing screen.
|
8
|
+
def install
|
9
|
+
DiscoApp::AppInstalledJob.perform_later(@shop.shopify_domain, cookies[DiscoApp::CODE_COOKIE_KEY], cookies[DiscoApp::SOURCE_COOKIE_KEY])
|
10
|
+
redirect_to action: :installing
|
11
|
+
end
|
12
|
+
|
13
|
+
# Display an "installing" page.
|
14
|
+
def installing
|
15
|
+
if @shop.installed?
|
16
|
+
redirect_to main_app.root_path
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Display an "uninstalling" page. Should be almost never used.
|
21
|
+
def uninstalling
|
22
|
+
if @shop.uninstalled?
|
23
|
+
redirect_to main_app.root_path
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class DiscoApp::SubscriptionsController < ApplicationController
|
2
|
+
include DiscoApp::Concerns::AuthenticatedController
|
3
|
+
|
4
|
+
skip_before_action :check_current_subscription
|
5
|
+
|
6
|
+
def new
|
7
|
+
@subscription = DiscoApp::Subscription.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def create
|
11
|
+
# Get the selected plan. If it's not available or couldn't be found,
|
12
|
+
# redirect back to the plan selection page.
|
13
|
+
if(plan = DiscoApp::Plan.available.find_by_id(subscription_params[:plan])).nil?
|
14
|
+
redirect_to action: :new and return
|
15
|
+
end
|
16
|
+
|
17
|
+
# Subscribe the current shop to the selected plan. Pass along any cookied
|
18
|
+
# plan code and source code.
|
19
|
+
if(subscription = DiscoApp::SubscriptionService.subscribe(@shop, plan, cookies[DiscoApp::CODE_COOKIE_KEY], cookies[DiscoApp::SOURCE_COOKIE_KEY])).nil?
|
20
|
+
redirect_to action: :new
|
21
|
+
else
|
22
|
+
redirect_to main_app.root_path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def subscription_params
|
29
|
+
params.require(:subscription).permit(:plan, :plan_code)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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
|
+
# Ensure a domain was provided in the headers.
|
12
|
+
unless domain
|
13
|
+
head :bad_request
|
14
|
+
end
|
15
|
+
|
16
|
+
# Try to find a matching background job task for the given topic using class name.
|
17
|
+
job_class = DiscoApp::WebhookService.find_job_class(topic)
|
18
|
+
|
19
|
+
# Return bad request if we couldn't match a job class.
|
20
|
+
unless job_class.present?
|
21
|
+
head :bad_request
|
22
|
+
end
|
23
|
+
|
24
|
+
# Decode the body data and enqueue the appropriate job.
|
25
|
+
data = ActiveSupport::JSON::decode(request.body.read).with_indifferent_access
|
26
|
+
job_class.perform_later(domain, data)
|
27
|
+
|
28
|
+
render nothing: true
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def verify_webhook
|
34
|
+
unless webhook_is_valid?
|
35
|
+
head :unauthorized
|
36
|
+
end
|
37
|
+
request.body.rewind
|
38
|
+
end
|
39
|
+
|
40
|
+
def webhook_is_valid?
|
41
|
+
return true if Rails.env.development? and DiscoApp.configuration.skip_webhook_verification?
|
42
|
+
DiscoApp::WebhookService.is_valid_hmac?(request.body.read.to_s, ShopifyApp.configuration.secret, request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'])
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class SessionsController < ApplicationController
|
2
|
+
include ShopifyApp::SessionsController
|
3
|
+
|
4
|
+
def referral
|
5
|
+
cookies[DiscoApp::SOURCE_COOKIE_KEY] = params[:source] if params[:source].present?
|
6
|
+
cookies[DiscoApp::CODE_COOKIE_KEY] = params[:code] if params[:code].present?
|
7
|
+
redirect_to root_path
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
# Override the authenticate method to allow skipping OAuth in development
|
13
|
+
# mode. Skipping OAuth still requires a shop with Shopify domain specified
|
14
|
+
# by the `shop` parameter to be present in the local database.
|
15
|
+
def authenticate
|
16
|
+
if Rails.env.development? and DiscoApp.configuration.skip_oauth?
|
17
|
+
shop = DiscoApp::Shop.find_by_shopify_domain!(sanitized_shop_name)
|
18
|
+
|
19
|
+
sess = ShopifyAPI::Session.new(shop.shopify_domain, shop.shopify_token)
|
20
|
+
session[:shopify] = ShopifyApp::SessionRepository.store(sess)
|
21
|
+
session[:shopify_domain] = sanitized_shop_name
|
22
|
+
|
23
|
+
redirect_to disco_app.frame_path and return
|
24
|
+
end
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module DiscoApp::ApplicationHelper
|
2
|
+
|
3
|
+
# Generates a link pointing to an object (such as an order or customer) inside
|
4
|
+
# the given shop's Shopify admin. This helper makes it easy to create links
|
5
|
+
# to objects within the admin that support both right-clicking and opening in
|
6
|
+
# a new tab as well as capturing a left click and redirecting to the relevant
|
7
|
+
# object using `ShopifyApp.redirect()`.
|
8
|
+
def link_to_shopify_admin(shop, name, admin_path, options = {})
|
9
|
+
options[:onclick] = "ShopifyApp.redirect('#{admin_path}'); return false;"
|
10
|
+
options[:'data-no-turbolink'] = true
|
11
|
+
link_to(name, "https://#{shop.shopify_domain}/admin/#{admin_path}", options)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Generate a link that will open its href in an embedded Shopify modal.
|
15
|
+
def link_to_modal(name, path, options = {})
|
16
|
+
modal_options = {
|
17
|
+
src: path,
|
18
|
+
title: options.delete(:modal_title),
|
19
|
+
width: options.delete(:modal_width),
|
20
|
+
height: options.delete(:modal_height),
|
21
|
+
buttons: options.delete(:modal_buttons),
|
22
|
+
}
|
23
|
+
options[:onclick] = "ShopifyApp.Modal.open(#{modal_options.to_json}); return false;"
|
24
|
+
options[:onclick].gsub!(/"function(.*?)"/, 'function\1')
|
25
|
+
link_to(name, path, options)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module DiscoApp::Concerns::AppInstalledJob
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_enqueue { @shop.awaiting_install! }
|
6
|
+
before_perform { @shop.installing! }
|
7
|
+
after_perform { @shop.installed! }
|
8
|
+
end
|
9
|
+
|
10
|
+
# Perform application installation.
|
11
|
+
#
|
12
|
+
# - Synchronise webhooks.
|
13
|
+
# - Synchronise carrier service, if required.
|
14
|
+
# - Perform initial update of shop information.
|
15
|
+
# - Subscribe to default plan, if any exists.
|
16
|
+
#
|
17
|
+
def perform(shopify_domain, plan_code = nil, source = nil)
|
18
|
+
DiscoApp::SynchroniseWebhooksJob.perform_now(shopify_domain)
|
19
|
+
DiscoApp::SynchroniseCarrierServiceJob.perform_now(shopify_domain)
|
20
|
+
DiscoApp::ShopUpdateJob.perform_now(shopify_domain)
|
21
|
+
|
22
|
+
@shop.reload
|
23
|
+
|
24
|
+
if default_plan.present?
|
25
|
+
DiscoApp::SubscriptionService.subscribe(@shop, default_plan, plan_code, source)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Provide an overridable hook for applications to examine the @shop object
|
30
|
+
# and return the default plan, if any, the shop should be subscribed to. If
|
31
|
+
# nil is returned, no automatic subscription will take place and the store
|
32
|
+
# owner will be forced to choose a plan after installation.
|
33
|
+
#
|
34
|
+
# If implementing this method, it should be memoized.
|
35
|
+
def default_plan
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module DiscoApp::Concerns::AppUninstalledJob
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_enqueue { @shop.awaiting_uninstall! }
|
6
|
+
before_perform { @shop.uninstalling! }
|
7
|
+
after_perform { @shop.uninstalled! }
|
8
|
+
end
|
9
|
+
|
10
|
+
# Perform application uninstallation.
|
11
|
+
#
|
12
|
+
# - Mark any recurring application charges as cancelled.
|
13
|
+
# - Remove any stored sessions for the shop.
|
14
|
+
#
|
15
|
+
def perform(domain, shop_data)
|
16
|
+
DiscoApp::ChargesService.cancel_recurring_charges(@shop)
|
17
|
+
@shop.sessions.delete_all
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module DiscoApp::Concerns::ShopUpdateJob
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
# Perform an update of the current shop's information.
|
5
|
+
def perform(shopify_domain, shop_data = nil)
|
6
|
+
# If we weren't provided with shop data (eg from a webhook), fetch it.
|
7
|
+
shop_data ||= ActiveSupport::JSON::decode(ShopifyAPI::Shop.current.to_json)
|
8
|
+
|
9
|
+
# Ensure we can access shop data through symbols.
|
10
|
+
shop_data = HashWithIndifferentAccess.new(shop_data)
|
11
|
+
|
12
|
+
# Update model attributes present in both our model and the data hash.
|
13
|
+
@shop.update_attributes(shop_data.except(:id, :created_at).slice(*DiscoApp::Shop.column_names))
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module DiscoApp::Concerns::SynchroniseCarrierServiceJob
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
# Ensure that any carrier service required by our app is registered.
|
5
|
+
def perform(shopify_domain)
|
6
|
+
# Don't proceed unless we have a name and callback url.
|
7
|
+
return unless carrier_service_name and callback_url
|
8
|
+
|
9
|
+
# Registered the carrier service if it hasn't been registered yet.
|
10
|
+
unless current_carrier_service_names.include?(carrier_service_name)
|
11
|
+
ShopifyAPI::CarrierService.create(
|
12
|
+
name: carrier_service_name,
|
13
|
+
callback_url: callback_url,
|
14
|
+
service_discovery: true,
|
15
|
+
format: :json
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Ensure any existing carrier services (with the correct name) are active
|
20
|
+
# and have a current callback URL.
|
21
|
+
current_carrier_services.each do |carrier_service|
|
22
|
+
if carrier_service.name == carrier_service_name
|
23
|
+
carrier_service.callback_url = callback_url
|
24
|
+
carrier_service.active = true
|
25
|
+
carrier_service.save
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def carrier_service_name
|
33
|
+
DiscoApp.configuration.app_name
|
34
|
+
end
|
35
|
+
|
36
|
+
def callback_url
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Return a list of currently registered carrier service names.
|
43
|
+
def current_carrier_service_names
|
44
|
+
current_carrier_services.map(&:name)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return a list of currently registered carrier services.
|
48
|
+
def current_carrier_services
|
49
|
+
@current_carrier_service ||= ShopifyAPI::CarrierService.find(:all)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module DiscoApp::Concerns::SynchroniseWebhooksJob
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
# Ensure the webhooks registered with our shop are the same as those listed
|
5
|
+
# in our application configuration.
|
6
|
+
def perform(shopify_domain)
|
7
|
+
# Get the full list of expected webhook topics.
|
8
|
+
expected_topics = [:'app/uninstalled', :'shop/update'] + topics
|
9
|
+
|
10
|
+
# Registered any webhooks that haven't been registered yet.
|
11
|
+
(expected_topics - current_topics).each do |topic|
|
12
|
+
ShopifyAPI::Webhook.create(
|
13
|
+
topic: topic,
|
14
|
+
address: webhooks_url,
|
15
|
+
format: 'json'
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Remove any extraneous topics.
|
20
|
+
current_webhooks.each do |webhook|
|
21
|
+
unless expected_topics.include?(webhook.topic.to_sym)
|
22
|
+
ShopifyAPI::Webhook.delete(webhook.id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Ensure webhook addresses are current.
|
27
|
+
current_webhooks.each do |webhook|
|
28
|
+
unless webhook.address == webhooks_url
|
29
|
+
webhook.address = webhooks_url
|
30
|
+
webhook.save
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
# Return a list of additional webhook topics to listen for. This method
|
38
|
+
# can be overridden in the application to provide a list of app-specific
|
39
|
+
# webhooks that should be created during synchronisation.
|
40
|
+
def topics
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Return a list of currently registered topics.
|
47
|
+
def current_topics
|
48
|
+
current_webhooks.map(&:topic).map(&:to_sym)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return a list of current registered webhooks.
|
52
|
+
def current_webhooks
|
53
|
+
@current_webhooks ||= ShopifyAPI::Webhook.find(:all)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return the absolute URL to the webhooks endpoint.
|
57
|
+
def webhooks_url
|
58
|
+
DiscoApp::Engine.routes.url_helpers.webhooks_url
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# The base class for all jobs that should be performed in the context of a
|
2
|
+
# particular Shop's API session. The first argument to any job inheriting from
|
3
|
+
# this class must be the domain of the relevant store, so that the appropriate
|
4
|
+
# Shop model can be fetched and the temporary API session created.
|
5
|
+
class DiscoApp::ShopJob < ActiveJob::Base
|
6
|
+
|
7
|
+
queue_as :default
|
8
|
+
|
9
|
+
before_perform { |job| find_shop(job) }
|
10
|
+
before_enqueue { |job| find_shop(job) }
|
11
|
+
|
12
|
+
around_enqueue { |job, block| shop_context(job, block) }
|
13
|
+
around_perform { |job, block| shop_context(job, block) }
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def find_shop(job)
|
18
|
+
@shop ||= DiscoApp::Shop.find_by!(shopify_domain: job.arguments.first)
|
19
|
+
end
|
20
|
+
|
21
|
+
def shop_context(job, block)
|
22
|
+
@shop.temp {
|
23
|
+
block.call(job.arguments)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|