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,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
9
+
10
+ accepts_nested_attributes_for :plan_codes
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,48 @@
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
+ end
19
+
20
+ # Only require an active charge if the amount to be charged is > 0.
21
+ def requires_active_charge?
22
+ amount > 0
23
+ end
24
+
25
+ # Convenience method to check if this subscription has an active charge.
26
+ def active_charge?
27
+ active_charge.present?
28
+ end
29
+
30
+ # Convenience method to get the active charge for this subscription.
31
+ def active_charge
32
+ charges.active.first
33
+ end
34
+
35
+ # Return the appropriate set of charges for this subscription's type.
36
+ def charges
37
+ recurring? ? recurring_charges : one_time_charges
38
+ end
39
+
40
+ def charge_class
41
+ recurring? ? DiscoApp::RecurringApplicationCharge : DiscoApp::ApplicationCharge
42
+ end
43
+
44
+ def shopify_charge_class
45
+ recurring? ? ShopifyAPI::RecurringApplicationCharge : ShopifyAPI::ApplicationCharge
46
+ end
47
+
48
+ 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,46 @@
1
+ require 'jsonapi/resource'
2
+
3
+ module DiscoApp::Admin::Resources::Concerns::ShopResource
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+
8
+ attributes :shopify_domain, :status, :email, :country_name
9
+ attributes :currency, :domain, :plan_display_name, :created_at
10
+ attributes :installed_duration
11
+
12
+ model_name 'DiscoApp::Shop'
13
+
14
+ filters :status
15
+
16
+ # Adjust the base records method to ensure only models for the authenticated domain are retrieved.
17
+ def self.records(options = {})
18
+ records = DiscoApp::Shop.order(created_at: :desc)
19
+ records
20
+ end
21
+
22
+ # Apply filters.
23
+ def self.apply_filter(records, filter, value, options)
24
+ return records if value.blank?
25
+
26
+ # Perform appropriate filtering.
27
+ case filter
28
+ when :status
29
+ return records.where(status: value.map { |v| DiscoApp::Shop.statuses[v.to_sym] } )
30
+ else
31
+ return super(records, filter, value)
32
+ end
33
+ end
34
+
35
+ # Don't allow the update of any fields via the API.
36
+ def self.updatable_fields(context)
37
+ []
38
+ end
39
+
40
+ # Don't allow the creation of any fields via the API.
41
+ def self.creatable_fields(context)
42
+ []
43
+ end
44
+
45
+ end
46
+ 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
@@ -0,0 +1,81 @@
1
+ class DiscoApp::ChargesService
2
+
3
+ # Create the appropriate type of Shopify charge for the given subscription
4
+ # (either one-time or recurring) and return.
5
+ def self.create(shop, subscription)
6
+ # Create the charge object locally first.
7
+ charge = subscription.charge_class.create!(
8
+ shop: shop,
9
+ subscription: subscription,
10
+ )
11
+
12
+ # Create the charge object on Shopify.
13
+ shopify_charge = shop.temp {
14
+ subscription.shopify_charge_class.create(
15
+ name: subscription.plan.name,
16
+ price: '%.2f' % (subscription.amount.to_f / 100.0),
17
+ trial_days: subscription.plan.has_trial? ? subscription.plan.trial_period_days : nil,
18
+ return_url: charge.activate_url,
19
+ test: !DiscoApp.configuration.real_charges?
20
+ )
21
+ }
22
+
23
+ # If we couldn't create the charge on Shopify, return nil.
24
+ if shopify_charge.nil?
25
+ return nil
26
+ end
27
+
28
+ # Update the local record of the charge from Shopify's created charge, then
29
+ # return it.
30
+ charge.update(
31
+ shopify_id: shopify_charge.id,
32
+ confirmation_url: shopify_charge.confirmation_url
33
+ )
34
+ charge
35
+ end
36
+
37
+ # Attempt to activate the given Shopify charge for the given Shop using the
38
+ # Shopify API. Returns true on successful activation, false otherwise.
39
+ def self.activate(shop, charge)
40
+ begin
41
+ # Start by fetching the Shopify charge to check that it was accepted.
42
+ shopify_charge = shop.temp {
43
+ charge.subscription.shopify_charge_class.find(charge.shopify_id)
44
+ }
45
+
46
+ # Update the status of the local charge based on the Shopify charge.
47
+ charge.send("#{shopify_charge.status}!") if charge.respond_to? "#{shopify_charge.status}!"
48
+
49
+ # If the charge wasn't accepted, fail and return.
50
+ return false unless charge.accepted?
51
+
52
+ # If the charge was indeed accepted, activate it via Shopify.
53
+ charge.shop.temp {
54
+ shopify_charge.activate
55
+ }
56
+
57
+ # If the charge was recurring, make sure that all other local recurring
58
+ # charges are marked inactive.
59
+ if charge.recurring?
60
+ self.cancel_recurring_charges(shop, charge)
61
+ end
62
+
63
+ charge.active!
64
+
65
+ true
66
+ rescue
67
+ false
68
+ end
69
+ end
70
+
71
+ # Cancel all recurring charges for the given shop. If the optional charge
72
+ # parameter is given, it will be excluded from the cancellation.
73
+ def self.cancel_recurring_charges(shop, charge = nil)
74
+ charges = DiscoApp::RecurringApplicationCharge.where(shop: shop)
75
+ if charge.present?
76
+ charges = charges.where.not(id: charge)
77
+ end
78
+ charges.update_all(status: DiscoApp::RecurringApplicationCharge.statuses[:cancelled])
79
+ end
80
+
81
+ end
@@ -0,0 +1,17 @@
1
+ class DiscoApp::ProxyService
2
+
3
+ # Return true iff the signature provided in the given query string matches
4
+ # that calculated from the remaining query parameters and the given secret.
5
+ def self.proxy_signature_is_valid?(query_string, secret)
6
+ query_hash = Rack::Utils.parse_query(query_string)
7
+ signature = query_hash.delete('signature').to_s
8
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(self.calculated_signature(query_hash, secret), signature)
9
+ end
10
+
11
+ # Return the calculated signature for the given query hash and secret.
12
+ def self.calculated_signature(query_hash, secret)
13
+ sorted_params = query_hash.collect{ |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join
14
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, sorted_params)
15
+ end
16
+
17
+ end
@@ -0,0 +1,37 @@
1
+ class DiscoApp::SubscriptionService
2
+
3
+ # Subscribe the given shop to the given plan, optionally using the given plan
4
+ # code.
5
+ def self.subscribe(shop, plan, plan_code = nil)
6
+ # Cancel any existing current subscriptions.
7
+ shop.subscriptions.current.update_all(
8
+ status: DiscoApp::Subscription.statuses[:cancelled],
9
+ cancelled_at: Time.now
10
+ )
11
+
12
+ # Get the amount that should be charged for the subscription.
13
+ subscription_amount = plan_code.present? ? plan_code.amount : plan.amount
14
+
15
+ # Get the date the subscription trial should end.
16
+ subscription_trial_end_at = plan.has_trial? ? (plan_code.present? ? plan_code.trial_period_days: plan.trial_period_days).days.from_now : nil
17
+
18
+ # Create the new subscription.
19
+ new_subscription = DiscoApp::Subscription.create!(
20
+ shop: shop,
21
+ plan: plan,
22
+ plan_code: plan_code,
23
+ status: DiscoApp::Subscription.statuses[plan.has_trial? ? :trial : :active],
24
+ subscription_type: plan.plan_type,
25
+ amount: subscription_amount,
26
+ trial_start_at: plan.has_trial? ? Time.now : nil,
27
+ trial_end_at: plan.has_trial? ? subscription_trial_end_at : nil
28
+ )
29
+
30
+ # Enqueue the subscription changed background job.
31
+ DiscoApp::SubscriptionChangedJob.perform_later(shop.shopify_domain, new_subscription)
32
+
33
+ # Return the new subscription.
34
+ new_subscription
35
+ end
36
+
37
+ end
@@ -0,0 +1,30 @@
1
+ class DiscoApp::WebhookService
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
+ # Try to find a job class for the given webhook topic.
16
+ def self.find_job_class(topic)
17
+ begin
18
+ # First try to find a top-level matching job class.
19
+ "#{topic}_job".gsub('/', '_').classify.constantize
20
+ rescue NameError
21
+ # If that fails, try to find a DiscoApp:: prefixed job class.
22
+ begin
23
+ %Q{DiscoApp::#{"#{topic}_job".gsub('/', '_').classify}}.constantize
24
+ rescue NameError
25
+ nil
26
+ end
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,5 @@
1
+ <% provide(:title, 'App Settings') %>
2
+
3
+ <p>Update 'app/views/disco_app/admin/app_settings/edit.html.erb' on your own rails app.</p>
4
+
5
+
@@ -0,0 +1,27 @@
1
+ <%= f.label(:name, 'Name') %>
2
+ <%= f.text_field(:name) %>
3
+
4
+ <%= f.label(:status, 'Status') %>
5
+ <%= f.select(:status, DiscoApp::Plan.statuses.map { |s| [s.first.humanize, s.first] }) %>
6
+
7
+ <%= f.label(:plan_type, 'Plan Type') %>
8
+ <%= f.select(:plan_type, DiscoApp::Plan.plan_types.map { |s| [s.first.humanize, s.first] }) %>
9
+
10
+ <%= f.label(:trial_period_days, 'Trial Period Days') %>
11
+ <%= f.number_field(:trial_period_days) %>
12
+
13
+ <%= f.label(:amount, 'Amount') %>
14
+ <%= f.number_field(:amount) %>
15
+
16
+ <%= f.fields_for :plan_codes do |plan_code| %>
17
+ <%= plan_code.label(:code, 'Code') %>
18
+ <%= plan_code.text_field(:code) %>
19
+
20
+ <%= plan_code.label(:trial_period_days, 'Trial Period Days') %>
21
+ <%= plan_code.number_field(:trial_period_days) %>
22
+
23
+ <%= plan_code.label(:amount, 'Amount') %>
24
+ <%= plan_code.number_field(:amount) %>
25
+ <% end %>
26
+
27
+ <%= f.submit %>
@@ -0,0 +1,7 @@
1
+ <% provide(:title, 'Edit Plan') %>
2
+
3
+ <%= form_for(@plan, url: admin_plan_path(@plan)) do |f| %>
4
+ <%= render 'form', f: f %>
5
+ <% end %>
6
+
7
+ <%= link_to 'Back', admin_plans_path %>
@@ -0,0 +1,32 @@
1
+ <% provide(:title, 'Plans') %>
2
+
3
+ <table class="table">
4
+ <thead>
5
+ <tr>
6
+ <th>Name</th>
7
+ <th>Status</th>
8
+ <th>Plan Type</th>
9
+ <th>Trial Period</th>
10
+ <th>Amount</th>
11
+ <th>Currency</th>
12
+ <th>Interval</th>
13
+ <th>Interval Count</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% @plans.each do |plan| %>
18
+ <tr>
19
+ <td><%= link_to(plan.name, edit_admin_plan_path(plan)) %></td>
20
+ <td><%= plan.status %></td>
21
+ <td><%= plan.plan_type %></td>
22
+ <td><%= plan.trial_period_days %></td>
23
+ <td><%= plan.amount %></td>
24
+ <td><%= plan.currency %></td>
25
+ <td><%= plan.interval %></td>
26
+ <td><%= plan.interval_count %></td>
27
+ </tr>
28
+ <% end %>
29
+ </tbody>
30
+ </table>
31
+
32
+ <%= link_to 'Create new plan', new_admin_plan_path %>
@@ -0,0 +1,7 @@
1
+ <% provide(:title, 'New Plan') %>
2
+
3
+ <%= form_for(@plan, url: admin_plans_path) do |f| %>
4
+ <%= render 'form', f: f %>
5
+ <% end %>
6
+
7
+ <%= link_to 'Back', admin_plans_path %>
@@ -0,0 +1,12 @@
1
+ <% provide(:title, 'Shops') %>
2
+
3
+ <div class="next-grid">
4
+ <div class="next-grid__cell">
5
+
6
+ <%= react_component('FilterableShopList', {
7
+ shopsUrl: admin_resources_shops_path(format: :json),
8
+ editShopUrl: edit_admin_shop_path(':id')
9
+ }) %>
10
+
11
+ </div>
12
+ </div>
@@ -0,0 +1 @@
1
+ activate_charge
@@ -0,0 +1 @@
1
+ create_charge
@@ -0,0 +1,12 @@
1
+ <% provide(:title, 'Thankyou') %>
2
+
3
+ <p>
4
+ Thanks for installing <%= DiscoApp.configuration.app_name %>!
5
+ </p>
6
+ <p>
7
+ Before we start setting things up, we need you to authorize a charge for the application.
8
+ </p>
9
+
10
+ <%= form_tag disco_app.subscription_charges_path(@subscription), method: 'POST', target: '_parent' do %>
11
+ <%= submit_tag 'Okay', class: 'form-input' %>
12
+ <% end %>