disco_app 0.8.8

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.
Files changed (254) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +37 -0
  3. data/app/assets/images/disco_app/icon.svg +1 -0
  4. data/app/assets/javascripts/disco_app/components/filterable_shop_list.js.jsx +65 -0
  5. data/app/assets/javascripts/disco_app/components/shop_filter_tab.js.jsx +34 -0
  6. data/app/assets/javascripts/disco_app/components/shop_filter_tabs.js.jsx +21 -0
  7. data/app/assets/javascripts/disco_app/components/shop_list.js.jsx +140 -0
  8. data/app/assets/javascripts/disco_app/components/shop_row.js.jsx +27 -0
  9. data/app/assets/javascripts/disco_app/components/shopify_admin_link.js.jsx +29 -0
  10. data/app/assets/javascripts/disco_app/components.js +5 -0
  11. data/app/assets/javascripts/disco_app/disco_app.js +7 -0
  12. data/app/assets/javascripts/disco_app/frame.js +152 -0
  13. data/app/assets/javascripts/disco_app/shopify-turbolinks.js +7 -0
  14. data/app/assets/stylesheets/disco_app/bootstrap/_custom.scss +54 -0
  15. data/app/assets/stylesheets/disco_app/bootstrap/_variables.scss +872 -0
  16. data/app/assets/stylesheets/disco_app/disco/_buttons.scss +31 -0
  17. data/app/assets/stylesheets/disco_app/disco/_cards.scss +51 -0
  18. data/app/assets/stylesheets/disco_app/disco/_forms.scss +23 -0
  19. data/app/assets/stylesheets/disco_app/disco/_grid.scss +58 -0
  20. data/app/assets/stylesheets/disco_app/disco/_sections.scss +61 -0
  21. data/app/assets/stylesheets/disco_app/disco/_tables.scss +57 -0
  22. data/app/assets/stylesheets/disco_app/disco/_tabs.scss +61 -0
  23. data/app/assets/stylesheets/disco_app/disco/_type.scss +39 -0
  24. data/app/assets/stylesheets/disco_app/disco/mixins/_flexbox.scss +394 -0
  25. data/app/assets/stylesheets/disco_app/disco_app.scss +16 -0
  26. data/app/assets/stylesheets/disco_app/frame/_buttons.scss +54 -0
  27. data/app/assets/stylesheets/disco_app/frame/_forms.scss +26 -0
  28. data/app/assets/stylesheets/disco_app/frame/_layout.scss +77 -0
  29. data/app/assets/stylesheets/disco_app/frame/_type.scss +32 -0
  30. data/app/assets/stylesheets/disco_app/frame.scss +9 -0
  31. data/app/controllers/disco_app/admin/app_settings_controller.rb +3 -0
  32. data/app/controllers/disco_app/admin/application_controller.rb +3 -0
  33. data/app/controllers/disco_app/admin/concerns/app_settings_controller.rb +24 -0
  34. data/app/controllers/disco_app/admin/concerns/authenticated_controller.rb +20 -0
  35. data/app/controllers/disco_app/admin/concerns/plans_controller.rb +51 -0
  36. data/app/controllers/disco_app/admin/concerns/shops_controller.rb +7 -0
  37. data/app/controllers/disco_app/admin/plans_controller.rb +3 -0
  38. data/app/controllers/disco_app/admin/resources/shops_controller.rb +3 -0
  39. data/app/controllers/disco_app/admin/shops_controller.rb +3 -0
  40. data/app/controllers/disco_app/charges_controller.rb +47 -0
  41. data/app/controllers/disco_app/concerns/app_proxy_controller.rb +40 -0
  42. data/app/controllers/disco_app/concerns/authenticated_controller.rb +56 -0
  43. data/app/controllers/disco_app/concerns/carrier_request_controller.rb +21 -0
  44. data/app/controllers/disco_app/frame_controller.rb +9 -0
  45. data/app/controllers/disco_app/install_controller.rb +27 -0
  46. data/app/controllers/disco_app/subscriptions_controller.rb +40 -0
  47. data/app/controllers/disco_app/webhooks_controller.rb +46 -0
  48. data/app/controllers/sessions_controller.rb +22 -0
  49. data/app/helpers/disco_app/application_helper.rb +28 -0
  50. data/app/jobs/disco_app/app_installed_job.rb +3 -0
  51. data/app/jobs/disco_app/app_uninstalled_job.rb +3 -0
  52. data/app/jobs/disco_app/concerns/app_installed_job.rb +39 -0
  53. data/app/jobs/disco_app/concerns/app_uninstalled_job.rb +20 -0
  54. data/app/jobs/disco_app/concerns/shop_update_job.rb +16 -0
  55. data/app/jobs/disco_app/concerns/subscription_changed_job.rb +7 -0
  56. data/app/jobs/disco_app/concerns/synchronise_carrier_service_job.rb +52 -0
  57. data/app/jobs/disco_app/concerns/synchronise_webhooks_job.rb +61 -0
  58. data/app/jobs/disco_app/shop_job.rb +27 -0
  59. data/app/jobs/disco_app/shop_update_job.rb +3 -0
  60. data/app/jobs/disco_app/subscription_changed_job.rb +3 -0
  61. data/app/jobs/disco_app/synchronise_carrier_service_job.rb +3 -0
  62. data/app/jobs/disco_app/synchronise_webhooks_job.rb +3 -0
  63. data/app/models/disco_app/app_settings.rb +3 -0
  64. data/app/models/disco_app/application_charge.rb +18 -0
  65. data/app/models/disco_app/concerns/app_settings.rb +7 -0
  66. data/app/models/disco_app/concerns/plan.rb +26 -0
  67. data/app/models/disco_app/concerns/plan_code.rb +15 -0
  68. data/app/models/disco_app/concerns/shop.rb +76 -0
  69. data/app/models/disco_app/concerns/subscription.rb +48 -0
  70. data/app/models/disco_app/concerns/synchronises.rb +39 -0
  71. data/app/models/disco_app/plan.rb +3 -0
  72. data/app/models/disco_app/plan_code.rb +3 -0
  73. data/app/models/disco_app/recurring_application_charge.rb +18 -0
  74. data/app/models/disco_app/session_storage.rb +18 -0
  75. data/app/models/disco_app/shop.rb +3 -0
  76. data/app/models/disco_app/subscription.rb +3 -0
  77. data/app/resources/disco_app/admin/resources/concerns/shop_resource.rb +46 -0
  78. data/app/resources/disco_app/admin/resources/shop_resource.rb +4 -0
  79. data/app/services/disco_app/carrier_request_service.rb +15 -0
  80. data/app/services/disco_app/charges_service.rb +81 -0
  81. data/app/services/disco_app/proxy_service.rb +17 -0
  82. data/app/services/disco_app/subscription_service.rb +37 -0
  83. data/app/services/disco_app/webhook_service.rb +30 -0
  84. data/app/views/disco_app/admin/app_settings/edit.html.erb +5 -0
  85. data/app/views/disco_app/admin/plans/_form.html.erb +27 -0
  86. data/app/views/disco_app/admin/plans/edit.html.erb +7 -0
  87. data/app/views/disco_app/admin/plans/index.html.erb +32 -0
  88. data/app/views/disco_app/admin/plans/new.html.erb +7 -0
  89. data/app/views/disco_app/admin/shops/index.html.erb +12 -0
  90. data/app/views/disco_app/charges/activate.html.erb +1 -0
  91. data/app/views/disco_app/charges/create.html.erb +1 -0
  92. data/app/views/disco_app/charges/new.html.erb +12 -0
  93. data/app/views/disco_app/frame/frame.html.erb +36 -0
  94. data/app/views/disco_app/install/installing.html.erb +7 -0
  95. data/app/views/disco_app/install/uninstalling.html.erb +1 -0
  96. data/app/views/disco_app/proxy_errors/404.html.erb +1 -0
  97. data/app/views/disco_app/shared/_card.html.erb +14 -0
  98. data/app/views/disco_app/shared/_section.html.erb +17 -0
  99. data/app/views/disco_app/subscriptions/new.html.erb +25 -0
  100. data/app/views/layouts/admin/_navbar.html.erb +25 -0
  101. data/app/views/layouts/admin.html.erb +27 -0
  102. data/app/views/layouts/application.html.erb +18 -0
  103. data/app/views/layouts/embedded_app.html.erb +41 -0
  104. data/app/views/layouts/embedded_app_modal.html.erb +17 -0
  105. data/app/views/sessions/new.html.erb +26 -0
  106. data/config/routes.rb +44 -0
  107. data/db/migrate/20150525000000_create_shops_if_not_existent.rb +15 -0
  108. data/db/migrate/20150525162112_add_status_to_shops.rb +5 -0
  109. data/db/migrate/20150525171422_add_meta_to_shops.rb +11 -0
  110. data/db/migrate/20150629210346_add_charge_status_to_shop.rb +5 -0
  111. data/db/migrate/20150814214025_add_more_meta_to_shops.rb +15 -0
  112. data/db/migrate/20151017231302_create_disco_app_plans.rb +13 -0
  113. data/db/migrate/20151017232027_create_disco_app_subscriptions.rb +15 -0
  114. data/db/migrate/20151017234409_move_shop_to_disco_app_engine.rb +5 -0
  115. data/db/migrate/20160112233706_create_disco_app_sessions.rb +12 -0
  116. data/db/migrate/20160113194418_add_shop_id_to_disco_app_sessions.rb +6 -0
  117. data/db/migrate/20160223111044_create_disco_app_settings.rb +8 -0
  118. data/db/migrate/20160301223215_update_plans.rb +22 -0
  119. data/db/migrate/20160301224558_update_subscriptions.rb +13 -0
  120. data/db/migrate/20160302104816_create_disco_app_recurring_application_charges.rb +14 -0
  121. data/db/migrate/20160302105259_create_disco_app_application_charges.rb +14 -0
  122. data/db/migrate/20160302134728_drop_charge_status_from_shops.rb +5 -0
  123. data/db/migrate/20160302142941_add_shopify_attributes_to_charges.rb +8 -0
  124. data/db/migrate/20160331093148_create_disco_app_plan_codes.rb +14 -0
  125. data/db/migrate/20160401044420_add_status_to_plan_codes.rb +5 -0
  126. data/db/migrate/20160401045551_add_amount_and_plan_code_to_disco_app_subscriptions.rb +7 -0
  127. data/lib/disco_app/configuration.rb +39 -0
  128. data/lib/disco_app/engine.rb +26 -0
  129. data/lib/disco_app/session.rb +14 -0
  130. data/lib/disco_app/support/file_fixtures.rb +23 -0
  131. data/lib/disco_app/test_help.rb +11 -0
  132. data/lib/disco_app/version.rb +3 -0
  133. data/lib/disco_app.rb +6 -0
  134. data/lib/generators/disco_app/USAGE +5 -0
  135. data/lib/generators/disco_app/adminify/adminify_generator.rb +35 -0
  136. data/lib/generators/disco_app/disco_app_generator.rb +164 -0
  137. data/lib/generators/disco_app/mailify/mailify_generator.rb +54 -0
  138. data/lib/generators/disco_app/monitorify/monitorify_generator.rb +28 -0
  139. data/lib/generators/disco_app/monitorify/templates/config/newrelic.yml +26 -0
  140. data/lib/generators/disco_app/monitorify/templates/initializers/rollbar.rb +12 -0
  141. data/lib/generators/disco_app/reactify/reactify_generator.rb +45 -0
  142. data/lib/generators/disco_app/templates/assets/javascripts/application.js +17 -0
  143. data/lib/generators/disco_app/templates/assets/stylesheets/application.scss +5 -0
  144. data/lib/generators/disco_app/templates/config/puma.rb +15 -0
  145. data/lib/generators/disco_app/templates/controllers/home_controller.rb +7 -0
  146. data/lib/generators/disco_app/templates/initializers/disco_app.rb +19 -0
  147. data/lib/generators/disco_app/templates/initializers/session_store.rb +2 -0
  148. data/lib/generators/disco_app/templates/initializers/shopify_app.rb +7 -0
  149. data/lib/generators/disco_app/templates/initializers/shopify_session_repository.rb +7 -0
  150. data/lib/generators/disco_app/templates/root/Procfile +2 -0
  151. data/lib/generators/disco_app/templates/views/home/index.html.erb +2 -0
  152. data/lib/tasks/carrier_service.rake +10 -0
  153. data/lib/tasks/sessions.rake +9 -0
  154. data/lib/tasks/start.rake +3 -0
  155. data/lib/tasks/webhooks.rake +10 -0
  156. data/test/controllers/disco_app/admin/shops_controller_test.rb +54 -0
  157. data/test/controllers/disco_app/charges_controller_test.rb +91 -0
  158. data/test/controllers/disco_app/install_controller_test.rb +50 -0
  159. data/test/controllers/disco_app/subscriptions_controller_test.rb +73 -0
  160. data/test/controllers/disco_app/webhooks_controller_test.rb +58 -0
  161. data/test/controllers/home_controller_test.rb +92 -0
  162. data/test/controllers/proxy_controller_test.rb +42 -0
  163. data/test/disco_app_test.rb +7 -0
  164. data/test/dummy/Rakefile +6 -0
  165. data/test/dummy/app/assets/javascripts/application.js +17 -0
  166. data/test/dummy/app/assets/stylesheets/application.scss +5 -0
  167. data/test/dummy/app/controllers/application_controller.rb +6 -0
  168. data/test/dummy/app/controllers/disco_app/admin/shops_controller.rb +8 -0
  169. data/test/dummy/app/controllers/home_controller.rb +7 -0
  170. data/test/dummy/app/controllers/proxy_controller.rb +8 -0
  171. data/test/dummy/app/helpers/application_helper.rb +2 -0
  172. data/test/dummy/app/jobs/disco_app/app_installed_job.rb +16 -0
  173. data/test/dummy/app/jobs/disco_app/app_uninstalled_job.rb +11 -0
  174. data/test/dummy/app/jobs/products_create_job.rb +7 -0
  175. data/test/dummy/app/jobs/products_delete_job.rb +7 -0
  176. data/test/dummy/app/jobs/products_update_job.rb +7 -0
  177. data/test/dummy/app/models/disco_app/shop.rb +15 -0
  178. data/test/dummy/app/models/product.rb +6 -0
  179. data/test/dummy/app/views/home/index.html.erb +2 -0
  180. data/test/dummy/bin/bundle +3 -0
  181. data/test/dummy/bin/rails +4 -0
  182. data/test/dummy/bin/rake +4 -0
  183. data/test/dummy/bin/setup +29 -0
  184. data/test/dummy/config/application.rb +38 -0
  185. data/test/dummy/config/boot.rb +5 -0
  186. data/test/dummy/config/database.codeship.yml +23 -0
  187. data/test/dummy/config/database.yml +20 -0
  188. data/test/dummy/config/environment.rb +5 -0
  189. data/test/dummy/config/environments/development.rb +41 -0
  190. data/test/dummy/config/environments/production.rb +85 -0
  191. data/test/dummy/config/environments/test.rb +42 -0
  192. data/test/dummy/config/initializers/assets.rb +11 -0
  193. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  194. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  195. data/test/dummy/config/initializers/disco_app.rb +19 -0
  196. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  197. data/test/dummy/config/initializers/inflections.rb +16 -0
  198. data/test/dummy/config/initializers/mime_types.rb +4 -0
  199. data/test/dummy/config/initializers/omniauth.rb +9 -0
  200. data/test/dummy/config/initializers/session_store.rb +2 -0
  201. data/test/dummy/config/initializers/shopify_app.rb +7 -0
  202. data/test/dummy/config/initializers/shopify_session_repository.rb +7 -0
  203. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  204. data/test/dummy/config/locales/en.yml +23 -0
  205. data/test/dummy/config/routes.rb +10 -0
  206. data/test/dummy/config/secrets.yml +22 -0
  207. data/test/dummy/config.ru +4 -0
  208. data/test/dummy/db/migrate/20160307182229_create_products.rb +11 -0
  209. data/test/dummy/db/schema.rb +138 -0
  210. data/test/dummy/public/404.html +67 -0
  211. data/test/dummy/public/422.html +67 -0
  212. data/test/dummy/public/500.html +66 -0
  213. data/test/dummy/public/favicon.ico +0 -0
  214. data/test/fixtures/api/widget_store/charges/activate_application_charge_request.json +16 -0
  215. data/test/fixtures/api/widget_store/charges/activate_application_charge_response.json +1 -0
  216. data/test/fixtures/api/widget_store/charges/activate_recurring_application_charge_request.json +20 -0
  217. data/test/fixtures/api/widget_store/charges/activate_recurring_application_charge_response.json +1 -0
  218. data/test/fixtures/api/widget_store/charges/create_application_charge_request.json +9 -0
  219. data/test/fixtures/api/widget_store/charges/create_application_charge_response.json +16 -0
  220. data/test/fixtures/api/widget_store/charges/create_recurring_application_charge_request.json +9 -0
  221. data/test/fixtures/api/widget_store/charges/create_recurring_application_charge_response.json +20 -0
  222. data/test/fixtures/api/widget_store/charges/create_second_recurring_application_charge_request.json +9 -0
  223. data/test/fixtures/api/widget_store/charges/create_second_recurring_application_charge_response.json +20 -0
  224. data/test/fixtures/api/widget_store/charges/get_accepted_application_charge_response.json +16 -0
  225. data/test/fixtures/api/widget_store/charges/get_accepted_recurring_application_charge_response.json +20 -0
  226. data/test/fixtures/api/widget_store/charges/get_declined_application_charge_response.json +16 -0
  227. data/test/fixtures/api/widget_store/charges/get_declined_recurring_application_charge_response.json +20 -0
  228. data/test/fixtures/api/widget_store/charges/get_pending_application_charge_response.json +16 -0
  229. data/test/fixtures/api/widget_store/charges/get_pending_recurring_application_charge_response.json +20 -0
  230. data/test/fixtures/api/widget_store/shop.json +46 -0
  231. data/test/fixtures/api/widget_store/webhooks.json +1 -0
  232. data/test/fixtures/disco_app/application_charges.yml +11 -0
  233. data/test/fixtures/disco_app/plan_codes.yml +6 -0
  234. data/test/fixtures/disco_app/plans.yml +37 -0
  235. data/test/fixtures/disco_app/recurring_application_charges.yml +11 -0
  236. data/test/fixtures/disco_app/shops.yml +10 -0
  237. data/test/fixtures/disco_app/subscriptions.yml +21 -0
  238. data/test/fixtures/products.yml +4 -0
  239. data/test/fixtures/webhooks/app_uninstalled.json +46 -0
  240. data/test/fixtures/webhooks/product_created.json +167 -0
  241. data/test/fixtures/webhooks/product_deleted.json +3 -0
  242. data/test/fixtures/webhooks/product_updated.json +167 -0
  243. data/test/integration/synchronises_test.rb +55 -0
  244. data/test/jobs/disco_app/app_installed_job_test.rb +42 -0
  245. data/test/jobs/disco_app/app_uninstalled_job_test.rb +30 -0
  246. data/test/models/disco_app/plan_test.rb +5 -0
  247. data/test/models/disco_app/session_test.rb +31 -0
  248. data/test/models/disco_app/shop_test.rb +27 -0
  249. data/test/services/disco_app/charges_service_test.rb +104 -0
  250. data/test/services/disco_app/subscription_service_test.rb +59 -0
  251. data/test/support/test_file_fixtures.rb +29 -0
  252. data/test/support/test_shopify_api.rb +16 -0
  253. data/test/test_helper.rb +52 -0
  254. metadata +660 -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,7 @@
