disco_app 0.9.0

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 (287) 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/images/disco_app/icons.svg +0 -0
  5. data/app/assets/javascripts/disco_app/components/custom/filterable_shop_list.js.jsx +60 -0
  6. data/app/assets/javascripts/disco_app/components/custom/inline-radio-options.es6.jsx +59 -0
  7. data/app/assets/javascripts/disco_app/components/custom/rules-editor.es6.jsx +360 -0
  8. data/app/assets/javascripts/disco_app/components/custom/shop_filter_tab.js.jsx +34 -0
  9. data/app/assets/javascripts/disco_app/components/custom/shop_filter_tabs.js.jsx +21 -0
  10. data/app/assets/javascripts/disco_app/components/custom/shop_list.js.jsx +142 -0
  11. data/app/assets/javascripts/disco_app/components/custom/shop_row.js.jsx +43 -0
  12. data/app/assets/javascripts/disco_app/components/custom/shopify_admin_link.js.jsx +29 -0
  13. data/app/assets/javascripts/disco_app/components/ui-kit/cards/card-section.es6.jsx +30 -0
  14. data/app/assets/javascripts/disco_app/components/ui-kit/cards/card.es6.jsx +9 -0
  15. data/app/assets/javascripts/disco_app/components/ui-kit/cards/cart-section-title.es6.jsx +7 -0
  16. data/app/assets/javascripts/disco_app/components/ui-kit/forms/base_form.es6.jsx +72 -0
  17. data/app/assets/javascripts/disco_app/components/ui-kit/forms/base_input.es6.jsx +20 -0
  18. data/app/assets/javascripts/disco_app/components/ui-kit/forms/button.es6.jsx +13 -0
  19. data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-radio.es6.jsx +30 -0
  20. data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-select.es6.jsx +39 -0
  21. data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-text.es6.jsx +59 -0
  22. data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-textarea.es6.jsx +48 -0
  23. data/app/assets/javascripts/disco_app/components/ui-kit/icons/icon-chevron.es6.jsx +33 -0
  24. data/app/assets/javascripts/disco_app/components/ui-kit/icons/next-icon.es6.jsx +18 -0
  25. data/app/assets/javascripts/disco_app/components/ui-kit/input_select.es6.jsx +21 -0
  26. data/app/assets/javascripts/disco_app/components/ui-kit/ui-layout/ui-annotated-section.es6.jsx +29 -0
  27. data/app/assets/javascripts/disco_app/components/ui-kit/ui-layout/ui-empty-state.es6.jsx +35 -0
  28. data/app/assets/javascripts/disco_app/components/ui-kit/ui-layout/ui-footer-help.es6.jsx +13 -0
  29. data/app/assets/javascripts/disco_app/components/ui-kit/ui-layout/ui-page-actions.es6.jsx +39 -0
  30. data/app/assets/javascripts/disco_app/components.js +2 -0
  31. data/app/assets/javascripts/disco_app/disco_app.js +10 -0
  32. data/app/assets/javascripts/disco_app/frame.js +152 -0
  33. data/app/assets/javascripts/disco_app/shopify-turbolinks.js +7 -0
  34. data/app/assets/javascripts/disco_app/ui-kit.js +1 -0
  35. data/app/assets/stylesheets/disco_app/admin/_header.scss +66 -0
  36. data/app/assets/stylesheets/disco_app/admin/_layout.scss +40 -0
  37. data/app/assets/stylesheets/disco_app/admin/_nav.scss +172 -0
  38. data/app/assets/stylesheets/disco_app/admin.scss +11 -0
  39. data/app/assets/stylesheets/disco_app/disco_app.scss +16 -0
  40. data/app/assets/stylesheets/disco_app/frame/_buttons.scss +54 -0
  41. data/app/assets/stylesheets/disco_app/frame/_forms.scss +26 -0
  42. data/app/assets/stylesheets/disco_app/frame/_layout.scss +77 -0
  43. data/app/assets/stylesheets/disco_app/frame/_type.scss +32 -0
  44. data/app/assets/stylesheets/disco_app/frame.scss +9 -0
  45. data/app/assets/stylesheets/disco_app/mixins/_flexbox.scss +400 -0
  46. data/app/assets/stylesheets/disco_app/ui-kit/_ui-empty-state.scss +94 -0
  47. data/app/assets/stylesheets/disco_app/ui-kit/_ui-footer-help.scss +25 -0
  48. data/app/assets/stylesheets/disco_app/ui-kit/_ui-icons.scss +28 -0
  49. data/app/assets/stylesheets/disco_app/ui-kit/_ui-kit.scss +5086 -0
  50. data/app/assets/stylesheets/disco_app/ui-kit/_ui-layout.scss +10 -0
  51. data/app/assets/stylesheets/disco_app/ui-kit/_ui-page-actions.scss +21 -0
  52. data/app/assets/stylesheets/disco_app/ui-kit/_ui-tabs.scss +63 -0
  53. data/app/assets/stylesheets/disco_app/ui-kit/_ui-type.scss +13 -0
  54. data/app/controllers/disco_app/admin/app_settings_controller.rb +3 -0
  55. data/app/controllers/disco_app/admin/application_controller.rb +3 -0
  56. data/app/controllers/disco_app/admin/concerns/app_settings_controller.rb +24 -0
  57. data/app/controllers/disco_app/admin/concerns/authenticated_controller.rb +20 -0
  58. data/app/controllers/disco_app/admin/concerns/plans_controller.rb +54 -0
  59. data/app/controllers/disco_app/admin/concerns/shops_controller.rb +7 -0
  60. data/app/controllers/disco_app/admin/concerns/subscriptions_controller.rb +32 -0
  61. data/app/controllers/disco_app/admin/plans_controller.rb +3 -0
  62. data/app/controllers/disco_app/admin/resources/shops_controller.rb +3 -0
  63. data/app/controllers/disco_app/admin/shops_controller.rb +3 -0
  64. data/app/controllers/disco_app/admin/subscriptions_controller.rb +3 -0
  65. data/app/controllers/disco_app/charges_controller.rb +47 -0
  66. data/app/controllers/disco_app/concerns/app_proxy_controller.rb +40 -0
  67. data/app/controllers/disco_app/concerns/authenticated_controller.rb +56 -0
  68. data/app/controllers/disco_app/concerns/carrier_request_controller.rb +21 -0
  69. data/app/controllers/disco_app/frame_controller.rb +9 -0
  70. data/app/controllers/disco_app/install_controller.rb +27 -0
  71. data/app/controllers/disco_app/subscriptions_controller.rb +32 -0
  72. data/app/controllers/disco_app/webhooks_controller.rb +46 -0
  73. data/app/controllers/sessions_controller.rb +28 -0
  74. data/app/helpers/disco_app/application_helper.rb +50 -0
  75. data/app/jobs/disco_app/app_installed_job.rb +3 -0
  76. data/app/jobs/disco_app/app_uninstalled_job.rb +3 -0
  77. data/app/jobs/disco_app/concerns/app_installed_job.rb +39 -0
  78. data/app/jobs/disco_app/concerns/app_uninstalled_job.rb +20 -0
  79. data/app/jobs/disco_app/concerns/shop_update_job.rb +16 -0
  80. data/app/jobs/disco_app/concerns/subscription_changed_job.rb +7 -0
  81. data/app/jobs/disco_app/concerns/synchronise_carrier_service_job.rb +52 -0
  82. data/app/jobs/disco_app/concerns/synchronise_webhooks_job.rb +61 -0
  83. data/app/jobs/disco_app/shop_job.rb +27 -0
  84. data/app/jobs/disco_app/shop_update_job.rb +3 -0
  85. data/app/jobs/disco_app/subscription_changed_job.rb +3 -0
  86. data/app/jobs/disco_app/synchronise_carrier_service_job.rb +3 -0
  87. data/app/jobs/disco_app/synchronise_webhooks_job.rb +3 -0
  88. data/app/models/disco_app/app_settings.rb +3 -0
  89. data/app/models/disco_app/application_charge.rb +18 -0
  90. data/app/models/disco_app/concerns/app_settings.rb +7 -0
  91. data/app/models/disco_app/concerns/plan.rb +26 -0
  92. data/app/models/disco_app/concerns/plan_code.rb +15 -0
  93. data/app/models/disco_app/concerns/shop.rb +76 -0
  94. data/app/models/disco_app/concerns/subscription.rb +60 -0
  95. data/app/models/disco_app/concerns/synchronises.rb +39 -0
  96. data/app/models/disco_app/plan.rb +3 -0
  97. data/app/models/disco_app/plan_code.rb +3 -0
  98. data/app/models/disco_app/recurring_application_charge.rb +18 -0
  99. data/app/models/disco_app/session_storage.rb +18 -0
  100. data/app/models/disco_app/shop.rb +3 -0
  101. data/app/models/disco_app/subscription.rb +3 -0
  102. data/app/resources/disco_app/admin/resources/concerns/shop_resource.rb +82 -0
  103. data/app/resources/disco_app/admin/resources/shop_resource.rb +4 -0
  104. data/app/services/disco_app/carrier_request_service.rb +15 -0
  105. data/app/services/disco_app/charges_service.rb +81 -0
  106. data/app/services/disco_app/proxy_service.rb +17 -0
  107. data/app/services/disco_app/subscription_service.rb +46 -0
  108. data/app/services/disco_app/webhook_service.rb +30 -0
  109. data/app/views/disco_app/admin/app_settings/edit.html.erb +5 -0
  110. data/app/views/disco_app/admin/plans/_form.html.erb +72 -0
  111. data/app/views/disco_app/admin/plans/_plan_code_fields.html.erb +15 -0
  112. data/app/views/disco_app/admin/plans/edit.html.erb +7 -0
  113. data/app/views/disco_app/admin/plans/index.html.erb +43 -0
  114. data/app/views/disco_app/admin/plans/new.html.erb +7 -0
  115. data/app/views/disco_app/admin/shops/index.html.erb +13 -0
  116. data/app/views/disco_app/admin/subscriptions/edit.html.erb +33 -0
  117. data/app/views/disco_app/charges/activate.html.erb +1 -0
  118. data/app/views/disco_app/charges/create.html.erb +1 -0
  119. data/app/views/disco_app/charges/new.html.erb +12 -0
  120. data/app/views/disco_app/frame/frame.html.erb +36 -0
  121. data/app/views/disco_app/install/installing.html.erb +7 -0
  122. data/app/views/disco_app/install/uninstalling.html.erb +1 -0
  123. data/app/views/disco_app/proxy_errors/404.html.erb +1 -0
  124. data/app/views/disco_app/shared/_card.html.erb +14 -0
  125. data/app/views/disco_app/shared/_icons.html.erb +3 -0
  126. data/app/views/disco_app/shared/_section.html.erb +17 -0
  127. data/app/views/disco_app/subscriptions/new.html.erb +25 -0
  128. data/app/views/layouts/admin/_nav_items.erb +20 -0
  129. data/app/views/layouts/admin.html.erb +67 -0
  130. data/app/views/layouts/application.html.erb +18 -0
  131. data/app/views/layouts/embedded_app.html.erb +43 -0
  132. data/app/views/layouts/embedded_app_modal.html.erb +28 -0
  133. data/app/views/sessions/new.html.erb +26 -0
  134. data/config/routes.rb +48 -0
  135. data/db/migrate/20150525000000_create_shops_if_not_existent.rb +15 -0
  136. data/db/migrate/20150525162112_add_status_to_shops.rb +5 -0
  137. data/db/migrate/20150525171422_add_meta_to_shops.rb +11 -0
  138. data/db/migrate/20150629210346_add_charge_status_to_shop.rb +5 -0
  139. data/db/migrate/20150814214025_add_more_meta_to_shops.rb +15 -0
  140. data/db/migrate/20151017231302_create_disco_app_plans.rb +13 -0
  141. data/db/migrate/20151017232027_create_disco_app_subscriptions.rb +15 -0
  142. data/db/migrate/20151017234409_move_shop_to_disco_app_engine.rb +5 -0
  143. data/db/migrate/20160112233706_create_disco_app_sessions.rb +12 -0
  144. data/db/migrate/20160113194418_add_shop_id_to_disco_app_sessions.rb +6 -0
  145. data/db/migrate/20160223111044_create_disco_app_settings.rb +8 -0
  146. data/db/migrate/20160301223215_update_plans.rb +22 -0
  147. data/db/migrate/20160301224558_update_subscriptions.rb +13 -0
  148. data/db/migrate/20160302104816_create_disco_app_recurring_application_charges.rb +14 -0
  149. data/db/migrate/20160302105259_create_disco_app_application_charges.rb +14 -0
  150. data/db/migrate/20160302134728_drop_charge_status_from_shops.rb +5 -0
  151. data/db/migrate/20160302142941_add_shopify_attributes_to_charges.rb +8 -0
  152. data/db/migrate/20160331093148_create_disco_app_plan_codes.rb +14 -0
  153. data/db/migrate/20160401044420_add_status_to_plan_codes.rb +5 -0
  154. data/db/migrate/20160401045551_add_amount_and_plan_code_to_disco_app_subscriptions.rb +7 -0
  155. data/db/migrate/20160425205211_add_source_to_disco_app_subscriptions.rb +5 -0
  156. data/db/migrate/20160426033520_add_trial_period_days_to_disco_app_subscriptions.rb +5 -0
  157. data/lib/disco_app/configuration.rb +39 -0
  158. data/lib/disco_app/constants.rb +4 -0
  159. data/lib/disco_app/engine.rb +27 -0
  160. data/lib/disco_app/session.rb +14 -0
  161. data/lib/disco_app/support/file_fixtures.rb +23 -0
  162. data/lib/disco_app/test_help.rb +11 -0
  163. data/lib/disco_app/version.rb +3 -0
  164. data/lib/disco_app.rb +7 -0
  165. data/lib/generators/disco_app/USAGE +5 -0
  166. data/lib/generators/disco_app/disco_app_generator.rb +197 -0
  167. data/lib/generators/disco_app/mailify/mailify_generator.rb +54 -0
  168. data/lib/generators/disco_app/monitorify/monitorify_generator.rb +28 -0
  169. data/lib/generators/disco_app/monitorify/templates/config/newrelic.yml +26 -0
  170. data/lib/generators/disco_app/monitorify/templates/initializers/rollbar.rb +12 -0
  171. data/lib/generators/disco_app/templates/assets/javascripts/application.js +17 -0
  172. data/lib/generators/disco_app/templates/assets/javascripts/components.js +3 -0
  173. data/lib/generators/disco_app/templates/assets/stylesheets/application.scss +5 -0
  174. data/lib/generators/disco_app/templates/config/database.yml.tt +20 -0
  175. data/lib/generators/disco_app/templates/config/puma.rb +15 -0
  176. data/lib/generators/disco_app/templates/controllers/home_controller.rb +7 -0
  177. data/lib/generators/disco_app/templates/initializers/disco_app.rb +19 -0
  178. data/lib/generators/disco_app/templates/initializers/session_store.rb +2 -0
  179. data/lib/generators/disco_app/templates/initializers/shopify_app.rb +7 -0
  180. data/lib/generators/disco_app/templates/initializers/shopify_session_repository.rb +7 -0
  181. data/lib/generators/disco_app/templates/root/Procfile +2 -0
  182. data/lib/generators/disco_app/templates/views/home/index.html.erb +2 -0
  183. data/lib/tasks/carrier_service.rake +10 -0
  184. data/lib/tasks/database.rake +8 -0
  185. data/lib/tasks/sessions.rake +9 -0
  186. data/lib/tasks/start.rake +3 -0
  187. data/lib/tasks/webhooks.rake +10 -0
  188. data/test/controllers/disco_app/admin/shops_controller_test.rb +54 -0
  189. data/test/controllers/disco_app/charges_controller_test.rb +99 -0
  190. data/test/controllers/disco_app/install_controller_test.rb +50 -0
  191. data/test/controllers/disco_app/subscriptions_controller_test.rb +68 -0
  192. data/test/controllers/disco_app/webhooks_controller_test.rb +58 -0
  193. data/test/controllers/home_controller_test.rb +92 -0
  194. data/test/controllers/proxy_controller_test.rb +42 -0
  195. data/test/disco_app_test.rb +7 -0
  196. data/test/dummy/Rakefile +6 -0
  197. data/test/dummy/app/assets/javascripts/application.js +17 -0
  198. data/test/dummy/app/assets/stylesheets/application.scss +5 -0
  199. data/test/dummy/app/controllers/application_controller.rb +6 -0
  200. data/test/dummy/app/controllers/disco_app/admin/shops_controller.rb +8 -0
  201. data/test/dummy/app/controllers/home_controller.rb +7 -0
  202. data/test/dummy/app/controllers/proxy_controller.rb +8 -0
  203. data/test/dummy/app/helpers/application_helper.rb +2 -0
  204. data/test/dummy/app/jobs/disco_app/app_installed_job.rb +16 -0
  205. data/test/dummy/app/jobs/disco_app/app_uninstalled_job.rb +11 -0
  206. data/test/dummy/app/jobs/products_create_job.rb +7 -0
  207. data/test/dummy/app/jobs/products_delete_job.rb +7 -0
  208. data/test/dummy/app/jobs/products_update_job.rb +7 -0
  209. data/test/dummy/app/models/disco_app/shop.rb +15 -0
  210. data/test/dummy/app/models/product.rb +6 -0
  211. data/test/dummy/app/views/home/index.html.erb +2 -0
  212. data/test/dummy/bin/bundle +3 -0
  213. data/test/dummy/bin/rails +4 -0
  214. data/test/dummy/bin/rake +4 -0
  215. data/test/dummy/bin/setup +29 -0
  216. data/test/dummy/config/application.rb +38 -0
  217. data/test/dummy/config/boot.rb +5 -0
  218. data/test/dummy/config/database.codeship.yml +23 -0
  219. data/test/dummy/config/database.yml +20 -0
  220. data/test/dummy/config/environment.rb +5 -0
  221. data/test/dummy/config/environments/development.rb +41 -0
  222. data/test/dummy/config/environments/production.rb +85 -0
  223. data/test/dummy/config/environments/test.rb +42 -0
  224. data/test/dummy/config/initializers/assets.rb +11 -0
  225. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  226. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  227. data/test/dummy/config/initializers/disco_app.rb +19 -0
  228. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  229. data/test/dummy/config/initializers/inflections.rb +16 -0
  230. data/test/dummy/config/initializers/mime_types.rb +4 -0
  231. data/test/dummy/config/initializers/omniauth.rb +9 -0
  232. data/test/dummy/config/initializers/session_store.rb +2 -0
  233. data/test/dummy/config/initializers/shopify_app.rb +7 -0
  234. data/test/dummy/config/initializers/shopify_session_repository.rb +7 -0
  235. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  236. data/test/dummy/config/locales/en.yml +23 -0
  237. data/test/dummy/config/routes.rb +10 -0
  238. data/test/dummy/config/secrets.yml +22 -0
  239. data/test/dummy/config.ru +4 -0
  240. data/test/dummy/db/migrate/20160307182229_create_products.rb +11 -0
  241. data/test/dummy/db/schema.rb +140 -0
  242. data/test/dummy/public/404.html +67 -0
  243. data/test/dummy/public/422.html +67 -0
  244. data/test/dummy/public/500.html +66 -0
  245. data/test/dummy/public/favicon.ico +0 -0
  246. data/test/fixtures/api/widget_store/charges/activate_application_charge_request.json +16 -0
  247. data/test/fixtures/api/widget_store/charges/activate_application_charge_response.json +1 -0
  248. data/test/fixtures/api/widget_store/charges/activate_recurring_application_charge_request.json +20 -0
  249. data/test/fixtures/api/widget_store/charges/activate_recurring_application_charge_response.json +1 -0
  250. data/test/fixtures/api/widget_store/charges/create_application_charge_request.json +9 -0
  251. data/test/fixtures/api/widget_store/charges/create_application_charge_response.json +16 -0
  252. data/test/fixtures/api/widget_store/charges/create_recurring_application_charge_request.json +9 -0
  253. data/test/fixtures/api/widget_store/charges/create_recurring_application_charge_response.json +20 -0
  254. data/test/fixtures/api/widget_store/charges/create_second_recurring_application_charge_request.json +9 -0
  255. data/test/fixtures/api/widget_store/charges/create_second_recurring_application_charge_response.json +20 -0
  256. data/test/fixtures/api/widget_store/charges/get_accepted_application_charge_response.json +16 -0
  257. data/test/fixtures/api/widget_store/charges/get_accepted_recurring_application_charge_response.json +20 -0
  258. data/test/fixtures/api/widget_store/charges/get_declined_application_charge_response.json +16 -0
  259. data/test/fixtures/api/widget_store/charges/get_declined_recurring_application_charge_response.json +20 -0
  260. data/test/fixtures/api/widget_store/charges/get_pending_application_charge_response.json +16 -0
  261. data/test/fixtures/api/widget_store/charges/get_pending_recurring_application_charge_response.json +20 -0
  262. data/test/fixtures/api/widget_store/shop.json +46 -0
  263. data/test/fixtures/api/widget_store/webhooks.json +1 -0
  264. data/test/fixtures/disco_app/application_charges.yml +11 -0
  265. data/test/fixtures/disco_app/plan_codes.yml +13 -0
  266. data/test/fixtures/disco_app/plans.yml +37 -0
  267. data/test/fixtures/disco_app/recurring_application_charges.yml +11 -0
  268. data/test/fixtures/disco_app/shops.yml +10 -0
  269. data/test/fixtures/disco_app/subscriptions.yml +22 -0
  270. data/test/fixtures/products.yml +4 -0
  271. data/test/fixtures/webhooks/app_uninstalled.json +46 -0
  272. data/test/fixtures/webhooks/product_created.json +167 -0
  273. data/test/fixtures/webhooks/product_deleted.json +3 -0
  274. data/test/fixtures/webhooks/product_updated.json +167 -0
  275. data/test/integration/synchronises_test.rb +55 -0
  276. data/test/jobs/disco_app/app_installed_job_test.rb +55 -0
  277. data/test/jobs/disco_app/app_uninstalled_job_test.rb +30 -0
  278. data/test/models/disco_app/plan_test.rb +5 -0
  279. data/test/models/disco_app/session_test.rb +31 -0
  280. data/test/models/disco_app/shop_test.rb +27 -0
  281. data/test/models/disco_app/subscription_test.rb +19 -0
  282. data/test/services/disco_app/charges_service_test.rb +112 -0
  283. data/test/services/disco_app/subscription_service_test.rb +60 -0
  284. data/test/support/test_file_fixtures.rb +29 -0
  285. data/test/support/test_shopify_api.rb +16 -0
  286. data/test/test_helper.rb +55 -0
  287. metadata +722 -0
