shopify_app 21.5.0 → 21.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +4 -4
- data/README.md +1 -1
- data/app/assets/javascripts/shopify_app/app_bridge_3.7.8.js +10 -0
- data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +2 -2
- data/app/assets/javascripts/shopify_app/redirect.js +1 -2
- data/docs/shopify_app/authentication.md +62 -57
- data/docs/shopify_app/controller-concerns.md +35 -15
- data/docs/shopify_app/sessions.md +250 -0
- data/docs/shopify_app/webhooks.md +35 -1
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +3 -1
- data/lib/shopify_app/configuration.rb +5 -1
- data/lib/shopify_app/controller_concerns/ensure_billing.rb +5 -2
- data/lib/shopify_app/controller_concerns/localization.rb +11 -8
- data/lib/shopify_app/managers/webhooks_manager.rb +3 -2
- data/lib/shopify_app/session/in_memory_user_session_store.rb +1 -1
- data/lib/shopify_app/session/session_repository.rb +37 -3
- data/lib/shopify_app/version.rb +1 -1
- data/package.json +1 -1
- data/shopify_app.gemspec +1 -1
- data/yarn.lock +3 -3
- metadata +7 -9
- data/.github/workflows/stale.yml +0 -43
- data/app/assets/javascripts/shopify_app/app_bridge_3.1.1.js +0 -10
- data/app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js +0 -1
- data/docs/shopify_app/session-repository.md +0 -79
@@ -1,37 +1,75 @@
|
|
1
1
|
# Authentication
|
2
2
|
|
3
|
-
The Shopify App gem implements [OAuth 2.0](https://shopify.dev/tutorials/authenticate-with-oauth) to get [
|
3
|
+
The Shopify App gem implements [OAuth 2.0](https://shopify.dev/tutorials/authenticate-with-oauth) to get [session tokens](https://shopify.dev/concepts/about-apis/authentication#api-access-modes). These are used to authenticate requests made by the app to the Shopify API.
|
4
4
|
|
5
5
|
By default, the gem generates an embedded app frontend that uses [Shopify App Bridge](https://shopify.dev/tools/app-bridge) to fetch [session tokens](https://shopify.dev/concepts/apps/building-embedded-apps-using-session-tokens). Session tokens are used by the embedded app to make authenticated requests to the app backend.
|
6
6
|
|
7
|
-
See [*
|
7
|
+
See [*Getting started with session token authentication*](https://shopify.dev/docs/apps/auth/oauth/session-tokens/getting-started) to learn more.
|
8
8
|
|
9
9
|
> ⚠️ Be sure you understand the differences between the types of authentication schemes before reading this guide.
|
10
10
|
|
11
11
|
#### Table of contents
|
12
12
|
|
13
|
-
[OAuth callback](#oauth-callback)
|
13
|
+
* [OAuth callback](#oauth-callback)
|
14
|
+
* [Customizing callback controller](#customizing-callback-controller)
|
15
|
+
* [Run jobs after the OAuth flow](#run-jobs-after-the-oauth-flow)
|
16
|
+
* [Rotate API credentials](#rotate-api-credentials)
|
17
|
+
* [Making authenticated API requests after authorization](#making-authenticated-api-requests-after-authorization)
|
14
18
|
|
15
|
-
|
19
|
+
## OAuth callback
|
16
20
|
|
17
|
-
|
21
|
+
>️ **Note:** In Shopify App version 8.4.0, we have extracted the callback logic in its own controller. If you are upgrading from a version older than 8.4.0 the callback action and related helper methods were defined in `ShopifyApp::SessionsController` ==> you will have to extend `ShopifyApp::CallbackController` instead and port your logic to the new controller.
|
18
22
|
|
19
|
-
|
20
|
-
* [`ShopifyApp::Authenticated`](#shopifyappauthenticated)
|
21
|
-
* [`ShopifyApp::EnsureAuthenticatedLinks`](#shopifyappensureauthenticatedlinks)
|
23
|
+
Upon completing the OAuth flow, Shopify calls the app at `ShopifyApp.configuration.login_callback_url`.
|
22
24
|
|
23
|
-
|
25
|
+
The default callback controller [`ShopifyApp::CallbackController`](../../app/controllers/shopify_app/callback_controller.rb) provides the following behaviour:
|
24
26
|
|
25
|
-
|
27
|
+
1. Logging into the shop and resetting the session
|
28
|
+
2. Storing the session to the `SessionRepository`
|
29
|
+
3. [Installing Webhooks](/docs/shopify_app/webhooks.md)
|
30
|
+
4. [Setting Scripttags](/docs/shopify_app/script-tags.md)
|
31
|
+
5. [Run jobs after the OAuth flow](#run-jobs-after-the-oauth-flow)
|
32
|
+
6. Redirecting to the return address
|
26
33
|
|
27
|
-
Upon completing the OAuth flow, Shopify calls the app at the `callback_path`. If the app needs to do some extra work, it can define and configure the route to a custom callback controller, inheriting from `ShopifyApp::CallbackController` and hook into or override any of the defined helper methods. The default callback controller already provides the following behaviour:
|
28
|
-
* Logging into the shop and resetting the session
|
29
|
-
* [Installing Webhooks](/docs/shopify_app/webhooks.md)
|
30
|
-
* [Setting Scripttags](/docs/shopify_app/script-tags.md)
|
31
|
-
* [Run jobs after the OAuth flow](#run-jobs-after-the-oauth-flow)
|
32
|
-
* Redirecting to the return address
|
33
34
|
|
34
|
-
|
35
|
+
#### Customizing callback controller
|
36
|
+
If the app needs to do some extra work, it can define and configure the route to a custom callback controller.
|
37
|
+
Inheriting from `ShopifyApp::CallbackController` and hook into or override any of the defined helper methods.
|
38
|
+
|
39
|
+
Example:
|
40
|
+
|
41
|
+
1. Create the new custom callback controller
|
42
|
+
```ruby
|
43
|
+
# web/app/controllers/my_custom_callback_controller.rb
|
44
|
+
|
45
|
+
class MyCustomCallbackController < ShopifyApp::CallbackController
|
46
|
+
private
|
47
|
+
|
48
|
+
def install_webhooks(session)
|
49
|
+
# My custom override/definition to install webhooks
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
2. Override callback routing to this controller
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
# web/config/routes.rb
|
58
|
+
|
59
|
+
Rails.application.routes.draw do
|
60
|
+
root to: "home#index"
|
61
|
+
|
62
|
+
# Overriding the callback controller to the new custom one.
|
63
|
+
# This must be added before mounting the ShopifyApp::Engine
|
64
|
+
get ShopifyApp.configuration.login_callback_url, to: 'my_custom_callback#callback'
|
65
|
+
|
66
|
+
mount ShopifyApp::Engine, at: "/api"
|
67
|
+
|
68
|
+
# other routes
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
### Run jobs after the OAuth flow
|
35
73
|
|
36
74
|
See [`ShopifyApp::AfterAuthenticateJob`](/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb).
|
37
75
|
|
@@ -49,7 +87,7 @@ If you need the job to run synchronously add the `inline` flag:
|
|
49
87
|
|
50
88
|
```ruby
|
51
89
|
ShopifyApp.configure do |config|
|
52
|
-
config.after_authenticate_job = { job: Shopify::AfterAuthenticateJob, inline: true }
|
90
|
+
config.after_authenticate_job = { job: "Shopify::AfterAuthenticateJob", inline: true }
|
53
91
|
end
|
54
92
|
```
|
55
93
|
|
@@ -63,7 +101,7 @@ If you want to perform that action only once, e.g. send a welcome email to the u
|
|
63
101
|
|
64
102
|
## Rotate API credentials
|
65
103
|
|
66
|
-
If your Shopify secret key is leaked, you can use the RotateShopifyTokenJob to perform [API Credential Rotation](https://help.shopify.com/en/api/getting-started/authentication/oauth/api-credential-rotation).
|
104
|
+
If your Shopify secret key is leaked, you can use the `RotateShopifyTokenJob` to perform [API Credential Rotation](https://help.shopify.com/en/api/getting-started/authentication/oauth/api-credential-rotation).
|
67
105
|
|
68
106
|
Before running the job, you'll need to generate a new secret key from your Shopify Partner dashboard, and update the `/config/initializers/shopify_app.rb` to hold your new and old secret keys:
|
69
107
|
|
@@ -86,43 +124,10 @@ strategy.options[:old_client_secret] = ShopifyApp.configuration.old_secret
|
|
86
124
|
|
87
125
|
> **Note:** If you are updating `shopify_app` from a version prior to 8.4.2 (and do not wish to run the default/install generator again), you will need to add [the following line](https://github.com/Shopify/shopify_app/blob/4f7e6cca2a472d8f7af44b938bd0fcafe4d8e88a/lib/generators/shopify_app/install/templates/shopify_provider.rb#L18) to `config/initializers/omniauth.rb`:
|
88
126
|
|
89
|
-
##
|
90
|
-
|
91
|
-
### `ShopifyApp::Authenticated`
|
92
|
-
|
93
|
-
The engine provides a [`ShopifyApp::Authenticated`](/app/controllers/concerns/shopify_app/authenticated.rb) concern which should be included in any controller that is intended to be behind Shopify OAuth. It adds `before_action`s to ensure that the user is authenticated and will redirect to the Shopify login page if not. It is best practice to include this concern in a base controller inheriting from your `ApplicationController`, from which all controllers that require Shopify authentication inherit.
|
94
|
-
|
95
|
-
*Example:*
|
96
|
-
|
97
|
-
```rb
|
98
|
-
class AuthenticatedController < ApplicationController
|
99
|
-
include ShopifyApp::Authenticated
|
100
|
-
end
|
101
|
-
|
102
|
-
class ApiController < AuthenticatedController
|
103
|
-
# Actions in this controller are protected
|
104
|
-
end
|
105
|
-
```
|
106
|
-
|
107
|
-
For backwards compatibility, the engine still provides a controller called `ShopifyApp::AuthenticatedController` which includes the `ShopifyApp::Authenticated` concern. Note that it inherits directly from `ActionController::Base`, so you will not be able to share functionality between it and your application's `ApplicationController`.
|
108
|
-
|
109
|
-
#### Embedded apps and `ShopifyApp::Authenticated`
|
110
|
-
|
111
|
-
Embedded Shopify Admin apps can only use the `ShopifyApp::Authenticated` controller concern *if* the requests originate from App Bridge's `authenticatedFetch` method. Those who include this concern in the `HomeController` or some other embedded controller will see what looks like an OAuth redirect loop as the `ShopifyApp::Authenticated` concern will be fighting with the App Bridge. For more details on how to handle embedded sessions, refer to [the session token documentation](https://shopify.dev/apps/auth/oauth/session-tokens).
|
112
|
-
|
113
|
-
### `ShopifyApp::EnsureAuthenticatedLinks`
|
127
|
+
## Making authenticated API requests after authorization
|
128
|
+
After the app is installed onto a shop and has been granted all necessary permission, a new session record will be added to `SessionRepository#shop_storage`, or `SessionRepository#user_storage` if online sessions are enabled.
|
114
129
|
|
115
|
-
|
116
|
-
|
117
|
-
Include this concern in your app's `AuthenticatedController` if your app uses session tokens with [Turbolinks](https://github.com/turbolinks/turbolinks). It adds a `before_action` filter that detects whether a session token is present or not. If a session token is not found, the user is redirected to your app's splash page path (`root_path`) along with `return_to` and `shop` parameters.
|
118
|
-
|
119
|
-
*Example:*
|
120
|
-
|
121
|
-
```rb
|
122
|
-
class AuthenticatedController < ApplicationController
|
123
|
-
include ShopifyApp::EnsureAuthenticatedLinks
|
124
|
-
include ShopifyApp::Authenticated
|
125
|
-
end
|
126
|
-
```
|
130
|
+
When your app needs to make API requests to Shopify, `ShopifyApp`'s `ActiveSupport` controller concerns can help you retrieve the active session token from the repository to make the authenticate API call.
|
127
131
|
|
128
|
-
|
132
|
+
- ⚠️ See [Sessions](./sessions.md) page to understand how sessions work.
|
133
|
+
- ⚠️ See [Controller Concerns](./controller-concerns.md) page to understand when to use which concern.
|
@@ -1,12 +1,45 @@
|
|
1
1
|
# Controller Concerns
|
2
2
|
|
3
|
-
The following controller concerns are designed to be public and can be included in your controllers.
|
3
|
+
The following controller concerns are designed to be public and can be included in your controllers.
|
4
4
|
|
5
|
-
|
5
|
+
If you are looking to make requests within the context of a logged in **User**, you will include `EnsureHasSession` into your controller to ensure users have a valid session to make requests.
|
6
|
+
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
class YourController < ApplicationController
|
10
|
+
include ShopifyApp::EnsureHasSession
|
11
|
+
end
|
12
|
+
```
|
13
|
+
|
14
|
+
If you don't require a user to be present but make requests scoped by a **Shop**, you can include `EnsureInstalled` in your controller to ensure your app has been installed by the Shop and they have a shop session.
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
class YourController < ApplicationController
|
18
|
+
include ShopifyApp::EnsureInstalled
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
## EnsureHasSession - Online User Sessions
|
6
23
|
Designed for controllers that are designed to handle authenticated actions by ensuring there is a valid session for the request.
|
7
24
|
|
8
25
|
In addition to session management, this concern will also handle localization, CSRF protection, embedded app settings, and billing enforcement.
|
9
26
|
|
27
|
+
## EnsureInstalled - Offline Shop Sessions
|
28
|
+
Designed to handle request scoped by a shop for *embedded apps*. If you are non-embedded app, we recommend using `EnsureHasSession` concern instead of this one.
|
29
|
+
|
30
|
+
Rather than using the JWT to determine the requested shop of the request, the `shop` name param is taken from the query string that Shopify Admin provides.
|
31
|
+
|
32
|
+
If the shop session cannot be found for the provided `shop` in the query string, the request will be redirected to login or the `embedded_redirect_url`.
|
33
|
+
|
34
|
+
## EnsureAuthenticatedLinks
|
35
|
+
Designed to be more of a lightweight session concern specifically for XHR requests. Where `EnsureHasSession` does far more than just session management, this concern will redirect to the splash page of the app if no active session was found.
|
36
|
+
|
37
|
+
## ShopAccessScopesVerification
|
38
|
+
If scopes for the session don't match configuration of scopes defined in `config/initializers/shopify_app.rb` the user will be redirected to login or the `embedded_redirect_url`.
|
39
|
+
|
40
|
+
# Private Concerns used with EnsureHasSession
|
41
|
+
These concerns shouldn't be included directly, but we provide some documentation as to their functionality that are included with `EnsureHasSession`. Concerns defined in `lib/shopify_app/controller_concerns` are designed to be private and are not meant to be included directly into your controllers.
|
42
|
+
|
10
43
|
#### LoginProtection - Session Management
|
11
44
|
This concern will setup and teardown the session around the action. If the session cannot be setup for the requested shop the request will be redirected to login.
|
12
45
|
|
@@ -33,16 +66,3 @@ If your ShopifyApp configuration has the `embedded_app` config set to true, [P3P
|
|
33
66
|
|
34
67
|
#### EnsureBilling
|
35
68
|
If billing is enabled for the app, the active payment for the session is queried and enforced if needed. If billing is required the user will be redirected to a page requesting payment.
|
36
|
-
|
37
|
-
## EnsureAuthenticatedLinks
|
38
|
-
Designed to be more of a lightweight session concern specifically for XHR requests. Where `Authenticated` does far more than just session management, this concern will redirect to the splash page of the app if no active session was found.
|
39
|
-
|
40
|
-
## RequireKnownShop
|
41
|
-
Designed to handle unauthenticated requests for *embedded apps*. If you are non-embedded app, we recommend using `Authenticated` concern instead of this one.
|
42
|
-
|
43
|
-
Rather than using the JWT to determine the requested shop of the request, the `shop` name param is taken from the query string that Shopify Admin provides.
|
44
|
-
|
45
|
-
If the shop session cannot be found for the provided `shop` in the query string, the request will be redirected to login or the `embedded_redirect_url`.
|
46
|
-
|
47
|
-
## ShopAccessScopesVerification
|
48
|
-
If scopes for the session don't match configuration of scopes defined in `config/initializers/shopify_app.rb` the user will be redirected to login or the `embedded_redirect_url`
|
@@ -0,0 +1,250 @@
|
|
1
|
+
# Sessions
|
2
|
+
|
3
|
+
Sessions are used to make contextual API calls for either a shop (offline session) or a user (online session). This gem has ownership of session persistence.
|
4
|
+
|
5
|
+
#### Table of contents
|
6
|
+
|
7
|
+
- [Sessions](#sessions-1)
|
8
|
+
- [Types of session tokens](#types-of-session-tokens) - Shop (offline) v.s. User (online)
|
9
|
+
- [Session token storage](#session-token-storage)
|
10
|
+
- [Shop (offline) token storage](#shop-offline-token-storage)
|
11
|
+
- [User (online) token storage](#user-online-token-storage)
|
12
|
+
- [In-Memory Session Storage for Testing](#in-memory-session-storage-for-testing)
|
13
|
+
- [Customizing Session Storage with `ShopifyApp::SessionRepository`](#customizing-session-storage-with-shopifyappsessionrepository)
|
14
|
+
- [Loading Sessions](#loading-sessions)
|
15
|
+
- [Getting Sessions with Controller Concerns](#getting-sessions-with-controller-concerns)
|
16
|
+
- [Shop session - "EnsureInstalled" ](#shop-sessions---ensureinstalled)
|
17
|
+
- [User session - "EnsureHasSession" ](#user-sessions---ensurehassession)
|
18
|
+
- [Getting Sessions from a Shop or User model record - "with_shopify_session"](#getting-sessions-from-a-shop-or-user-model-record---with_shopify_session)
|
19
|
+
- [Access scopes](#access-scopes)
|
20
|
+
- [`ShopifyApp::ShopSessionStorageWithScopes`](#shopifyappshopsessionstoragewithscopes)
|
21
|
+
- [``ShopifyApp::UserSessionStorageWithScopes``](#shopifyappusersessionstoragewithscopes)
|
22
|
+
- [Migrating from shop-based to user-based token strategy](#migrating-from-shop-based-to-user-based-token-strategy)
|
23
|
+
- [Migrating from ShopifyApi::Auth::SessionStorage to ShopifyApp::SessionStorage](#migrating-from-shopifyapiauthsessionstorage-to-shopifyappsessionstorage)
|
24
|
+
|
25
|
+
## Sessions
|
26
|
+
#### Types of session tokens
|
27
|
+
- **Shop** ([offline access](https://shopify.dev/docs/apps/auth/oauth/access-modes#offline-access))
|
28
|
+
- Access token is linked to the store
|
29
|
+
- Meant for long-term access to a store, where no user interaction is involved
|
30
|
+
- Ideal for background jobs or maintenance work
|
31
|
+
- **User** ([online access](https://shopify.dev/docs/apps/auth/oauth/access-modes#online-access))
|
32
|
+
- Access token is linked to an individual user on a store
|
33
|
+
- Meant to be used when a user is interacting with your app through the web
|
34
|
+
|
35
|
+
⚠️ [Read more about Online vs. Offline access here](https://shopify.dev/apps/auth/oauth/access-modes).
|
36
|
+
|
37
|
+
#### Session token storage
|
38
|
+
##### Shop (offline) token storage
|
39
|
+
⚠️ All apps must have a shop session storage, if you started from the [Ruby App Template](https://github.com/Shopify/shopify-app-template-ruby), it's already configured to have a Shop model by default.
|
40
|
+
|
41
|
+
If you don't already have a repository to store the access tokens:
|
42
|
+
|
43
|
+
1. Run the following generator to create a shop model to store the access tokens
|
44
|
+
|
45
|
+
```sh
|
46
|
+
rails generate shopify_app:shop_model
|
47
|
+
```
|
48
|
+
|
49
|
+
2. Configure `config/initializers/shopify_app.rb` to enable shop session token persistance:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
config.shop_session_repository = 'Shop'
|
53
|
+
```
|
54
|
+
|
55
|
+
##### User (online) token storage
|
56
|
+
If your app has user interactions and would like to control permission based on individual users, you need to configure a User token storage to persist unique tokens for each user.
|
57
|
+
|
58
|
+
[Shop (offline) tokens must still be maintained](#shop-offline-token-storage).
|
59
|
+
|
60
|
+
1. Run the following generator to create a user model to store the individual based access tokens
|
61
|
+
```sh
|
62
|
+
rails generate shopify_app:user_model
|
63
|
+
```
|
64
|
+
|
65
|
+
2. Configure `config/initializers/shopify_app.rb` to enable user session token persistance:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
config.user_session_repository = 'User'
|
69
|
+
```
|
70
|
+
|
71
|
+
The current Shopify user will be stored in the rails session at `session[:shopify_user]`
|
72
|
+
|
73
|
+
##### In-memory Session Storage for testing
|
74
|
+
The `ShopifyApp` gem includes methods for in-memory storage for both shop and user sessions. In-memory storage is intended to be used in a testing environment, please use a persistent storage for your application.
|
75
|
+
- [InMemoryShopSessionStore](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/in_memory_shop_session_store.rb)
|
76
|
+
- [InMemoryUserSessionStore](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/in_memory_user_session_store.rb)
|
77
|
+
|
78
|
+
You can configure the `ShopifyApp` configuration to use the in-memory storage method during manual testing:
|
79
|
+
```ruby
|
80
|
+
# config/initializers/shopify_app.rb
|
81
|
+
|
82
|
+
config.shop_session_repository = ShopifyApp::InMemoryShopSessionStore
|
83
|
+
config.user_session_repository = ShopifyApp::InMemoryUserSessionStore
|
84
|
+
```
|
85
|
+
|
86
|
+
##### Customizing Session Storage with `ShopifyApp::SessionRepository`
|
87
|
+
|
88
|
+
In the rare event that you would like to break Rails convention for storing/retrieving records, the `ShopifyApp::SessionRepository` allows you to define how your sessions are stored and retrieved for shops. The specific repository for `shop` & `user` is configured in the `config/initializers/shopify_app.rb` file and can be set to any object.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
# config/initializers/shopify_app.rb
|
92
|
+
|
93
|
+
config.shop_session_repository = MyCustomShopSessionRepository
|
94
|
+
config.user_session_repository = MyCustomUserSessionRepository
|
95
|
+
```
|
96
|
+
|
97
|
+
##### ⚠️ Custom Session Storage Requirements
|
98
|
+
|
99
|
+
The custom **Shop** repository must implement the following methods:
|
100
|
+
|
101
|
+
| Method | Parameters | Return Type |
|
102
|
+
|---------------------------------------------------|--------------------------------------------|---------------------------|
|
103
|
+
| `self.store(auth_session)` | `auth_session` (ShopifyAPI::Auth::Session) | - |
|
104
|
+
| `self.retrieve(id)` | `id` (String) | ShopifyAPI::Auth::Session |
|
105
|
+
| `self.retrieve_by_shopify_domain(shopify_domain)` | `shopify_domain` (String) | ShopifyAPI::Auth::Session |
|
106
|
+
|
107
|
+
The custom **User** repository must implement the following methods:
|
108
|
+
| Method | Parameters | Return Type |
|
109
|
+
|---------------------------------------------|-------------------------------------|------------------------------|
|
110
|
+
| `self.store(auth_session, user)` | <li>`auth_session` (ShopifyAPI::Auth::Session)<br><li>`user` (ShopifyAPI::Auth::AssociatedUser) | - |
|
111
|
+
| `self.retrieve(id)` | `id` (String) | `ShopifyAPI::Auth::Session` |
|
112
|
+
| `self.retrieve_by_shopify_user_id(user_id)` | `user_id` (String) | `ShopifyAPI::Auth::Session` |
|
113
|
+
|
114
|
+
|
115
|
+
These methods are already implemented as a part of the `User` and `Shop` models generated from this gem's generator.
|
116
|
+
- `Shop` model includes the [ShopSessionStorageWithScopes](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/shop_session_storage_with_scopes.rb) concern.
|
117
|
+
- `User` model includes the [UserSessionStorageWithScopes](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/user_session_storage_with_scopes.rb) concern.
|
118
|
+
|
119
|
+
##### Available `ActiveSupport::Concerns` that contains implementation of the above methods
|
120
|
+
Simply include these concerns if you want to use the implementation, and overwrite methods for custom implementation
|
121
|
+
|
122
|
+
- `Shop` storage
|
123
|
+
- [ShopSessionStorageWithScopes](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/shop_session_storage_with_scopes.rb)
|
124
|
+
- [ShopSessionStorage](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/shop_session_storage.rb)
|
125
|
+
|
126
|
+
- `User` storage
|
127
|
+
- [UserSessionStorageWithScopes](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/user_session_storage_with_scopes.rb)
|
128
|
+
- [UserSessionStorage](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/user_session_storage.rb)
|
129
|
+
|
130
|
+
### Loading Sessions
|
131
|
+
By using the appropriate controller concern, sessions are loaded for you.
|
132
|
+
|
133
|
+
#### Getting Sessions with Controller Concerns
|
134
|
+
|
135
|
+
⚠️ **Note: These controller concerns cannot both be included in the same controller.**
|
136
|
+
##### **Shop Sessions - `EnsureInstalled`**
|
137
|
+
- [EnsureInstalled](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/ensure_installed.rb) controller concern will load a shop session with the `installed_shop_session` helper. If a shop session is not found, meaning the app wasn't installed for this shop, the request will be redirected to be installed.
|
138
|
+
- This controller concern should NOT be used if you don't need your app to make calls on behalf of a user.
|
139
|
+
- Example
|
140
|
+
```ruby
|
141
|
+
class MyController < ApplicationController
|
142
|
+
include ShopifyApp::EnsureInstalled
|
143
|
+
|
144
|
+
def method
|
145
|
+
current_session = installed_shop_session # `installed_shop_session` is a helper from `EnsureInstalled`
|
146
|
+
|
147
|
+
client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_session)
|
148
|
+
client.query(
|
149
|
+
#...
|
150
|
+
)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
##### User Sessions - `EnsureHasSession`
|
156
|
+
- [EnsureHasSession](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/ensure_has_session.rb) controller concern will load a user session via `current_shopify_session`. As part of loading this session, this concern will also ensure that the user session has the appropriate scopes needed for the application. If the user isn't found or has fewer permitted scopes than are required, they will be prompted to authorize the application.
|
157
|
+
- This controller concern should be used if you don't need your app to make calls on behalf of a user. With that in mind, there are a few other embedded concerns that are mixed in to ensure that embedding, CSRF, localization, and billing allow the action for the user.
|
158
|
+
- Example
|
159
|
+
```ruby
|
160
|
+
class MyController < ApplicationController
|
161
|
+
include ShopifyApp::EnsureHasSession
|
162
|
+
|
163
|
+
def method
|
164
|
+
current_session = current_shopify_session # `current_shopify_session` is a helper from `EnsureHasSession`
|
165
|
+
|
166
|
+
client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_session)
|
167
|
+
client.query(
|
168
|
+
#...
|
169
|
+
)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
#### Getting sessions from a Shop or User model record - 'with_shopify_session'
|
175
|
+
The [ShopifyApp::SessionStorage#with_shopify_session](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/session_storage.rb#L12)
|
176
|
+
helper allows you to make API calls within the context of a user or shop, by using that record's access token.
|
177
|
+
|
178
|
+
This mixin is already included in ActiveSupport [concerns](#available-activesupportconcerns-that-contains-implementation-of-the-above-methods) from this gem.
|
179
|
+
If you're using a custom implementation of session storage, you can include the [ShopifyApp::SessionStorage](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/session_storage.rb) concern.
|
180
|
+
|
181
|
+
All calls made within the block passed into this helper will be made in that context:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
# To use shop context for "my_shopify_domain.myshopify.com"
|
185
|
+
shopify_domain = "my_shopify_domain.myshopify.com"
|
186
|
+
shop = Shop.find_by(shopify_domain: shopify_domain)
|
187
|
+
shop.with_shopify_session do
|
188
|
+
ShopifyAPI::Product.find(id: product_id)
|
189
|
+
# This will call the Shopify API with my_shopify_domain's access token
|
190
|
+
end
|
191
|
+
|
192
|
+
# To use user context for user ID "my_user_id"
|
193
|
+
user = User.find_by(shopify_user_id: "my_user_id")
|
194
|
+
user.with_shopify_session do
|
195
|
+
ShopifyAPI::Product.find(id: product_id)
|
196
|
+
# This will call the Shopify API with my_user_id's access token
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
## Access scopes
|
201
|
+
If you want to customize how access scopes are stored for shops and users, you can implement the `access_scopes` getters and setters in the models that include `ShopifyApp::ShopSessionStorageWithScopes` and `ShopifyApp::UserSessionStorageWithScopes` as shown:
|
202
|
+
|
203
|
+
### `ShopifyApp::ShopSessionStorageWithScopes`
|
204
|
+
```ruby
|
205
|
+
class Shop < ActiveRecord::Base
|
206
|
+
include ShopifyApp::ShopSessionStorageWithScopes
|
207
|
+
|
208
|
+
def access_scopes=(scopes)
|
209
|
+
# Store access scopes
|
210
|
+
end
|
211
|
+
def access_scopes
|
212
|
+
# Find access scopes
|
213
|
+
end
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
### `ShopifyApp::UserSessionStorageWithScopes`
|
218
|
+
```ruby
|
219
|
+
class User < ActiveRecord::Base
|
220
|
+
include ShopifyApp::UserSessionStorageWithScopes
|
221
|
+
|
222
|
+
def access_scopes=(scopes)
|
223
|
+
# Store access scopes
|
224
|
+
end
|
225
|
+
def access_scopes
|
226
|
+
# Find access scopes
|
227
|
+
end
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
## Migrating from shop-based to user-based token strategy
|
232
|
+
|
233
|
+
1. Run the `user_model` generator as [mentioned above](#user-online-token-storage).
|
234
|
+
2. Ensure that both your `Shop` model and `User` model includes the [necessary concerns](#available-activesupportconcerns-that-contains-implementation-of-the-above-methods)
|
235
|
+
3. Update the configuration file to use the new session storage.
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
# config/initializers/shopify_app.rb
|
239
|
+
|
240
|
+
config.shop_session_repository = {YOUR_SHOP_MODEL_CLASS}
|
241
|
+
config.user_session_repository = {YOUR_USER_MODEL_CLASS}
|
242
|
+
```
|
243
|
+
|
244
|
+
## Migrating from `ShopifyApi::Auth::SessionStorage` to `ShopifyApp::SessionStorage`
|
245
|
+
- Support for using `ShopifyApi::Auth::SessionStorage` was removed from ShopifyApi [version 13.0.0](https://github.com/Shopify/shopify-api-ruby/blob/main/CHANGELOG.md#1300)
|
246
|
+
- Sessions storage are now handled with [ShopifyApp::SessionRepository](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/session_repository.rb)
|
247
|
+
- To migrate and specify your shop or user session storage method:
|
248
|
+
1. Remove `session_storage` configuration from `config/initializers/shopify_app.rb`
|
249
|
+
2. Follow ["Session Token Storage" instructions](#session-token-storage) to specify the storage repository for shop and user sessions.
|
250
|
+
- [Customizing session storage](#customizing-session-storage-with-shopifyappsessionrepository)
|
@@ -82,4 +82,38 @@ We have three mandatory GDPR webhooks
|
|
82
82
|
|
83
83
|
The `generate shopify_app` command generated three job templates corresponding to all three of these webhooks.
|
84
84
|
To pass our approval process you will need to set these webhooks in your partner dashboard.
|
85
|
-
You can read more about that [here](https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks).
|
85
|
+
You can read more about that [here](https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks).
|
86
|
+
|
87
|
+
## EventBridge and PubSub Webhooks
|
88
|
+
|
89
|
+
You can also register webhooks for delivery to Amazon EventBridge or Google Cloud Pub/Sub. In this case the `path` argument to needs to be of a specific form.
|
90
|
+
|
91
|
+
For EventBridge, the `path` must be the ARN of the partner event source.
|
92
|
+
|
93
|
+
```rb
|
94
|
+
ShopifyApp.configure do |config|
|
95
|
+
config.webhooks = [
|
96
|
+
{
|
97
|
+
delivery_method: :event_bridge,
|
98
|
+
topic: 'carts/update',
|
99
|
+
path: 'arn:aws:events....'
|
100
|
+
}
|
101
|
+
]
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
For Pub/Sub, the `path` must be of the form `pubsub://[PROJECT-ID]:[PUB-SUB-TOPIC-ID]`. For example, if you created a topic with id `red` in the project `blue`, then the value of path would be `pubsub://blue:red`.
|
106
|
+
|
107
|
+
```rb
|
108
|
+
ShopifyApp.configure do |config|
|
109
|
+
config.webhooks = [
|
110
|
+
{
|
111
|
+
delivery_method: :pub_sub,
|
112
|
+
topic: 'carts/update',
|
113
|
+
path: 'pubsub://project-id:pub-sub-topic-id'
|
114
|
+
}
|
115
|
+
]
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
When registering for an EventBridge or PubSub Webhook you'll need to implement a handler that will fetch webhooks from the queue and process them yourself.
|
@@ -12,7 +12,7 @@ ShopifyApp.configure do |config|
|
|
12
12
|
config.webhooks = [
|
13
13
|
{ topic: "app/uninstalled", address: "webhooks/app_uninstalled"},
|
14
14
|
{ topic: "customers/data_request", address: "webhooks/customers_data_request" },
|
15
|
-
{ topic: "
|
15
|
+
{ topic: "customers/redact", address: "webhooks/customers_redact"},
|
16
16
|
{ topic: "shop/redact", address: "webhooks/shop_redact"}
|
17
17
|
]
|
18
18
|
|
@@ -30,6 +30,8 @@ ShopifyApp.configure do |config|
|
|
30
30
|
# amount: 5,
|
31
31
|
# interval: ShopifyApp::BillingConfiguration::INTERVAL_EVERY_30_DAYS,
|
32
32
|
# currency_code: "USD", # Only supports USD for now
|
33
|
+
# trial_days: 0
|
34
|
+
# test: ENV.fetch('SHOPIFY_TEST_CHARGES', !Rails.env.production?)
|
33
35
|
# )
|
34
36
|
|
35
37
|
if defined? Rails::Server
|
@@ -129,12 +129,16 @@ module ShopifyApp
|
|
129
129
|
attr_reader :amount
|
130
130
|
attr_reader :currency_code
|
131
131
|
attr_reader :interval
|
132
|
+
attr_reader :trial_days
|
133
|
+
attr_reader :test
|
132
134
|
|
133
|
-
def initialize(charge_name:, amount:, interval:, currency_code: "USD")
|
135
|
+
def initialize(charge_name:, amount:, interval:, currency_code: "USD", trial_days: 0, test: !Rails.env.production?)
|
134
136
|
@charge_name = charge_name
|
135
137
|
@amount = amount
|
136
138
|
@currency_code = currency_code
|
137
139
|
@interval = interval
|
140
|
+
@trial_days = trial_days
|
141
|
+
@test = test
|
138
142
|
end
|
139
143
|
end
|
140
144
|
|
@@ -136,7 +136,8 @@ module ShopifyApp
|
|
136
136
|
},
|
137
137
|
},
|
138
138
|
returnUrl: return_url,
|
139
|
-
|
139
|
+
trialDays: ShopifyApp.configuration.billing.trial_days,
|
140
|
+
test: ShopifyApp.configuration.billing.test,
|
140
141
|
},
|
141
142
|
)
|
142
143
|
|
@@ -154,7 +155,7 @@ module ShopifyApp
|
|
154
155
|
currencyCode: ShopifyApp.configuration.billing.currency_code,
|
155
156
|
},
|
156
157
|
returnUrl: return_url,
|
157
|
-
test:
|
158
|
+
test: ShopifyApp.configuration.billing.test,
|
158
159
|
},
|
159
160
|
)
|
160
161
|
|
@@ -208,12 +209,14 @@ module ShopifyApp
|
|
208
209
|
$name: String!
|
209
210
|
$lineItems: [AppSubscriptionLineItemInput!]!
|
210
211
|
$returnUrl: URL!
|
212
|
+
$trialDays: Int
|
211
213
|
$test: Boolean
|
212
214
|
) {
|
213
215
|
appSubscriptionCreate(
|
214
216
|
name: $name
|
215
217
|
lineItems: $lineItems
|
216
218
|
returnUrl: $returnUrl
|
219
|
+
trialDays: $trialDays
|
217
220
|
test: $test
|
218
221
|
) {
|
219
222
|
confirmationUrl
|
@@ -5,20 +5,23 @@ module ShopifyApp
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
|
8
|
+
around_action :set_locale
|
9
9
|
end
|
10
10
|
|
11
11
|
private
|
12
12
|
|
13
|
-
def set_locale
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
def set_locale(&action)
|
14
|
+
locale = params[:locale] || I18n.default_locale
|
15
|
+
|
16
|
+
# Fallback to the 2 letter language code if the requested locale unavailable
|
17
|
+
unless I18n.available_locales.include?(locale.to_sym)
|
18
|
+
locale = locale.split("-").first
|
18
19
|
end
|
19
|
-
|
20
|
+
|
21
|
+
session[:locale] = locale
|
22
|
+
I18n.with_locale(session[:locale], &action)
|
20
23
|
rescue I18n::InvalidLocale
|
21
|
-
I18n.
|
24
|
+
I18n.with_locale(I18n.default_locale, &action)
|
22
25
|
end
|
23
26
|
end
|
24
27
|
end
|
@@ -43,12 +43,13 @@ module ShopifyApp
|
|
43
43
|
ShopifyApp::Logger.debug("Adding registrations to webhooks")
|
44
44
|
ShopifyApp.configuration.webhooks.each do |attributes|
|
45
45
|
webhook_path = path(attributes)
|
46
|
+
delivery_method = attributes[:delivery_method] || :http
|
46
47
|
|
47
48
|
ShopifyAPI::Webhooks::Registry.add_registration(
|
48
49
|
topic: attributes[:topic],
|
49
|
-
delivery_method:
|
50
|
+
delivery_method: delivery_method,
|
50
51
|
path: webhook_path,
|
51
|
-
handler: webhook_job_klass(webhook_path),
|
52
|
+
handler: delivery_method == :http ? webhook_job_klass(webhook_path) : nil,
|
52
53
|
fields: attributes[:fields],
|
53
54
|
)
|
54
55
|
end
|