1
+ module DiscoApp::Admin::Concerns::ShopsController
2
+ extend ActiveSupport::Concern
3
+
4
+ def index
5
+ end
6
+
7
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::Admin::PlansController < DiscoApp::Admin::ApplicationController
2
+ include DiscoApp::Admin::Concerns::PlansController
3
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::Admin::Resources::ShopsController < JSONAPI::ResourceController
2
+ include DiscoApp::Admin::Concerns::AuthenticatedController
3
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::Admin::ShopsController < DiscoApp::Admin::ApplicationController
2
+ include DiscoApp::Admin::Concerns::ShopsController
3
+ 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,9 @@
1
+ class DiscoApp::FrameController < ActionController::Base
2
+
3
+ layout nil
4
+
5
+ def frame
6
+
7
+ end
8
+
9
+ 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)
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,40 @@
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
+ # If a plan code was provided, check that it's (a) valid and available and
18
+ # (b) valid for the selected plan.
19
+ plan_code = nil
20
+ if subscription_params[:plan_code].present?
21
+ if(plan_code = DiscoApp::PlanCode.available.find_by(plan: plan, code: subscription_params[:plan_code])).nil?
22
+ redirect_to action: :new and return
23
+ end
24
+ end
25
+
26
+ # Subscribe the current shop to the selected plan.
27
+ if(subscription = DiscoApp::SubscriptionService.subscribe(@shop, plan, plan_code)).nil?
28
+ redirect_to action: :new
29
+ else
30
+ redirect_to main_app.root_path
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def subscription_params
37
+ params.require(:subscription).permit(:plan, :plan_code)
38
+ end
39
+
40
+ 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,22 @@
1
+ class SessionsController < ApplicationController
2
+ include ShopifyApp::SessionsController
3
+
4
+ protected
5
+
6
+ # Override the authenticate method to allow skipping OAuth in development
7
+ # mode. Skipping OAuth still requires a shop with Shopify domain specified
8
+ # by the `shop` parameter to be present in the local database.
9
+ def authenticate
10
+ if Rails.env.development? and DiscoApp.configuration.skip_oauth?
11
+ shop = DiscoApp::Shop.find_by_shopify_domain!(sanitized_shop_name)
12
+
13
+ sess = ShopifyAPI::Session.new(shop.shopify_domain, shop.shopify_token)
14
+ session[:shopify] = ShopifyApp::SessionRepository.store(sess)
15
+ session[:shopify_domain] = sanitized_shop_name
16
+
17
+ redirect_to disco_app.frame_path and return
18
+ end
19
+ super
20
+ end
21
+
22
+ 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,3 @@
1
+ class DiscoApp::AppInstalledJob < DiscoApp::ShopJob
2
+ include DiscoApp::Concerns::AppInstalledJob
3
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::AppUninstalledJob < DiscoApp::ShopJob
2
+ include DiscoApp::Concerns::AppUninstalledJob
3
+ 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)
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)
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,7 @@
1
+ module DiscoApp::Concerns::SubscriptionChangedJob
2
+ extend ActiveSupport::Concern
3
+
4
+ def perform(shopify_domain, subscription)
5
+ end
6
+
7
+ 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
@@ -0,0 +1,3 @@
1
+ class DiscoApp::ShopUpdateJob < DiscoApp::ShopJob
2
+ include DiscoApp::Concerns::ShopUpdateJob
3
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::SubscriptionChangedJob < DiscoApp::ShopJob
2
+ include DiscoApp::Concerns::SubscriptionChangedJob
3
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::SynchroniseCarrierServiceJob < DiscoApp::ShopJob
2
+ include DiscoApp::Concerns::SynchroniseCarrierServiceJob
3
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::SynchroniseWebhooksJob < DiscoApp::ShopJob
2
+ include DiscoApp::Concerns::SynchroniseWebhooksJob
3
+ end