@@ -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,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
@@ -0,0 +1,3 @@
1
+ class DiscoApp::AppSettings < ActiveRecord::Base
2
+ include DiscoApp::Concerns::AppSettings
3
+ end
@@ -0,0 +1,18 @@
1
+ class DiscoApp::ApplicationCharge < ActiveRecord::Base
2
+
3
+ belongs_to :shop
4
+ belongs_to :subscription
5
+
6
+ enum status: [:pending, :accepted, :declined, :expired, :active]
7
+
8
+ scope :active, -> { where status: statuses[:active] }
9
+
10
+ def recurring?
11
+ false
12
+ end
13
+
14
+ def activate_url
15
+ DiscoApp::Engine.routes.url_helpers.activate_subscription_charge_url(subscription, self)
16
+ end
17
+
18
+ end
@@ -0,0 +1,7 @@
1
+ module DiscoApp::Concerns::AppSettings
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ acts_as_singleton
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ module DiscoApp::Concerns::Plan
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+
6
+ has_many :subscriptions
7
+ has_many :shops, through: :subscriptions
8
+ has_many :plan_codes, dependent: :destroy
9
+
10
+ accepts_nested_attributes_for :plan_codes, allow_destroy: true
11
+
12
+ enum status: [:available, :unavailable]
13
+ enum plan_type: [:recurring, :one_time]
14
+ enum interval: [:month, :year]
15
+
16
+ scope :available, -> { where status: statuses[:available] }
17
+
18
+ validates_presence_of :name
19
+
20
+ end
21
+
22
+ def has_trial?
23
+ trial_period_days.present? and trial_period_days > 0
24
+ end
25
+
26
+ end
@@ -0,0 +1,15 @@
1
+ module DiscoApp::Concerns::PlanCode
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+
6
+ belongs_to :plan
7
+
8
+ enum status: [:available, :unavailable]
9
+
10
+ validates_presence_of :code
11
+ validates_presence_of :amount
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,76 @@
1
+ module DiscoApp::Concerns::Shop
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ include ShopifyApp::Shop
6
+ include ActionView::Helpers::DateHelper
7
+
8
+ # Define relationships to plans and subscriptions.
9
+ has_many :subscriptions
10
+ has_many :plans, through: :subscriptions
11
+
12
+ # Define relationship to sessions.
13
+ has_many :sessions, class_name: 'DiscoApp::Session', dependent: :destroy
14
+
15
+ # Define possible installation statuses as an enum.
16
+ enum status: [:never_installed, :awaiting_install, :installing, :installed, :awaiting_uninstall, :uninstalling, :uninstalled]
17
+
18
+ # Define some useful scopes.
19
+ scope :status, -> (status) { where status: status }
20
+ scope :installed, -> { where status: statuses[:installed] }
21
+ scope :has_active_shopify_plan, -> { where.not(plan_name: [:cancelled, :frozen, :fraudulent]) }
22
+
23
+ # Alias 'with_shopify_session' as 'temp', as per our existing conventions.
24
+ alias_method :temp, :with_shopify_session
25
+
26
+ # Return a hash of attributes that should be used to create a new charge for this shop.
27
+ # This method can be overridden by the inheriting Shop class in order to provide charges
28
+ # customised to a particular shop. Otherwise, the default settings configured in application.rb
29
+ # will be used.
30
+ def new_charge_attributes
31
+ {
32
+ type: Rails.configuration.x.shopify_charges_default_type,
33
+ name: DiscoApp.configuration.app_name,
34
+ price: Rails.configuration.x.shopify_charges_default_price,
35
+ trial_days: Rails.configuration.x.shopify_charges_default_trial_days,
36
+ }
37
+ end
38
+
39
+ # Convenience method to check if this shop has a current subscription.
40
+ def current_subscription?
41
+ current_subscription.present?
42
+ end
43
+
44
+ # Convenience method to get the current subscription for this shop, if any.
45
+ def current_subscription
46
+ subscriptions.current.first
47
+ end
48
+
49
+ # Convenience method to get the current plan for this shop, if any.
50
+ def current_plan
51
+ current_subscription&.plan
52
+ end
53
+
54
+ # Return the absolute URL to the shop's storefront.
55
+ def url
56
+ "#{protocol}://#{domain}"
57
+ end
58
+
59
+ # Return the protocol the shop's storefront uses. This should now always be
60
+ # https as all Shopify stores have SSL enabled.
61
+ def protocol
62
+ 'https'
63
+ end
64
+
65
+ # Return the absolute URL to the shop's admin.
66
+ def admin_url
67
+ "https://#{shopify_domain}/admin"
68
+ end
69
+
70
+ def installed_duration
71
+ distance_of_time_in_words_to_now(created_at.time)
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,60 @@
1
+ module DiscoApp::Concerns::Subscription
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+
6
+ belongs_to :shop
7
+ belongs_to :plan
8
+ belongs_to :plan_code
9
+
10
+ has_many :one_time_charges, class_name: 'DiscoApp::ApplicationCharge', dependent: :destroy
11
+ has_many :recurring_charges, class_name: 'DiscoApp::RecurringApplicationCharge', dependent: :destroy
12
+
13
+ enum status: [:trial, :active, :cancelled]
14
+ enum subscription_type: [:recurring, :one_time]
15
+
16
+ scope :current, -> { where status: [statuses[:trial], statuses[:active]] }
17
+
18
+ after_commit :cancel_charge
19
+
20
+ end
21
+
22
+ # Only require an active charge if the amount to be charged is > 0.
23
+ def requires_active_charge?
24
+ amount > 0
25
+ end
26
+
27
+ # Convenience method to check if this subscription has an active charge.
28
+ def active_charge?
29
+ active_charge.present?
30
+ end
31
+
32
+ # Convenience method to get the active charge for this subscription.
33
+ def active_charge
34
+ charges.active.first
35
+ end
36
+
37
+ # Return the appropriate set of charges for this subscription's type.
38
+ def charges
39
+ recurring? ? recurring_charges : one_time_charges
40
+ end
41
+
42
+ def charge_class
43
+ recurring? ? DiscoApp::RecurringApplicationCharge : DiscoApp::ApplicationCharge
44
+ end
45
+
46
+ def shopify_charge_class
47
+ recurring? ? ShopifyAPI::RecurringApplicationCharge : ShopifyAPI::ApplicationCharge
48
+ end
49
+
50
+ private
51
+
52
+ # If the amount or trial period for this subscription changes, clear any
53
+ # active charge, as the user will need to re-authorize the charge.
54
+ def cancel_charge
55
+ return if (previous_changes.keys & ['amount', 'trial_period_days']).empty?
56
+ return unless active_charge?
57
+ active_charge.cancelled!
58
+ end
59
+
60
+ end
@@ -0,0 +1,39 @@
1
+ module DiscoApp::Concerns::Synchronises
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+
6
+ def should_synchronise?(shop, data)
7
+ true
8
+ end
9
+
10
+ def synchronise(shop, data)
11
+ data = data.with_indifferent_access
12
+
13
+ return unless should_synchronise?(shop, data)
14
+
15
+ instance = self.find_or_create_by!(id: data[:id]) do |instance|
16
+ instance.shop = shop
17
+ instance.data = data
18
+ end
19
+
20
+ instance.update(data: data)
21
+
22
+ instance
23
+ end
24
+
25
+ def should_synchronise_deletion?(shop, data)
26
+ true
27
+ end
28
+
29
+ def synchronise_deletion(shop, data)
30
+ data = data.with_indifferent_access
31
+
32
+ return unless should_synchronise_deletion?(shop, data)
33
+
34
+ self.destroy_all(shop: shop, id: data[:id])
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::Plan < ActiveRecord::Base
2
+ include DiscoApp::Concerns::Plan
3
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::PlanCode < ActiveRecord::Base
2
+ include DiscoApp::Concerns::PlanCode
3
+ end
@@ -0,0 +1,18 @@
1
+ class DiscoApp::RecurringApplicationCharge < ActiveRecord::Base
2
+
3
+ belongs_to :shop
4
+ belongs_to :subscription
5
+
6
+ enum status: [:pending, :accepted, :declined, :active, :cancelled, :expired]
7
+
8
+ scope :active, -> { where status: statuses[:active] }
9
+
10
+ def recurring?
11
+ true
12
+ end
13
+
14
+ def activate_url
15
+ DiscoApp::Engine.routes.url_helpers.activate_subscription_charge_url(subscription, self)
16
+ end
17
+
18
+ end
@@ -0,0 +1,18 @@
1
+ module DiscoApp
2
+ class SessionStorage
3
+ def self.store(session)
4
+ shop = Shop.find_or_initialize_by(shopify_domain: session.url)
5
+ shop.shopify_token = session.token
6
+ shop.save!
7
+ shop.id
8
+ end
9
+
10
+ def self.retrieve(id)
11
+ return unless id
12
+ shop = Shop.find(id)
13
+ ShopifyAPI::Session.new(shop.shopify_domain, shop.shopify_token)
14
+ rescue ActiveRecord::RecordNotFound
15
+ nil
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::Shop < ActiveRecord::Base
2
+ include DiscoApp::Concerns::Shop
3
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::Subscription < ActiveRecord::Base
2
+ include DiscoApp::Concerns::Subscription
3
+ end
@@ -0,0 +1,82 @@
1
+ require 'jsonapi/resource'
2
+
3
+ module DiscoApp::Admin::Resources::Concerns::ShopResource
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+
8
+ attributes :domain, :status, :email, :country_name
9
+ attributes :currency, :plan_display_name, :created_at
10
+ attributes :current_subscription_id, :current_subscription_display_amount, :current_subscription_display_plan, :current_subscription_display_plan_code, :current_subscription_source
11
+ attributes :installed_duration
12
+
13
+ model_name 'DiscoApp::Shop'
14
+
15
+ filters :status
16
+
17
+ # Adjust the base records method to ensure only models for the authenticated domain are retrieved.
18
+ def self.records(options = {})
19
+ records = DiscoApp::Shop.order(created_at: :desc)
20
+ records
21
+ end
22
+
23
+ # Apply filters.
24
+ def self.apply_filter(records, filter, value, options)
25
+ return records if value.blank?
26
+
27
+ # Perform appropriate filtering.
28
+ case filter
29
+ when :status
30
+ return records.where(status: value.map { |v| DiscoApp::Shop.statuses[v.to_sym] } )
31
+ else
32
+ return super(records, filter, value)
33
+ end
34
+ end
35
+
36
+ # Don't allow the update of any fields via the API.
37
+ def self.updatable_fields(context)
38
+ []
39
+ end
40
+
41
+ # Don't allow the creation of any fields via the API.
42
+ def self.creatable_fields(context)
43
+ []
44
+ end
45
+
46
+ def current_subscription_id
47
+ if @model.current_subscription?
48
+ @model.current_subscription.id
49
+ end
50
+ end
51
+
52
+ def current_subscription_display_amount
53
+ if @model.current_subscription?
54
+ @model.current_subscription.amount
55
+ else
56
+ '-'
57
+ end
58
+ end
59
+
60
+ def current_subscription_display_plan
61
+ if @model.current_subscription?
62
+ @model.current_plan.name
63
+ else
64
+ 'None'
65
+ end
66
+ end
67
+
68
+ def current_subscription_display_plan_code
69
+ @model.current_subscription&.plan_code&.code
70
+ end
71
+
72
+ def current_subscription_source
73
+ if @model.current_subscription?
74
+ @model.current_subscription.source || '-'
75
+ else
76
+ '-'
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,4 @@
1
+ class DiscoApp::Admin::Resources::ShopResource < JSONAPI::Resource
2
+ include DiscoApp::Admin::Resources::Concerns::ShopResource
3
+
4
+ end
@@ -0,0 +1,15 @@
1
+ class DiscoApp::CarrierRequestService
2
+
3
+ # Return true iff the provided hmac_to_verify matches that calculated from the
4
+ # given data and secret.
5
+ def self.is_valid_hmac?(body, secret, hmac_to_verify)
6
+ ActiveSupport::SecurityUtils.secure_compare(self.calculated_hmac(body, secret), hmac_to_verify.to_s)
7
+ end
8
+
9
+ # Calculate the HMAC for the given data and secret.
10
+ def self.calculated_hmac(body, secret)
11
+ digest = OpenSSL::Digest.new('sha256')
12
+ Base64.encode64(OpenSSL::HMAC.digest(digest, secret, body)).strip
13
+ end
14
+
15
+ end