disco_app 0.12.5
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.
- checksums.yaml +7 -0
- data/Rakefile +37 -0
- data/app/assets/components/disco_app/buttons/model-destroy-button.es6.jsx +31 -0
- data/app/assets/components/disco_app/forms/model-form.es6.jsx +64 -0
- data/app/assets/components/embedded_app/bar.es6.jsx +31 -0
- data/app/assets/components/shopify/buttons/_buttons.scss +547 -0
- data/app/assets/components/shopify/buttons/button.es6.jsx +15 -0
- data/app/assets/components/shopify/card/_card.scss +342 -0
- data/app/assets/components/shopify/card/card-header.es6.jsx +34 -0
- data/app/assets/components/shopify/card/card-section.es6.jsx +26 -0
- data/app/assets/components/shopify/card/card.es6.jsx +16 -0
- data/app/assets/components/shopify/image/_image.scss +82 -0
- data/app/assets/components/shopify/table/_table.scss +18 -0
- data/app/assets/components/shopify/typography/_typography.scss +23 -0
- data/app/assets/components/shopify/typography/ui-heading.es6.jsx +16 -0
- data/app/assets/components/shopify/ui-layout/_ui-layout.scss +157 -0
- data/app/assets/components/shopify/ui-layout/ui-annotated-section.es6.jsx +29 -0
- data/app/assets/components/shopify/ui-layout/ui-empty-state.es6.jsx +35 -0
- data/app/assets/components/shopify/ui-layout/ui-footer-help.es6.jsx +13 -0
- data/app/assets/components/shopify/ui-layout/ui-layout-item.es6.jsx +11 -0
- data/app/assets/components/shopify/ui-layout/ui-layout-section.es6.jsx +19 -0
- data/app/assets/components/shopify/ui-layout/ui-layout-sections.es6.jsx +11 -0
- data/app/assets/components/shopify/ui-layout/ui-layout.es6.jsx +11 -0
- data/app/assets/components/shopify/ui-layout/ui-page-actions.es6.jsx +13 -0
- data/app/assets/components/shopify/ui-layout/ui-page-actions__buttons.es6.jsx +27 -0
- data/app/assets/components/shopify/ui-stack/_ui-stack.scss +39 -0
- data/app/assets/components/shopify/ui-stack/ui-stack-item.es6.jsx +21 -0
- data/app/assets/components/shopify/ui-stack/ui-stack.es6.jsx +24 -0
- data/app/assets/images/disco_app/icon.svg +1 -0
- data/app/assets/images/disco_app/icons.svg +0 -0
- data/app/assets/javascripts/disco_app/components/custom/filterable_shop_list.js.jsx +61 -0
- data/app/assets/javascripts/disco_app/components/custom/inline-radio-options.es6.jsx +59 -0
- data/app/assets/javascripts/disco_app/components/custom/rules-editor.es6.jsx +432 -0
- data/app/assets/javascripts/disco_app/components/custom/shop_filter_query.js.jsx +13 -0
- data/app/assets/javascripts/disco_app/components/custom/shop_filter_tab.js.jsx +34 -0
- data/app/assets/javascripts/disco_app/components/custom/shop_filter_tabs.js.jsx +21 -0
- data/app/assets/javascripts/disco_app/components/custom/shop_list.js.jsx +142 -0
- data/app/assets/javascripts/disco_app/components/custom/shop_row.js.jsx +43 -0
- data/app/assets/javascripts/disco_app/components/custom/shopify_admin_link.js.jsx +29 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/base_form.es6.jsx +72 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/base_input.es6.jsx +20 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-checkbox.es6.jsx +30 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-radio.es6.jsx +30 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-select.es6.jsx +45 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-text.es6.jsx +69 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-textarea.es6.jsx +48 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-time.es6.jsx +7 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/ui-form__element.es6.jsx +17 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/ui-form__group.es6.jsx +11 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/forms/ui-form__section.es6.jsx +11 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/icons/icon-chevron.es6.jsx +33 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/icons/next-icon.es6.jsx +19 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/input_select.es6.jsx +21 -0
- data/app/assets/javascripts/disco_app/components/ui-kit/tables/table.es6.jsx +27 -0
- data/app/assets/javascripts/disco_app/components.js +3 -0
- data/app/assets/javascripts/disco_app/disco_app.js +9 -0
- data/app/assets/javascripts/disco_app/frame.js +152 -0
- data/app/assets/javascripts/disco_app/shopify-turbolinks.js +7 -0
- data/app/assets/javascripts/disco_app/ui-kit.js +1 -0
- data/app/assets/stylesheets/disco_app/admin/_header.scss +75 -0
- data/app/assets/stylesheets/disco_app/admin/_layout.scss +32 -0
- data/app/assets/stylesheets/disco_app/admin/_nav.scss +184 -0
- data/app/assets/stylesheets/disco_app/admin.scss +11 -0
- data/app/assets/stylesheets/disco_app/disco_app.scss +26 -0
- data/app/assets/stylesheets/disco_app/frame/_buttons.scss +54 -0
- data/app/assets/stylesheets/disco_app/frame/_forms.scss +26 -0
- data/app/assets/stylesheets/disco_app/frame/_layout.scss +77 -0
- data/app/assets/stylesheets/disco_app/frame/_type.scss +25 -0
- data/app/assets/stylesheets/disco_app/frame.scss +10 -0
- data/app/assets/stylesheets/disco_app/mixins/_flexbox.scss +400 -0
- data/app/assets/stylesheets/disco_app/ui-kit/_ui-forms.scss +69 -0
- data/app/assets/stylesheets/disco_app/ui-kit/_ui-icons.scss +28 -0
- data/app/assets/stylesheets/disco_app/ui-kit/_ui-kit.scss +5121 -0
- data/app/assets/stylesheets/disco_app/ui-kit/_ui-layout.scss +15 -0
- data/app/assets/stylesheets/disco_app/ui-kit/_ui-tabs.scss +75 -0
- data/app/assets/stylesheets/disco_app/ui-kit/_ui-type.scss +13 -0
- data/app/clients/disco_app/api_client.rb +27 -0
- data/app/clients/disco_app/disco_api_error.rb +2 -0
- data/app/controllers/disco_app/admin/app_settings_controller.rb +3 -0
- data/app/controllers/disco_app/admin/application_controller.rb +10 -0
- data/app/controllers/disco_app/admin/concerns/app_settings_controller.rb +24 -0
- data/app/controllers/disco_app/admin/concerns/authenticated_controller.rb +20 -0
- data/app/controllers/disco_app/admin/concerns/plans_controller.rb +54 -0
- data/app/controllers/disco_app/admin/concerns/shops_controller.rb +7 -0
- data/app/controllers/disco_app/admin/concerns/subscriptions_controller.rb +29 -0
- data/app/controllers/disco_app/admin/plans_controller.rb +3 -0
- data/app/controllers/disco_app/admin/resources/shops_controller.rb +3 -0
- data/app/controllers/disco_app/admin/shops_controller.rb +3 -0
- data/app/controllers/disco_app/admin/subscriptions_controller.rb +3 -0
- data/app/controllers/disco_app/charges_controller.rb +47 -0
- data/app/controllers/disco_app/concerns/app_proxy_controller.rb +40 -0
- data/app/controllers/disco_app/concerns/authenticated_controller.rb +71 -0
- data/app/controllers/disco_app/concerns/carrier_request_controller.rb +35 -0
- data/app/controllers/disco_app/frame_controller.rb +9 -0
- data/app/controllers/disco_app/install_controller.rb +27 -0
- data/app/controllers/disco_app/subscriptions_controller.rb +32 -0
- data/app/controllers/disco_app/webhooks_controller.rb +46 -0
- data/app/controllers/sessions_controller.rb +33 -0
- data/app/helpers/disco_app/application_helper.rb +68 -0
- data/app/jobs/disco_app/app_installed_job.rb +3 -0
- data/app/jobs/disco_app/app_uninstalled_job.rb +3 -0
- data/app/jobs/disco_app/concerns/app_installed_job.rb +39 -0
- data/app/jobs/disco_app/concerns/app_uninstalled_job.rb +21 -0
- data/app/jobs/disco_app/concerns/render_asset_group_job.rb +8 -0
- data/app/jobs/disco_app/concerns/shop_update_job.rb +13 -0
- data/app/jobs/disco_app/concerns/subscription_changed_job.rb +8 -0
- data/app/jobs/disco_app/concerns/synchronise_carrier_service_job.rb +55 -0
- data/app/jobs/disco_app/concerns/synchronise_resources_job.rb +12 -0
- data/app/jobs/disco_app/concerns/synchronise_webhooks_job.rb +52 -0
- data/app/jobs/disco_app/render_asset_group_job.rb +3 -0
- data/app/jobs/disco_app/send_subscription_job.rb +7 -0
- data/app/jobs/disco_app/shop_job.rb +27 -0
- data/app/jobs/disco_app/shop_update_job.rb +3 -0
- data/app/jobs/disco_app/subscription_changed_job.rb +3 -0
- data/app/jobs/disco_app/synchronise_carrier_service_job.rb +3 -0
- data/app/jobs/disco_app/synchronise_resources_job.rb +3 -0
- data/app/jobs/disco_app/synchronise_webhooks_job.rb +3 -0
- data/app/models/disco_app/app_settings.rb +3 -0
- data/app/models/disco_app/application_charge.rb +18 -0
- data/app/models/disco_app/concerns/app_settings.rb +7 -0
- data/app/models/disco_app/concerns/can_be_liquified.rb +45 -0
- data/app/models/disco_app/concerns/has_metafields.rb +48 -0
- data/app/models/disco_app/concerns/plan.rb +26 -0
- data/app/models/disco_app/concerns/plan_code.rb +15 -0
- data/app/models/disco_app/concerns/renders_assets.rb +166 -0
- data/app/models/disco_app/concerns/shop.rb +96 -0
- data/app/models/disco_app/concerns/subscription.rb +66 -0
- data/app/models/disco_app/concerns/synchronises.rb +58 -0
- data/app/models/disco_app/concerns/taggable.rb +16 -0
- data/app/models/disco_app/plan.rb +3 -0
- data/app/models/disco_app/plan_code.rb +3 -0
- data/app/models/disco_app/recurring_application_charge.rb +18 -0
- data/app/models/disco_app/session_storage.rb +18 -0
- data/app/models/disco_app/shop.rb +3 -0
- data/app/models/disco_app/subscription.rb +3 -0
- data/app/resources/disco_app/admin/resources/concerns/shop_resource.rb +100 -0
- data/app/resources/disco_app/admin/resources/shop_resource.rb +4 -0
- data/app/services/disco_app/carrier_request_service.rb +15 -0
- data/app/services/disco_app/charges_service.rb +81 -0
- data/app/services/disco_app/proxy_service.rb +17 -0
- data/app/services/disco_app/request_validation_service.rb +15 -0
- data/app/services/disco_app/subscription_service.rb +46 -0
- data/app/services/disco_app/webhook_service.rb +30 -0
- data/app/views/disco_app/admin/app_settings/edit.html.erb +5 -0
- data/app/views/disco_app/admin/plans/_form.html.erb +72 -0
- data/app/views/disco_app/admin/plans/_plan_code_fields.html.erb +15 -0
- data/app/views/disco_app/admin/plans/edit.html.erb +7 -0
- data/app/views/disco_app/admin/plans/index.html.erb +43 -0
- data/app/views/disco_app/admin/plans/new.html.erb +7 -0
- data/app/views/disco_app/admin/shops/index.html.erb +13 -0
- data/app/views/disco_app/admin/subscriptions/edit.html.erb +33 -0
- data/app/views/disco_app/charges/activate.html.erb +1 -0
- data/app/views/disco_app/charges/create.html.erb +1 -0
- data/app/views/disco_app/charges/new.html.erb +23 -0
- data/app/views/disco_app/frame/frame.html.erb +36 -0
- data/app/views/disco_app/install/installing.html.erb +22 -0
- data/app/views/disco_app/install/uninstalling.html.erb +1 -0
- data/app/views/disco_app/proxy_errors/404.html.erb +1 -0
- data/app/views/disco_app/shared/_card.html.erb +14 -0
- data/app/views/disco_app/shared/_icons.html.erb +1 -0
- data/app/views/disco_app/shared/_section.html.erb +17 -0
- data/app/views/disco_app/subscriptions/new.html.erb +25 -0
- data/app/views/layouts/admin/_nav_items.erb +20 -0
- data/app/views/layouts/admin.html.erb +67 -0
- data/app/views/layouts/application.html.erb +18 -0
- data/app/views/layouts/embedded_app.html.erb +50 -0
- data/app/views/layouts/embedded_app_modal.html.erb +28 -0
- data/app/views/shopify_app/sessions/new.html.erb +42 -0
- data/config/routes.rb +64 -0
- data/db/migrate/20150525000000_create_shops_if_not_existent.rb +110 -0
- data/lib/disco_app/configuration.rb +45 -0
- data/lib/disco_app/constants.rb +4 -0
- data/lib/disco_app/engine.rb +26 -0
- data/lib/disco_app/session.rb +14 -0
- data/lib/disco_app/support/file_fixtures.rb +23 -0
- data/lib/disco_app/test_help.rb +11 -0
- data/lib/disco_app/version.rb +3 -0
- data/lib/disco_app.rb +7 -0
- data/lib/generators/disco_app/USAGE +5 -0
- data/lib/generators/disco_app/disco_app_generator.rb +236 -0
- data/lib/generators/disco_app/templates/assets/javascripts/application.js +17 -0
- data/lib/generators/disco_app/templates/assets/javascripts/components.js +3 -0
- data/lib/generators/disco_app/templates/assets/stylesheets/application.scss +5 -0
- data/lib/generators/disco_app/templates/config/database.yml.tt +20 -0
- data/lib/generators/disco_app/templates/config/newrelic.yml +26 -0
- data/lib/generators/disco_app/templates/config/puma.rb +15 -0
- data/lib/generators/disco_app/templates/controllers/home_controller.rb +7 -0
- data/lib/generators/disco_app/templates/initializers/disco_app.rb +28 -0
- data/lib/generators/disco_app/templates/initializers/rollbar.rb +23 -0
- data/lib/generators/disco_app/templates/initializers/session_store.rb +2 -0
- data/lib/generators/disco_app/templates/initializers/shopify_app.rb +6 -0
- data/lib/generators/disco_app/templates/initializers/shopify_session_repository.rb +7 -0
- data/lib/generators/disco_app/templates/root/CHECKS +4 -0
- data/lib/generators/disco_app/templates/root/Procfile +2 -0
- data/lib/generators/disco_app/templates/views/home/index.html.erb +2 -0
- data/lib/tasks/api.rake +10 -0
- data/lib/tasks/carrier_service.rake +10 -0
- data/lib/tasks/database.rake +8 -0
- data/lib/tasks/sessions.rake +9 -0
- data/lib/tasks/shops.rake +10 -0
- data/lib/tasks/start.rake +3 -0
- data/lib/tasks/webhooks.rake +10 -0
- data/test/clients/disco_app/api_client_test.rb +22 -0
- data/test/controllers/disco_app/admin/shops_controller_test.rb +54 -0
- data/test/controllers/disco_app/charges_controller_test.rb +99 -0
- data/test/controllers/disco_app/install_controller_test.rb +50 -0
- data/test/controllers/disco_app/subscriptions_controller_test.rb +68 -0
- data/test/controllers/disco_app/webhooks_controller_test.rb +58 -0
- data/test/controllers/home_controller_test.rb +101 -0
- data/test/controllers/proxy_controller_test.rb +42 -0
- data/test/disco_app_test.rb +7 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +17 -0
- data/test/dummy/app/assets/stylesheets/application.scss +5 -0
- data/test/dummy/app/controllers/application_controller.rb +6 -0
- data/test/dummy/app/controllers/carrier_request_controller.rb +10 -0
- data/test/dummy/app/controllers/disco_app/admin/shops_controller.rb +8 -0
- data/test/dummy/app/controllers/home_controller.rb +7 -0
- data/test/dummy/app/controllers/proxy_controller.rb +8 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/jobs/carts_update_job.rb +7 -0
- data/test/dummy/app/jobs/disco_app/app_installed_job.rb +16 -0
- data/test/dummy/app/jobs/disco_app/app_uninstalled_job.rb +11 -0
- data/test/dummy/app/jobs/products_create_job.rb +7 -0
- data/test/dummy/app/jobs/products_delete_job.rb +7 -0
- data/test/dummy/app/jobs/products_update_job.rb +7 -0
- data/test/dummy/app/models/cart.rb +24 -0
- data/test/dummy/app/models/disco_app/shop.rb +20 -0
- data/test/dummy/app/models/js_configuration.rb +8 -0
- data/test/dummy/app/models/product.rb +9 -0
- data/test/dummy/app/models/widget_configuration.rb +10 -0
- data/test/dummy/app/views/assets/script_tag.js.erb +1 -0
- data/test/dummy/app/views/assets/test.js.erb +1 -0
- data/test/dummy/app/views/assets/widget.js.erb +2 -0
- data/test/dummy/app/views/assets/widget.scss.erb +3 -0
- data/test/dummy/app/views/home/index.html.erb +2 -0
- data/test/dummy/app/views/snippets/widget.liquid.erb +1 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config/application.rb +38 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.codeship.yml +23 -0
- data/test/dummy/config/database.gitlab-ci.yml +24 -0
- data/test/dummy/config/database.yml +20 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +85 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/disco_app.rb +28 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/omniauth.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +2 -0
- data/test/dummy/config/initializers/shopify_app.rb +6 -0
- data/test/dummy/config/initializers/shopify_session_repository.rb +7 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +11 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/migrate/20160307182229_create_products.rb +11 -0
- data/test/dummy/db/migrate/20160530160739_create_asset_models.rb +19 -0
- data/test/dummy/db/migrate/20161105054746_create_carts.rb +13 -0
- data/test/dummy/db/schema.rb +152 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/fixtures/api/subscriptions/valid_request.json +40 -0
- data/test/fixtures/api/widget_store/assets/create_script_tag_js_request.json +6 -0
- data/test/fixtures/api/widget_store/assets/create_script_tag_js_response.json +12 -0
- data/test/fixtures/api/widget_store/assets/create_script_tag_request.json +6 -0
- data/test/fixtures/api/widget_store/assets/create_script_tag_response.json +10 -0
- data/test/fixtures/api/widget_store/assets/create_test_js_request.json +6 -0
- data/test/fixtures/api/widget_store/assets/create_test_js_response.json +12 -0
- data/test/fixtures/api/widget_store/assets/create_widget_js_request.json +6 -0
- data/test/fixtures/api/widget_store/assets/create_widget_js_response.json +12 -0
- data/test/fixtures/api/widget_store/assets/create_widget_liquid_request.json +6 -0
- data/test/fixtures/api/widget_store/assets/create_widget_liquid_response.json +12 -0
- data/test/fixtures/api/widget_store/assets/create_widget_scss_request.json +6 -0
- data/test/fixtures/api/widget_store/assets/create_widget_scss_response.json +12 -0
- data/test/fixtures/api/widget_store/assets/get_script_tags_empty_request.json +1 -0
- data/test/fixtures/api/widget_store/assets/get_script_tags_empty_response.json +1 -0
- data/test/fixtures/api/widget_store/assets/get_script_tags_preexisting_request.json +1 -0
- data/test/fixtures/api/widget_store/assets/get_script_tags_preexisting_response.json +12 -0
- data/test/fixtures/api/widget_store/assets/update_script_tag_request.json +10 -0
- data/test/fixtures/api/widget_store/assets/update_script_tag_response.json +10 -0
- data/test/fixtures/api/widget_store/carrier_services.json +1 -0
- data/test/fixtures/api/widget_store/carrier_services_create.json +8 -0
- data/test/fixtures/api/widget_store/charges/activate_application_charge_request.json +16 -0
- data/test/fixtures/api/widget_store/charges/activate_application_charge_response.json +1 -0
- data/test/fixtures/api/widget_store/charges/activate_recurring_application_charge_request.json +20 -0
- data/test/fixtures/api/widget_store/charges/activate_recurring_application_charge_response.json +1 -0
- data/test/fixtures/api/widget_store/charges/create_application_charge_request.json +9 -0
- data/test/fixtures/api/widget_store/charges/create_application_charge_response.json +16 -0
- data/test/fixtures/api/widget_store/charges/create_recurring_application_charge_request.json +9 -0
- data/test/fixtures/api/widget_store/charges/create_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/charges/create_second_recurring_application_charge_request.json +9 -0
- data/test/fixtures/api/widget_store/charges/create_second_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/charges/get_accepted_application_charge_response.json +16 -0
- data/test/fixtures/api/widget_store/charges/get_accepted_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/charges/get_declined_application_charge_response.json +16 -0
- data/test/fixtures/api/widget_store/charges/get_declined_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/charges/get_pending_application_charge_response.json +16 -0
- data/test/fixtures/api/widget_store/charges/get_pending_recurring_application_charge_response.json +20 -0
- data/test/fixtures/api/widget_store/products/write_metafields_multiple_namespaces_request.json +31 -0
- data/test/fixtures/api/widget_store/products/write_metafields_multiple_namespaces_response.json +1 -0
- data/test/fixtures/api/widget_store/products/write_metafields_single_namespace_request.json +19 -0
- data/test/fixtures/api/widget_store/products/write_metafields_single_namespace_response.json +1 -0
- data/test/fixtures/api/widget_store/shop.json +46 -0
- data/test/fixtures/api/widget_store/webhooks.json +1 -0
- data/test/fixtures/assets/test.js +1 -0
- data/test/fixtures/assets/test.min.js +1 -0
- data/test/fixtures/carts.yml +5 -0
- data/test/fixtures/disco_app/application_charges.yml +11 -0
- data/test/fixtures/disco_app/plan_codes.yml +13 -0
- data/test/fixtures/disco_app/plans.yml +37 -0
- data/test/fixtures/disco_app/recurring_application_charges.yml +13 -0
- data/test/fixtures/disco_app/shops.yml +12 -0
- data/test/fixtures/disco_app/subscriptions.yml +25 -0
- data/test/fixtures/js_configurations.yml +3 -0
- data/test/fixtures/liquid/model.liquid +8 -0
- data/test/fixtures/products.yml +4 -0
- data/test/fixtures/webhooks/app_uninstalled.json +46 -0
- data/test/fixtures/webhooks/cart_updated.json +28 -0
- data/test/fixtures/webhooks/product_created.json +167 -0
- data/test/fixtures/webhooks/product_deleted.json +3 -0
- data/test/fixtures/webhooks/product_updated.json +167 -0
- data/test/fixtures/widget_configurations.yml +4 -0
- data/test/integration/synchronises_test.rb +62 -0
- data/test/jobs/disco_app/app_installed_job_test.rb +57 -0
- data/test/jobs/disco_app/app_uninstalled_job_test.rb +30 -0
- data/test/jobs/disco_app/send_subscription_job_test.rb +24 -0
- data/test/jobs/disco_app/synchronise_carrier_service_job_test.rb +25 -0
- data/test/jobs/disco_app/synchronise_webhooks_job_test.rb +30 -0
- data/test/models/disco_app/can_be_liquified_test.rb +55 -0
- data/test/models/disco_app/has_metafields_test.rb +40 -0
- data/test/models/disco_app/plan_test.rb +5 -0
- data/test/models/disco_app/renders_assets_test.rb +109 -0
- data/test/models/disco_app/session_test.rb +31 -0
- data/test/models/disco_app/shop_test.rb +43 -0
- data/test/models/disco_app/subscription_test.rb +19 -0
- data/test/services/disco_app/charges_service_test.rb +112 -0
- data/test/services/disco_app/subscription_service_test.rb +60 -0
- data/test/support/test_file_fixtures.rb +29 -0
- data/test/support/test_shopify_api.rb +16 -0
- data/test/test_helper.rb +58 -0
- metadata +987 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require 'render_anywhere'
|
|
2
|
+
require 'uglifier'
|
|
3
|
+
|
|
4
|
+
module DiscoApp::Concerns::RendersAssets
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
include RenderAnywhere
|
|
9
|
+
after_commit :queue_render_asset_group_job
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class_methods do
|
|
13
|
+
|
|
14
|
+
# Ruby "macro" that allows the definition of a number of Shopify assets that
|
|
15
|
+
# should be rendered and uploaded when certain attributes on the including
|
|
16
|
+
# class change. This assumes that the including class (1) is an ActiveRecord
|
|
17
|
+
# model that supports an `after_commit` callback; and (2) that the model has
|
|
18
|
+
# a `shop` method (or attribute) that references the DiscoApp::Shop instance
|
|
19
|
+
# associated with the current model.
|
|
20
|
+
#
|
|
21
|
+
# Options
|
|
22
|
+
#
|
|
23
|
+
# assets: Required. A list of asset templates to be rendered and
|
|
24
|
+
# uploaded to Shopify. The order of assets matters only
|
|
25
|
+
# in that subsequent asset templates will have access to
|
|
26
|
+
# the public CDN url of earlier-rendered assets through
|
|
27
|
+
# a "@public_urls" context variable.
|
|
28
|
+
#
|
|
29
|
+
# triggered_by: Optional. A list of attributes that should trigger the
|
|
30
|
+
# re-rendering and upload of the assets defined by the
|
|
31
|
+
# `assets` option, provided as a list of string. If
|
|
32
|
+
# empty or nil, nothing will be triggered.
|
|
33
|
+
#
|
|
34
|
+
# script_tags: Optional. A list of assets that should have script
|
|
35
|
+
# tags created or updated after being rendered to the
|
|
36
|
+
# storefront.
|
|
37
|
+
#
|
|
38
|
+
# minify: Optional. Whether Javascript assets should be minified
|
|
39
|
+
# after being rendered. Defaults to true in production
|
|
40
|
+
# environments, false otherwise. Note that stylesheet
|
|
41
|
+
# assets, when uploaded as .scss files, are
|
|
42
|
+
# automatically minified by Shopify, so we don't need to
|
|
43
|
+
# do it on our end.
|
|
44
|
+
#
|
|
45
|
+
def renders_assets(*asset_groups)
|
|
46
|
+
options = asset_groups.last.is_a?(Hash) ? asset_groups.pop : {}
|
|
47
|
+
options = renders_assets_default_options.merge(options)
|
|
48
|
+
|
|
49
|
+
# Ensure assets, triggered by and script tag options are arrays.
|
|
50
|
+
options[:assets] = options[:assets] ? Array(options[:assets]).map(&:to_sym) : []
|
|
51
|
+
options[:triggered_by] = options[:triggered_by] ? Array(options[:triggered_by]).map(&:to_s) : []
|
|
52
|
+
options[:script_tags] = options[:script_tags] ? Array(options[:script_tags]).map(&:to_sym) : []
|
|
53
|
+
|
|
54
|
+
asset_groups.each do |asset_group|
|
|
55
|
+
renderable_asset_groups[asset_group.to_sym] = options
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Return a list of renderable asset groups, along with their options.
|
|
60
|
+
def renderable_asset_groups
|
|
61
|
+
@renderable_asset_groups ||= {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Return the default options for the `renders_assets` macro.
|
|
65
|
+
def renders_assets_default_options
|
|
66
|
+
{
|
|
67
|
+
assets: nil,
|
|
68
|
+
triggered_by: nil,
|
|
69
|
+
script_tags: nil,
|
|
70
|
+
minify: Rails.env.production?
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Callback, triggered after a model save. Iterates through each asset group
|
|
77
|
+
# defined on the model and queues a render job if any of the changed
|
|
78
|
+
# attributes are found in the asset group's triggered_by list.
|
|
79
|
+
def queue_render_asset_group_job
|
|
80
|
+
renderable_asset_groups.each do |asset_group, options|
|
|
81
|
+
unless (previous_changes.keys & options[:triggered_by]).empty?
|
|
82
|
+
DiscoApp::RenderAssetGroupJob.perform_later(shop, self, asset_group.to_s)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Copies the class-level hash of assets with symbol asset name as keys and
|
|
88
|
+
# their corresponding options as values to the instance.
|
|
89
|
+
def renderable_asset_groups
|
|
90
|
+
@renderable_asset_groups ||= self.class.renderable_asset_groups.dup
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Render the specified asset group and upload the result to Shopify.
|
|
94
|
+
def render_asset_group(asset_group)
|
|
95
|
+
options = renderable_asset_groups[asset_group]
|
|
96
|
+
public_urls = {}.with_indifferent_access
|
|
97
|
+
|
|
98
|
+
options[:assets].each do |asset|
|
|
99
|
+
# Create/replace the asset via the Shopify API.
|
|
100
|
+
shopify_asset = shop.with_api_context {
|
|
101
|
+
ShopifyAPI::Asset.create(
|
|
102
|
+
key: asset,
|
|
103
|
+
value: render_asset_group_asset(asset, public_urls, options)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Store the public URL to this asset, so that we're able to use it in
|
|
108
|
+
# subsequent template renders. Adds a .css suffix to .scss assets, so that
|
|
109
|
+
# we use the Shopify-compiled version of any SCSS stylesheets.
|
|
110
|
+
if shopify_asset.public_url.present?
|
|
111
|
+
public_urls[asset] = shopify_asset.public_url.gsub(/\.scss\?/, '.scss.css?')
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# If we specified the creation of any script tags based on newly rendered
|
|
116
|
+
# assets, do that now.
|
|
117
|
+
unless options[:script_tags].empty?
|
|
118
|
+
render_asset_script_tags(options, public_urls)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
# Render an individual asset within an asset group.
|
|
125
|
+
def render_asset_group_asset(asset, public_urls, options)
|
|
126
|
+
rendered_asset = render_asset_renderer.render_to_string(
|
|
127
|
+
template: asset,
|
|
128
|
+
layout: nil,
|
|
129
|
+
locals: {
|
|
130
|
+
:"@#{self.class.name.underscore}" => self,
|
|
131
|
+
:@public_urls => public_urls
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if should_be_minified?(asset, options)
|
|
136
|
+
::Uglifier.compile(rendered_asset)
|
|
137
|
+
else
|
|
138
|
+
rendered_asset
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Return true if the given asset should be minified with Uglifier.
|
|
143
|
+
def should_be_minified?(asset, options)
|
|
144
|
+
asset.to_s.end_with?('.js') and options[:minify]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def render_asset_renderer
|
|
148
|
+
@render_asset_renderer ||= self.class.const_get('RenderingController').new
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Render any script tags defined by the :script_tags options that we have
|
|
152
|
+
# a public URL for.
|
|
153
|
+
def render_asset_script_tags(options, public_urls)
|
|
154
|
+
# Fetch the current set of script tags for the store.
|
|
155
|
+
current_script_tags = shop.with_api_context { ShopifyAPI::ScriptTag.find(:all) }
|
|
156
|
+
|
|
157
|
+
# Iterate each script tag for which we have a known public URL and create
|
|
158
|
+
# or update the corresponding script tag resource.
|
|
159
|
+
public_urls.slice(*options[:script_tags]).each do |asset, public_url|
|
|
160
|
+
script_tag = current_script_tags.find(lambda { ShopifyAPI::ScriptTag.new(event: 'onload') }) { |script_tag| script_tag.src.include?("#{asset}?") }
|
|
161
|
+
script_tag.src = public_url
|
|
162
|
+
shop.with_api_context { script_tag.save }
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
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 'with_api_context' for better readability, but also as 'temp' for
|
|
24
|
+
# backward compatibility.
|
|
25
|
+
alias_method :with_api_context, :with_shopify_session
|
|
26
|
+
alias_method :temp, :with_shopify_session
|
|
27
|
+
|
|
28
|
+
# Return true if the shop is considered as in development mode.
|
|
29
|
+
def development?
|
|
30
|
+
['staff', 'custom', 'affiliate'].include?(plan_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Convenience method to check if this shop has a current subscription.
|
|
34
|
+
def current_subscription?
|
|
35
|
+
current_subscription.present?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Convenience method to get the current subscription for this shop, if any.
|
|
39
|
+
def current_subscription
|
|
40
|
+
subscriptions.current.first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Convenience method to get the current plan for this shop, if any.
|
|
44
|
+
def current_plan
|
|
45
|
+
current_subscription&.plan
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Return the absolute URL to the shop's storefront.
|
|
49
|
+
def url
|
|
50
|
+
"#{protocol}://#{domain}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Return the protocol the shop's storefront uses. This should now always be
|
|
54
|
+
# https as all Shopify stores have SSL enabled.
|
|
55
|
+
def protocol
|
|
56
|
+
'https'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Return the absolute URL to the shop's admin.
|
|
60
|
+
def admin_url
|
|
61
|
+
"https://#{shopify_domain}/admin"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Convenience method to get the email of the shop's admin, to display in Rollbar.
|
|
65
|
+
def email
|
|
66
|
+
self.data['email']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def installed_duration
|
|
70
|
+
distance_of_time_in_words_to_now(created_at.time)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Return the shop's configured timezone. If none can be parsed from the
|
|
74
|
+
# shop's "data" hash, return the default Rails zone (which should be UTC).
|
|
75
|
+
def time_zone
|
|
76
|
+
@time_zone ||= begin
|
|
77
|
+
Time.find_zone!(data['timezone'].to_s.gsub(/^\(.+\)\s/, ''))
|
|
78
|
+
rescue ArgumentError
|
|
79
|
+
Time.zone
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Return the shop's configured locale as a symbol. If none exists for some
|
|
84
|
+
# reason, 'en' is returned.
|
|
85
|
+
def locale
|
|
86
|
+
(data['primary_locale'] || 'en').to_sym
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Return an instance of the Disco API client.
|
|
90
|
+
def disco_api_client
|
|
91
|
+
@api_client ||= DiscoApp::ApiClient.new(self, ENV['DISCO_API_URL'])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
def as_json(options = {})
|
|
51
|
+
super.merge(
|
|
52
|
+
'active_charge' => active_charge
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# If the amount or trial period for this subscription changes, clear any
|
|
59
|
+
# active charge, as the user will need to re-authorize the charge.
|
|
60
|
+
def cancel_charge
|
|
61
|
+
return if (previous_changes.keys & ['amount', 'trial_period_days']).empty?
|
|
62
|
+
return unless active_charge?
|
|
63
|
+
active_charge.cancelled!
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module DiscoApp::Concerns::Synchronises
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
class_methods do
|
|
5
|
+
|
|
6
|
+
# Define the number of resources per page to fetch.
|
|
7
|
+
SYNCHRONISES_PAGE_LIMIT = 250
|
|
8
|
+
|
|
9
|
+
def should_synchronise?(shop, data)
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def synchronise_by(shop, data)
|
|
14
|
+
{ id: data[:id] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def synchronise(shop, data)
|
|
18
|
+
data = data.with_indifferent_access
|
|
19
|
+
|
|
20
|
+
return unless should_synchronise?(shop, data)
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
instance = self.find_or_create_by!(self.synchronise_by(shop, data)) do |instance|
|
|
24
|
+
instance.shop = shop
|
|
25
|
+
instance.data = data
|
|
26
|
+
end
|
|
27
|
+
rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
|
|
28
|
+
retry
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
instance.update(data: data)
|
|
32
|
+
|
|
33
|
+
instance
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def should_synchronise_deletion?(shop, data)
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def synchronise_deletion(shop, data)
|
|
41
|
+
data = data.with_indifferent_access
|
|
42
|
+
|
|
43
|
+
return unless should_synchronise_deletion?(shop, data)
|
|
44
|
+
|
|
45
|
+
self.destroy_all(shop: shop, id: data[:id])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def synchronise_all(shop, params = {})
|
|
49
|
+
resource_count = shop.with_api_context { self::SHOPIFY_API_CLASS.count(params) }
|
|
50
|
+
|
|
51
|
+
(1..(resource_count / SYNCHRONISES_PAGE_LIMIT.to_f).ceil).each do |page|
|
|
52
|
+
DiscoApp::SynchroniseResourcesJob.perform_later(shop, self.name, params.merge(page: page, limit: SYNCHRONISES_PAGE_LIMIT))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module DiscoApp::Concerns::Taggable
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
def tags
|
|
5
|
+
data['tags'].split(',').map(&:strip)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def add_tag(tag)
|
|
9
|
+
data['tags'] = (tags + [tag]).uniq.join(',')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def remove_tag(tag)
|
|
13
|
+
data['tags'] = (tags - [tag]).uniq.join(',')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
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,100 @@
|
|
|
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, :created_at
|
|
9
|
+
attributes :email, :country_name, :currency, :plan_display_name
|
|
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 :query, :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 :query
|
|
30
|
+
return records.where('name LIKE ? OR shopify_domain LIKE ? OR domain LIKE ?', "%#{value.first}%", "%#{value.first}%", "%#{value.first}%")
|
|
31
|
+
when :status
|
|
32
|
+
return records.where(status: value.map { |v| DiscoApp::Shop.statuses[v.to_sym] } )
|
|
33
|
+
else
|
|
34
|
+
return super(records, filter, value)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Don't allow the update of any fields via the API.
|
|
39
|
+
def self.updatable_fields(context)
|
|
40
|
+
[]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Don't allow the creation of any fields via the API.
|
|
44
|
+
def self.creatable_fields(context)
|
|
45
|
+
[]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def email
|
|
49
|
+
@model.data['email']
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def country_name
|
|
53
|
+
@model.data['country_name']
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def currency
|
|
57
|
+
@model.data['currency']
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def plan_display_name
|
|
61
|
+
@model.data['plan_display_name']
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def current_subscription_id
|
|
65
|
+
if @model.current_subscription?
|
|
66
|
+
@model.current_subscription.id
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def current_subscription_display_amount
|
|
71
|
+
if @model.current_subscription?
|
|
72
|
+
@model.current_subscription.amount
|
|
73
|
+
else
|
|
74
|
+
'-'
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def current_subscription_display_plan
|
|
79
|
+
if @model.current_subscription?
|
|
80
|
+
@model.current_plan.name
|
|
81
|
+
else
|
|
82
|
+
'None'
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def current_subscription_display_plan_code
|
|
87
|
+
@model.current_subscription&.plan_code&.code
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def current_subscription_source
|
|
91
|
+
if @model.current_subscription?
|
|
92
|
+
@model.current_subscription.source || '-'
|
|
93
|
+
else
|
|
94
|
+
'-'
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
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.with_api_context {
|
|
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.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.with_api_context {
|
|
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.with_api_context {
|
|
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
|