ruby_shopify_app 1.0.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 (186) hide show
  1. checksums.yaml +7 -0
  2. data/.babelrc +5 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/ISSUE_TEMPLATE/bug-report.md +63 -0
  5. data/.github/ISSUE_TEMPLATE/config.yml +1 -0
  6. data/.github/ISSUE_TEMPLATE/feature-request.md +33 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  8. data/.github/probots.yml +2 -0
  9. data/.github/workflows/build.yml +40 -0
  10. data/.github/workflows/release.yml +24 -0
  11. data/.github/workflows/rubocop.yml +22 -0
  12. data/.gitignore +14 -0
  13. data/.nvmrc +1 -0
  14. data/.rubocop.yml +18 -0
  15. data/.ruby-version +1 -0
  16. data/CHANGELOG-OLD.md +643 -0
  17. data/CHANGELOG.md +6 -0
  18. data/CONTRIBUTING.md +81 -0
  19. data/Gemfile +11 -0
  20. data/Gemfile.lock +280 -0
  21. data/LICENSE +19 -0
  22. data/README.md +132 -0
  23. data/Rakefile +7 -0
  24. data/SECURITY.md +59 -0
  25. data/app/assets/images/storage_access.svg +1 -0
  26. data/app/assets/javascripts/shopify_app/app_bridge_2.0.12.js +10 -0
  27. data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +22 -0
  28. data/app/assets/javascripts/shopify_app/enable_cookies.js +3 -0
  29. data/app/assets/javascripts/shopify_app/itp_helper.js +40 -0
  30. data/app/assets/javascripts/shopify_app/partition_cookies.js +8 -0
  31. data/app/assets/javascripts/shopify_app/post_redirect.js +9 -0
  32. data/app/assets/javascripts/shopify_app/redirect.js +31 -0
  33. data/app/assets/javascripts/shopify_app/request_storage_access.js +3 -0
  34. data/app/assets/javascripts/shopify_app/storage_access.js +148 -0
  35. data/app/assets/javascripts/shopify_app/storage_access_redirect.js +17 -0
  36. data/app/assets/javascripts/shopify_app/top_level.js +2 -0
  37. data/app/assets/javascripts/shopify_app/top_level_interaction.js +11 -0
  38. data/app/controllers/concerns/shopify_app/authenticated.rb +16 -0
  39. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +39 -0
  40. data/app/controllers/concerns/shopify_app/require_known_shop.rb +40 -0
  41. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +32 -0
  42. data/app/controllers/shopify_app/authenticated_controller.rb +8 -0
  43. data/app/controllers/shopify_app/callback_controller.rb +195 -0
  44. data/app/controllers/shopify_app/extension_verification_controller.rb +15 -0
  45. data/app/controllers/shopify_app/sessions_controller.rb +202 -0
  46. data/app/controllers/shopify_app/webhooks_controller.rb +36 -0
  47. data/app/views/shopify_app/partials/_button_styles.html.erb +109 -0
  48. data/app/views/shopify_app/partials/_card_styles.html.erb +33 -0
  49. data/app/views/shopify_app/partials/_empty_state_styles.html.erb +98 -0
  50. data/app/views/shopify_app/partials/_form_styles.html.erb +56 -0
  51. data/app/views/shopify_app/partials/_layout_styles.html.erb +182 -0
  52. data/app/views/shopify_app/partials/_typography_styles.html.erb +35 -0
  53. data/app/views/shopify_app/sessions/enable_cookies.html.erb +70 -0
  54. data/app/views/shopify_app/sessions/new.html.erb +51 -0
  55. data/app/views/shopify_app/sessions/request_storage_access.html.erb +68 -0
  56. data/app/views/shopify_app/sessions/top_level_interaction.html.erb +63 -0
  57. data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +13 -0
  58. data/app/views/shopify_app/shared/redirect.html.erb +23 -0
  59. data/config/locales/cs.yml +23 -0
  60. data/config/locales/da.yml +20 -0
  61. data/config/locales/de.yml +22 -0
  62. data/config/locales/en.yml +15 -0
  63. data/config/locales/es.yml +22 -0
  64. data/config/locales/fi.yml +20 -0
  65. data/config/locales/fr.yml +23 -0
  66. data/config/locales/it.yml +21 -0
  67. data/config/locales/ja.yml +17 -0
  68. data/config/locales/ko.yml +19 -0
  69. data/config/locales/nb.yml +21 -0
  70. data/config/locales/nl.yml +21 -0
  71. data/config/locales/pl.yml +21 -0
  72. data/config/locales/pt-BR.yml +21 -0
  73. data/config/locales/pt-PT.yml +22 -0
  74. data/config/locales/sv.yml +21 -0
  75. data/config/locales/th.yml +20 -0
  76. data/config/locales/tr.yml +22 -0
  77. data/config/locales/vi.yml +22 -0
  78. data/config/locales/zh-CN.yml +16 -0
  79. data/config/locales/zh-TW.yml +16 -0
  80. data/config/routes.rb +23 -0
  81. data/docs/Quickstart.md +31 -0
  82. data/docs/Releasing.md +21 -0
  83. data/docs/Troubleshooting.md +159 -0
  84. data/docs/Upgrading.md +132 -0
  85. data/docs/shopify_app/authentication.md +124 -0
  86. data/docs/shopify_app/engine.md +82 -0
  87. data/docs/shopify_app/generators.md +127 -0
  88. data/docs/shopify_app/handling-access-scopes-changes.md +24 -0
  89. data/docs/shopify_app/script-tags.md +28 -0
  90. data/docs/shopify_app/session-repository.md +88 -0
  91. data/docs/shopify_app/testing.md +38 -0
  92. data/docs/shopify_app/webhooks.md +72 -0
  93. data/images/app-proxy-screenshot.png +0 -0
  94. data/karma.conf.js +44 -0
  95. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +47 -0
  96. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +11 -0
  97. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +40 -0
  98. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +62 -0
  99. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +69 -0
  100. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +13 -0
  101. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +26 -0
  102. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +8 -0
  103. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +11 -0
  104. data/lib/generators/shopify_app/app_proxy_controller/templates/index.html.erb +19 -0
  105. data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +15 -0
  106. data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +5 -0
  107. data/lib/generators/shopify_app/controllers/controllers_generator.rb +30 -0
  108. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +53 -0
  109. data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +18 -0
  110. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +75 -0
  111. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +12 -0
  112. data/lib/generators/shopify_app/install/install_generator.rb +121 -0
  113. data/lib/generators/shopify_app/install/templates/_flash_messages.html.erb +3 -0
  114. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +44 -0
  115. data/lib/generators/shopify_app/install/templates/flash_messages.js +24 -0
  116. data/lib/generators/shopify_app/install/templates/omniauth.rb +4 -0
  117. data/lib/generators/shopify_app/install/templates/session_store.rb +4 -0
  118. data/lib/generators/shopify_app/install/templates/shopify_app.js +15 -0
  119. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +25 -0
  120. data/lib/generators/shopify_app/install/templates/shopify_app_importmap.js +13 -0
  121. data/lib/generators/shopify_app/install/templates/shopify_app_index.js +2 -0
  122. data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +8 -0
  123. data/lib/generators/shopify_app/install/templates/user_agent.rb +6 -0
  124. data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +19 -0
  125. data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +8 -0
  126. data/lib/generators/shopify_app/rotate_shopify_token_job/rotate_shopify_token_job_generator.rb +16 -0
  127. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +17 -0
  128. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +42 -0
  129. data/lib/generators/shopify_app/routes/routes_generator.rb +32 -0
  130. data/lib/generators/shopify_app/routes/templates/routes.rb +12 -0
  131. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +70 -0
  132. data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +5 -0
  133. data/lib/generators/shopify_app/shop_model/templates/db/migrate/create_shops.erb +15 -0
  134. data/lib/generators/shopify_app/shop_model/templates/shop.rb +8 -0
  135. data/lib/generators/shopify_app/shop_model/templates/shops.yml +3 -0
  136. data/lib/generators/shopify_app/shopify_app_generator.rb +18 -0
  137. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +5 -0
  138. data/lib/generators/shopify_app/user_model/templates/db/migrate/create_users.erb +16 -0
  139. data/lib/generators/shopify_app/user_model/templates/user.rb +8 -0
  140. data/lib/generators/shopify_app/user_model/templates/users.yml +4 -0
  141. data/lib/generators/shopify_app/user_model/user_model_generator.rb +70 -0
  142. data/lib/generators/shopify_app/views/views_generator.rb +30 -0
  143. data/lib/shopify_app/access_scopes/noop_strategy.rb +13 -0
  144. data/lib/shopify_app/access_scopes/shop_strategy.rb +24 -0
  145. data/lib/shopify_app/access_scopes/user_strategy.rb +41 -0
  146. data/lib/shopify_app/configuration.rb +119 -0
  147. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +38 -0
  148. data/lib/shopify_app/controller_concerns/csrf_protection.rb +15 -0
  149. data/lib/shopify_app/controller_concerns/embedded_app.rb +20 -0
  150. data/lib/shopify_app/controller_concerns/itp.rb +45 -0
  151. data/lib/shopify_app/controller_concerns/localization.rb +23 -0
  152. data/lib/shopify_app/controller_concerns/login_protection.rb +259 -0
  153. data/lib/shopify_app/controller_concerns/payload_verification.rb +24 -0
  154. data/lib/shopify_app/controller_concerns/webhook_verification.rb +23 -0
  155. data/lib/shopify_app/engine.rb +47 -0
  156. data/lib/shopify_app/jobs/scripttags_manager_job.rb +16 -0
  157. data/lib/shopify_app/jobs/webhooks_manager_job.rb +16 -0
  158. data/lib/shopify_app/managers/scripttags_manager.rb +78 -0
  159. data/lib/shopify_app/managers/webhooks_manager.rb +62 -0
  160. data/lib/shopify_app/middleware/jwt_middleware.rb +43 -0
  161. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +34 -0
  162. data/lib/shopify_app/omniauth/omniauth_configuration.rb +64 -0
  163. data/lib/shopify_app/session/in_memory_session_store.rb +31 -0
  164. data/lib/shopify_app/session/in_memory_shop_session_store.rb +16 -0
  165. data/lib/shopify_app/session/in_memory_user_session_store.rb +16 -0
  166. data/lib/shopify_app/session/jwt.rb +67 -0
  167. data/lib/shopify_app/session/null_user_session_store.rb +22 -0
  168. data/lib/shopify_app/session/session_repository.rb +56 -0
  169. data/lib/shopify_app/session/session_storage.rb +20 -0
  170. data/lib/shopify_app/session/shop_session_storage.rb +42 -0
  171. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +58 -0
  172. data/lib/shopify_app/session/user_session_storage.rb +42 -0
  173. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +58 -0
  174. data/lib/shopify_app/test_helpers/all.rb +2 -0
  175. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +17 -0
  176. data/lib/shopify_app/utils.rb +37 -0
  177. data/lib/shopify_app/version.rb +4 -0
  178. data/lib/shopify_app.rb +80 -0
  179. data/package.json +27 -0
  180. data/service.yml +4 -0
  181. data/shipit.rubygems.yml +4 -0
  182. data/shopify_app.gemspec +39 -0
  183. data/translation.yml +7 -0
  184. data/webpack.config.js +24 -0
  185. data/yarn.lock +5230 -0
  186. metadata +465 -0
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HomeController < AuthenticatedController
4
+ include ShopifyApp::ShopAccessScopesVerification
5
+
6
+ before_action :set_host
7
+
8
+ def index
9
+ @products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
10
+ @webhooks = ShopifyAPI::Webhook.find(:all)
11
+ end
12
+
13
+ private
14
+
15
+ def set_host
16
+ @host = params[:host]
17
+ end
18
+ end
@@ -0,0 +1,75 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <link
7
+ rel="stylesheet"
8
+ href="https://unpkg.com/@shopify/polaris@4.25.0/styles.min.css"
9
+ />
10
+ <% unless with_cookie_authentication? %> <script>
11
+ document.addEventListener("DOMContentLoaded", async function() {
12
+ <% if ShopifyApp.use_importmap? %>
13
+ await import("lib/shopify_app")
14
+ <% end %>
15
+
16
+ var SessionToken = window["app-bridge"].actions.SessionToken
17
+ var app = window.app;
18
+
19
+ app.dispatch(
20
+ SessionToken.request(),
21
+ );
22
+
23
+ // Save a session token for future requests
24
+ window.sessionToken = await new Promise((resolve) => {
25
+ app.subscribe(SessionToken.Action.RESPOND, (data) => {
26
+ resolve(data.sessionToken || "");
27
+ });
28
+ });
29
+
30
+ var fetchProducts = function() {
31
+ var headers = new Headers({ "Authorization": "Bearer " + window.sessionToken });
32
+ return fetch("/products", { headers })
33
+ .then(response => response.json())
34
+ .then(data => {
35
+ var products = data.products;
36
+
37
+ if (products === undefined || products.length == 0) {
38
+ document.getElementById("products").innerHTML = "<br>No products to display.";
39
+ } else {
40
+ var list = "";
41
+ products.forEach((product) => {
42
+ var link = `<a target="_top" href="https://<%%= @shop_origin %>/admin/products/${product.id}">`
43
+ list += "<li>" + link + product.title + "</a></li>";
44
+ });
45
+ document.getElementById("products").innerHTML = "<ul>" + list + "</ul>";
46
+ }
47
+ });
48
+ }();
49
+ });
50
+ </script>
51
+ <% end %> </head>
52
+ <body>
53
+ <h2>Products</h2>
54
+ <% unless with_cookie_authentication? %> <div id="products"><br>Loading...</div><% else %>
55
+ <ul>
56
+ <%% @products.each do |product| %>
57
+ <li><%%= link_to product.title, "https://#{@current_shopify_session.domain}/admin/products/#{product.id}", target: "_top" %></li>
58
+ <%% end %>
59
+ </ul>
60
+
61
+ <hr>
62
+ <% end %>
63
+ <h2>Webhooks</h2>
64
+
65
+ <%% if @webhooks.present? %>
66
+ <ul>
67
+ <%% @webhooks.each do |webhook| %>
68
+ <li><%%= webhook.topic %> : <%%= webhook.address %></li>
69
+ <%% end %>
70
+ </ul>
71
+ <%% else %>
72
+ <p>This app has not created any webhooks for this Shop. Add webhooks to your ShopifyApp initializer if you need webhooks</p>
73
+ <%% end %>
74
+ </body>
75
+ </html>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HomeController < ApplicationController
4
+ include ShopifyApp::EmbeddedApp
5
+ include ShopifyApp::RequireKnownShop
6
+ include ShopifyApp::ShopAccessScopesVerification
7
+
8
+ def index
9
+ @shop_origin = current_shopify_domain
10
+ @host = params[:host]
11
+ end
12
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+ require 'rails/generators/base'
3
+
4
+ module ShopifyApp
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ class_option :application_name, type: :array, default: ['My', 'Shopify', 'App']
11
+ class_option :scope, type: :array, default: ['read_products']
12
+ class_option :embedded, type: :string, default: 'true'
13
+ class_option :api_version, type: :string, default: nil
14
+ class_option :with_cookie_authentication, type: :boolean, default: false
15
+
16
+ def create_shopify_app_initializer
17
+ @application_name = format_array_argument(options['application_name'])
18
+ @scope = format_array_argument(options['scope'])
19
+ @api_version = options['api_version'] || ShopifyAPI::Meta.admin_versions.find(&:latest_supported).handle
20
+
21
+ template('shopify_app.rb', 'config/initializers/shopify_app.rb')
22
+ end
23
+
24
+ def create_session_store_initializer
25
+ copy_file('session_store.rb', 'config/initializers/session_store.rb')
26
+ end
27
+
28
+ def create_and_inject_into_omniauth_initializer
29
+ unless File.exist?("config/initializers/omniauth.rb")
30
+ copy_file('omniauth.rb', 'config/initializers/omniauth.rb')
31
+ end
32
+
33
+ return if !Rails.env.test? && shopify_provider_exists?
34
+
35
+ inject_into_file(
36
+ 'config/initializers/omniauth.rb',
37
+ shopify_provider_template,
38
+ after: "Rails.application.config.middleware.use(OmniAuth::Builder) do\n"
39
+ )
40
+ end
41
+
42
+ def create_embedded_app_layout
43
+ return unless embedded_app?
44
+
45
+ copy_file('embedded_app.html.erb', 'app/views/layouts/embedded_app.html.erb')
46
+ copy_file('_flash_messages.html.erb', 'app/views/layouts/_flash_messages.html.erb')
47
+
48
+ if ShopifyApp.use_webpacker?
49
+ copy_file('shopify_app.js', 'app/javascript/shopify_app/shopify_app.js')
50
+ copy_file('flash_messages.js', 'app/javascript/shopify_app/flash_messages.js')
51
+ copy_file('shopify_app_index.js', 'app/javascript/shopify_app/index.js')
52
+ append_to_file('app/javascript/packs/application.js', "require(\"shopify_app\")\n")
53
+ elsif ShopifyApp.use_importmap?
54
+ copy_file('shopify_app_importmap.js', 'app/javascript/lib/shopify_app.js')
55
+ copy_file('flash_messages.js', 'app/javascript/lib/flash_messages.js')
56
+ append_to_file('config/importmap.rb', "pin_all_from \"app/javascript/lib\", under: \"lib\"\n")
57
+ else
58
+ copy_file('shopify_app.js', 'app/assets/javascripts/shopify_app.js')
59
+ copy_file('flash_messages.js', 'app/assets/javascripts/flash_messages.js')
60
+ end
61
+ end
62
+
63
+ def create_user_agent_initializer
64
+ template('user_agent.rb', 'config/initializers/user_agent.rb')
65
+ end
66
+
67
+ def mount_engine
68
+ route("mount ShopifyApp::Engine, at: '/'")
69
+ end
70
+
71
+ def insert_hosts_into_development_config
72
+ inject_into_file(
73
+ 'config/environments/development.rb',
74
+ " config.hosts = (config.hosts rescue []) << /\[-\\w]+\\.ngrok\\.io/\n",
75
+ after: "Rails.application.configure do\n"
76
+ )
77
+ end
78
+
79
+ private
80
+
81
+ def shopify_provider_exists?
82
+ File.open("config/initializers/omniauth.rb") do |file|
83
+ file.each_line do |line|
84
+ if line =~ /provider :shopify/
85
+ puts "\e[33m#{omniauth_warning}\e[0m"
86
+ return true
87
+ end
88
+ end
89
+ end
90
+ false
91
+ end
92
+
93
+ def omniauth_warning
94
+ <<~OMNIAUTH
95
+ \n[WARNING] The Shopify App generator attempted to add the following Shopify Omniauth \
96
+ provider 'config/initializers/omniauth.rb':
97
+
98
+ \e[0m#{shopify_provider_template}\e[33m
99
+
100
+ Consider updating 'config/initializers/omniauth.rb' to match the configuration above.
101
+ OMNIAUTH
102
+ end
103
+
104
+ def shopify_provider_template
105
+ File.read(File.expand_path(find_in_source_paths('shopify_provider.rb.tt')))
106
+ end
107
+
108
+ def embedded_app?
109
+ options['embedded'] == 'true'
110
+ end
111
+
112
+ def format_array_argument(array)
113
+ array.join(' ').tr('"', '')
114
+ end
115
+
116
+ def with_cookie_authentication?
117
+ options['with_cookie_authentication'] || !embedded_app?
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,3 @@
1
+ <% content_for :javascript do %>
2
+ <%= content_tag(:div, nil, id: 'shopify-app-flash', data: { flash: { notice: flash[:notice], error: flash[:error] } }) %>
3
+ <% end %>
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <% application_name = ShopifyApp.configuration.application_name %>
6
+ <title><%= application_name %></title>
7
+ <%= stylesheet_link_tag 'application' %>
8
+ <% if ShopifyApp.use_webpacker? %>
9
+ <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
10
+ <% elsif ShopifyApp.use_importmap? %>
11
+ <%= javascript_importmap_tags %>
12
+ <% else %>
13
+ <%= javascript_include_tag 'application', "data-turbolinks-track" => true %>
14
+ <% end %>
15
+ <%= csrf_meta_tags %>
16
+ </head>
17
+
18
+ <body>
19
+ <div class="app-wrapper">
20
+ <div class="app-content">
21
+ <main role="main">
22
+ <%= yield %>
23
+ </main>
24
+ </div>
25
+ </div>
26
+
27
+ <%= render 'layouts/flash_messages' %>
28
+
29
+ <script src="https://unpkg.com/@shopify/app-bridge@2"></script>
30
+
31
+ <%= content_tag(:div, nil, id: 'shopify-app-init', data: {
32
+ api_key: ShopifyApp.configuration.api_key,
33
+ shop_origin: @shop_origin || (@current_shopify_session.domain if @current_shopify_session),
34
+ host: @host,
35
+ debug: Rails.env.development?
36
+ } ) %>
37
+
38
+ <% if content_for?(:javascript) %>
39
+ <div id="ContentForJavascript" data-turbolinks-temporary>
40
+ <%= yield :javascript %>
41
+ </div>
42
+ <% end %>
43
+ </body>
44
+ </html>
@@ -0,0 +1,24 @@
1
+ var eventName = typeof(Turbolinks) !== 'undefined' ? 'turbolinks:load' : 'DOMContentLoaded';
2
+
3
+ if (!document.documentElement.hasAttribute("data-turbolinks-preview")) {
4
+ document.addEventListener(eventName, function flash() {
5
+ var flashData = JSON.parse(document.getElementById('shopify-app-flash').dataset.flash);
6
+
7
+ var Toast = window['app-bridge'].actions.Toast;
8
+
9
+ if (flashData.notice) {
10
+ Toast.create(app, {
11
+ message: flashData.notice,
12
+ duration: 5000,
13
+ }).dispatch(Toast.Action.SHOW);
14
+ }
15
+
16
+ if (flashData.error) {
17
+ Toast.create(app, {
18
+ message: flashData.error,
19
+ duration: 5000,
20
+ isError: true,
21
+ }).dispatch(Toast.Action.SHOW);
22
+ }
23
+ });
24
+ }
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.config.middleware.use(OmniAuth::Builder) do
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ # Be sure to restart your server when you modify this file.
3
+
4
+ Rails.application.config.session_store(:cookie_store, key: '_example_session', expire_after: 14.days)
@@ -0,0 +1,15 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ var data = document.getElementById('shopify-app-init').dataset;
3
+ var AppBridge = window['app-bridge'];
4
+ var createApp = AppBridge.default;
5
+ window.app = createApp({
6
+ apiKey: data.apiKey,
7
+ host: data.host,
8
+ });
9
+
10
+ var actions = AppBridge.actions;
11
+ var TitleBar = actions.TitleBar;
12
+ TitleBar.create(app, {
13
+ title: data.page,
14
+ });
15
+ });
@@ -0,0 +1,25 @@
1
+ ShopifyApp.configure do |config|
2
+ config.application_name = "<%= @application_name %>"
3
+ config.old_secret = "<%= @old_secret %>"
4
+ config.scope = "<%= @scope %>" # Consult this page for more scope options:
5
+ # https://help.shopify.com/en/api/getting-started/authentication/oauth/scopes
6
+ config.embedded_app = <%= embedded_app? %>
7
+ config.after_authenticate_job = false
8
+ config.api_version = "<%= @api_version %>"
9
+ config.shop_session_repository = 'Shop'
10
+
11
+ config.reauth_on_access_scope_changes = true
12
+
13
+ config.allow_jwt_authentication = <%= !with_cookie_authentication? %>
14
+ config.allow_cookie_authentication = <%= with_cookie_authentication? %>
15
+
16
+ config.api_key = ENV.fetch('SHOPIFY_API_KEY', '').presence
17
+ config.secret = ENV.fetch('SHOPIFY_API_SECRET', '').presence
18
+ if defined? Rails::Server
19
+ raise('Missing SHOPIFY_API_KEY. See https://github.com/Shopify/shopify_app#requirements') unless config.api_key
20
+ raise('Missing SHOPIFY_API_SECRET. See https://github.com/Shopify/shopify_app#requirements') unless config.secret
21
+ end
22
+ end
23
+
24
+ # ShopifyApp::Utils.fetch_known_api_versions # Uncomment to fetch known api versions from shopify servers on boot
25
+ # ShopifyAPI::ApiVersion.version_lookup_mode = :raise_on_unknown # Uncomment to raise an error if attempting to use an api version that was not previously known
@@ -0,0 +1,13 @@
1
+ var data = document.getElementById('shopify-app-init').dataset;
2
+ var AppBridge = window['app-bridge'];
3
+ var createApp = AppBridge.default;
4
+ window.app = createApp({
5
+ apiKey: data.apiKey,
6
+ host: data.host,
7
+ });
8
+
9
+ var actions = AppBridge.actions;
10
+ var TitleBar = actions.TitleBar;
11
+ TitleBar.create(app, {
12
+ title: data.page,
13
+ });
@@ -0,0 +1,2 @@
1
+ require('./shopify_app')
2
+ require('./flash_messages')
@@ -0,0 +1,8 @@
1
+ provider :shopify,
2
+ ShopifyApp.configuration.api_key,
3
+ ShopifyApp.configuration.secret,
4
+ scope: ShopifyApp.configuration.scope,
5
+ setup: lambda { |env|
6
+ configuration = ShopifyApp::OmniAuthConfiguration.new(env['omniauth.strategy'], Rack::Request.new(env))
7
+ configuration.build_options
8
+ }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyAPI
3
+ class Base < ActiveResource::Base
4
+ headers['User-Agent'] << " | ShopifyApp/#{ShopifyApp::VERSION}"
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module ShopifyApp
6
+ module Generators
7
+ class ProductsControllerGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ def create_products_controller
11
+ template('products_controller.rb', 'app/controllers/products_controller.rb')
12
+ end
13
+
14
+ def add_products_route
15
+ route("get '/products', :to => 'products#index'")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ProductsController < AuthenticatedController
4
+ def index
5
+ @products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
6
+ render(json: { products: @products })
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module ShopifyApp
6
+ module Generators
7
+ class RotateShopifyTokenJobGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ def add_rotate_shopify_token_job
11
+ copy_file('rotate_shopify_token_job.rb', "app/jobs/shopify/rotate_shopify_token_job.rb")
12
+ copy_file('rotate_shopify_token.rake', "lib/tasks/shopify/rotate_shopify_token.rake")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ namespace :shopify do
3
+ desc "Rotate shopify tokens for all active shops"
4
+ task :rotate_shopify_tokens, [:refresh_token] => :environment do |_t, args|
5
+ all_active_shops.find_each do |shop|
6
+ Shopify::RotateShopifyTokenJob.perform_later(
7
+ shop_domain: shop.shopify_domain,
8
+ refresh_token: args[:refresh_token]
9
+ )
10
+ end
11
+ end
12
+
13
+ # Its implementation will depend on the app configuration. Change accordingly.
14
+ def all_active_shops
15
+ Shop.all
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shopify
4
+ class RotateShopifyTokenJob < ActiveJob::Base
5
+ def perform(params)
6
+ @shop = Shop.find_by(shopify_domain: params[:shop_domain])
7
+ return unless @shop
8
+
9
+ config = ShopifyApp.configuration
10
+ uri = URI("https://#{@shop.shopify_domain}/admin/oauth/access_token")
11
+ post_data = {
12
+ client_id: config.api_key,
13
+ client_secret: config.secret,
14
+ refresh_token: params[:refresh_token],
15
+ access_token: @shop.shopify_token,
16
+ }
17
+
18
+ @response = Net::HTTP.post_form(uri, post_data)
19
+ return log_error(response_exception_error_message) unless @response.is_a?(Net::HTTPSuccess)
20
+
21
+ access_token = JSON.parse(@response.body)['access_token']
22
+ return log_error(no_access_token_error_message) unless access_token
23
+
24
+ @shop.update(shopify_token: access_token)
25
+ end
26
+
27
+ private
28
+
29
+ def log_error(message)
30
+ Rails.logger.error(message)
31
+ end
32
+
33
+ def no_access_token_error_message
34
+ "RotateShopifyTokenJob response returned no access token for shop: #{@shop.shopify_domain}"
35
+ end
36
+
37
+ def response_exception_error_message
38
+ "RotateShopifyTokenJob failed for shop: #{@shop.shopify_domain}." \
39
+ "Response returned status: #{@response.code}. Error message: #{@response.message}. "
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ require 'rails/generators/base'
3
+
4
+ module ShopifyApp
5
+ module Generators
6
+ class RoutesGenerator < Rails::Generators::Base
7
+ source_root File.expand_path('../templates', __FILE__)
8
+
9
+ def inject_shopify_app_routes_into_application_routes
10
+ route(session_routes)
11
+ end
12
+
13
+ def disable_engine_routes
14
+ gsub_file(
15
+ 'config/routes.rb',
16
+ "mount ShopifyApp::Engine, at: '/'",
17
+ ''
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def session_routes
24
+ File.read(routes_file_path)
25
+ end
26
+
27
+ def routes_file_path
28
+ File.expand_path(find_in_source_paths('routes.rb'))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ controller :sessions do
4
+ get 'login' => :new, :as => :login
5
+ post 'login' => :create, :as => :authenticate
6
+ get 'auth/shopify/callback' => :callback
7
+ get 'logout' => :destroy, :as => :logout
8
+ end
9
+
10
+ namespace :webhooks do
11
+ post ':type' => :receive
12
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ require 'rails/generators/base'
3
+ require 'rails/generators/active_record'
4
+
5
+ module ShopifyApp
6
+ module Generators
7
+ class ShopModelGenerator < Rails::Generators::Base
8
+ include Rails::Generators::Migration
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ class_option :new_shopify_cli_app, type: :boolean, default: false
12
+
13
+ def create_shop_model
14
+ copy_file('shop.rb', 'app/models/shop.rb')
15
+ end
16
+
17
+ def create_shop_migration
18
+ migration_template('db/migrate/create_shops.erb', 'db/migrate/create_shops.rb')
19
+ end
20
+
21
+ def create_shop_with_access_scopes_migration
22
+ scopes_column_prompt = <<~PROMPT
23
+ It is highly recommended that apps record the access scopes granted by \
24
+ merchants during app installation. See app/models/shop.rb to modify how \
25
+ access scopes are stored and retrieved.
26
+
27
+ [WARNING] You will need to update the access_scopes accessors in the Shop model \
28
+ to allow shopify_app to store and retrieve scopes when going through OAuth.
29
+
30
+ The following migration will add an `access_scopes` column to the Shop model. \
31
+ Do you want to include this migration? [y/n]
32
+ PROMPT
33
+
34
+ if new_shopify_cli_app? || Rails.env.test? || yes?(scopes_column_prompt)
35
+ migration_template(
36
+ 'db/migrate/add_shop_access_scopes_column.erb',
37
+ 'db/migrate/add_shop_access_scopes_column.rb'
38
+ )
39
+ end
40
+ end
41
+
42
+ def update_shopify_app_initializer
43
+ gsub_file('config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryShopSessionStore', 'Shop')
44
+ end
45
+
46
+ def create_shop_fixtures
47
+ copy_file('shops.yml', 'test/fixtures/shops.yml')
48
+ end
49
+
50
+ private
51
+
52
+ def new_shopify_cli_app?
53
+ options['new_shopify_cli_app']
54
+ end
55
+
56
+ def rails_migration_version
57
+ Rails.version.match(/\d\.\d/)[0]
58
+ end
59
+
60
+ class << self
61
+ private :next_migration_number
62
+
63
+ # for generating a timestamp when using `create_migration`
64
+ def next_migration_number(dir)
65
+ ActiveRecord::Generators::Base.next_migration_number(dir)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,5 @@
1
+ class AddShopAccessScopesColumn < ActiveRecord::Migration[<%= rails_migration_version %>]
2
+ def change
3
+ add_column :shops, :access_scopes, :string
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ class CreateShops < ActiveRecord::Migration[<%= rails_migration_version %>]
2
+ def self.up
3
+ create_table :shops do |t|
4
+ t.string :shopify_domain, null: false
5
+ t.string :shopify_token, null: false
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :shops, :shopify_domain, unique: true
10
+ end
11
+
12
+ def self.down
13
+ drop_table :shops
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ class Shop < ActiveRecord::Base
3
+ include ShopifyApp::ShopSessionStorageWithScopes
4
+
5
+ def api_version
6
+ ShopifyApp.configuration.api_version
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ regular_shop:
2
+ shopify_domain: 'regular-shop.myshopify.com'
3
+ shopify_token: 'token'