shopify_api 14.3.0 → 14.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/BREAKING_CHANGES_FOR_V15.md +42 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +2 -2
- data/README.md +3 -0
- data/docs/usage/custom_apps.md +32 -1
- data/docs/usage/oauth.md +88 -16
- data/docs/usage/webhooks.md +3 -2
- data/lib/shopify_api/admin_versions.rb +2 -1
- data/lib/shopify_api/clients/graphql/client.rb +14 -3
- data/lib/shopify_api/clients/graphql/storefront.rb +12 -2
- data/lib/shopify_api/rest/base.rb +7 -1
- data/lib/shopify_api/rest/resources/2022_04/customer.rb +1 -0
- data/lib/shopify_api/rest/resources/2022_04/recurring_application_charge.rb +9 -0
- data/lib/shopify_api/rest/resources/2022_04/shop.rb +12 -2
- data/lib/shopify_api/rest/resources/2022_07/customer.rb +1 -0
- data/lib/shopify_api/rest/resources/2022_07/recurring_application_charge.rb +9 -0
- data/lib/shopify_api/rest/resources/2022_07/shop.rb +12 -2
- data/lib/shopify_api/rest/resources/2022_10/customer.rb +1 -0
- data/lib/shopify_api/rest/resources/2022_10/recurring_application_charge.rb +9 -0
- data/lib/shopify_api/rest/resources/2022_10/shop.rb +12 -2
- data/lib/shopify_api/rest/resources/2023_01/customer.rb +1 -0
- data/lib/shopify_api/rest/resources/2023_01/recurring_application_charge.rb +9 -0
- data/lib/shopify_api/rest/resources/2023_01/shop.rb +12 -2
- data/lib/shopify_api/rest/resources/2023_04/customer.rb +1 -0
- data/lib/shopify_api/rest/resources/2023_04/recurring_application_charge.rb +9 -0
- data/lib/shopify_api/rest/resources/2023_04/shop.rb +12 -2
- data/lib/shopify_api/rest/resources/2023_07/customer.rb +1 -0
- data/lib/shopify_api/rest/resources/2023_07/recurring_application_charge.rb +9 -0
- data/lib/shopify_api/rest/resources/2023_07/shop.rb +12 -2
- data/lib/shopify_api/rest/resources/2023_10/customer.rb +1 -0
- data/lib/shopify_api/rest/resources/2023_10/recurring_application_charge.rb +9 -0
- data/lib/shopify_api/rest/resources/2023_10/shop.rb +12 -2
- data/lib/shopify_api/rest/resources/2024_01/customer.rb +1 -0
- data/lib/shopify_api/rest/resources/2024_01/recurring_application_charge.rb +9 -0
- data/lib/shopify_api/rest/resources/2024_01/shop.rb +12 -2
- data/lib/shopify_api/rest/resources/2024_04/customer.rb +1 -0
- data/lib/shopify_api/rest/resources/2024_04/recurring_application_charge.rb +9 -0
- data/lib/shopify_api/rest/resources/2024_04/shop.rb +12 -2
- data/lib/shopify_api/rest/resources/2024_07/abandoned_checkout.rb +194 -0
- data/lib/shopify_api/rest/resources/2024_07/access_scope.rb +62 -0
- data/lib/shopify_api/rest/resources/2024_07/apple_pay_certificate.rb +109 -0
- data/lib/shopify_api/rest/resources/2024_07/application_charge.rb +113 -0
- data/lib/shopify_api/rest/resources/2024_07/application_credit.rb +95 -0
- data/lib/shopify_api/rest/resources/2024_07/article.rb +269 -0
- data/lib/shopify_api/rest/resources/2024_07/asset.rb +122 -0
- data/lib/shopify_api/rest/resources/2024_07/assigned_fulfillment_order.rb +92 -0
- data/lib/shopify_api/rest/resources/2024_07/balance.rb +58 -0
- data/lib/shopify_api/rest/resources/2024_07/blog.rb +166 -0
- data/lib/shopify_api/rest/resources/2024_07/cancellation_request.rb +87 -0
- data/lib/shopify_api/rest/resources/2024_07/carrier_service.rb +120 -0
- data/lib/shopify_api/rest/resources/2024_07/checkout.rb +213 -0
- data/lib/shopify_api/rest/resources/2024_07/collect.rb +146 -0
- data/lib/shopify_api/rest/resources/2024_07/collection.rb +114 -0
- data/lib/shopify_api/rest/resources/2024_07/collection_listing.rb +159 -0
- data/lib/shopify_api/rest/resources/2024_07/comment.rb +287 -0
- data/lib/shopify_api/rest/resources/2024_07/country.rb +141 -0
- data/lib/shopify_api/rest/resources/2024_07/currency.rb +61 -0
- data/lib/shopify_api/rest/resources/2024_07/custom_collection.rb +191 -0
- data/lib/shopify_api/rest/resources/2024_07/customer.rb +335 -0
- data/lib/shopify_api/rest/resources/2024_07/customer_address.rb +215 -0
- data/lib/shopify_api/rest/resources/2024_07/deprecated_api_call.rb +61 -0
- data/lib/shopify_api/rest/resources/2024_07/discount_code.rb +226 -0
- data/lib/shopify_api/rest/resources/2024_07/dispute.rb +115 -0
- data/lib/shopify_api/rest/resources/2024_07/dispute_evidence.rb +121 -0
- data/lib/shopify_api/rest/resources/2024_07/dispute_file_upload.rb +85 -0
- data/lib/shopify_api/rest/resources/2024_07/draft_order.rb +279 -0
- data/lib/shopify_api/rest/resources/2024_07/event.rb +152 -0
- data/lib/shopify_api/rest/resources/2024_07/fulfillment.rb +235 -0
- data/lib/shopify_api/rest/resources/2024_07/fulfillment_event.rb +167 -0
- data/lib/shopify_api/rest/resources/2024_07/fulfillment_order.rb +326 -0
- data/lib/shopify_api/rest/resources/2024_07/fulfillment_request.rb +101 -0
- data/lib/shopify_api/rest/resources/2024_07/fulfillment_service.rb +134 -0
- data/lib/shopify_api/rest/resources/2024_07/gift_card.rb +222 -0
- data/lib/shopify_api/rest/resources/2024_07/gift_card_adjustment.rb +122 -0
- data/lib/shopify_api/rest/resources/2024_07/image.rb +161 -0
- data/lib/shopify_api/rest/resources/2024_07/inventory_item.rb +112 -0
- data/lib/shopify_api/rest/resources/2024_07/inventory_level.rb +183 -0
- data/lib/shopify_api/rest/resources/2024_07/location.rb +171 -0
- data/lib/shopify_api/rest/resources/2024_07/locations_for_move.rb +60 -0
- data/lib/shopify_api/rest/resources/2024_07/marketing_event.rb +213 -0
- data/lib/shopify_api/rest/resources/2024_07/metafield.rb +348 -0
- data/lib/shopify_api/rest/resources/2024_07/mobile_platform_application.rb +114 -0
- data/lib/shopify_api/rest/resources/2024_07/order.rb +489 -0
- data/lib/shopify_api/rest/resources/2024_07/order_risk.rb +148 -0
- data/lib/shopify_api/rest/resources/2024_07/page.rb +198 -0
- data/lib/shopify_api/rest/resources/2024_07/payment.rb +144 -0
- data/lib/shopify_api/rest/resources/2024_07/payment_gateway.rb +147 -0
- data/lib/shopify_api/rest/resources/2024_07/payment_transaction.rb +114 -0
- data/lib/shopify_api/rest/resources/2024_07/payout.rb +101 -0
- data/lib/shopify_api/rest/resources/2024_07/policy.rb +73 -0
- data/lib/shopify_api/rest/resources/2024_07/price_rule.rb +227 -0
- data/lib/shopify_api/rest/resources/2024_07/product.rb +227 -0
- data/lib/shopify_api/rest/resources/2024_07/product_listing.rb +200 -0
- data/lib/shopify_api/rest/resources/2024_07/product_resource_feedback.rb +92 -0
- data/lib/shopify_api/rest/resources/2024_07/province.rb +136 -0
- data/lib/shopify_api/rest/resources/2024_07/recurring_application_charge.rb +184 -0
- data/lib/shopify_api/rest/resources/2024_07/redirect.rb +143 -0
- data/lib/shopify_api/rest/resources/2024_07/refund.rb +155 -0
- data/lib/shopify_api/rest/resources/2024_07/resource_feedback.rb +77 -0
- data/lib/shopify_api/rest/resources/2024_07/script_tag.rb +159 -0
- data/lib/shopify_api/rest/resources/2024_07/shipping_zone.rb +87 -0
- data/lib/shopify_api/rest/resources/2024_07/shop.rb +232 -0
- data/lib/shopify_api/rest/resources/2024_07/smart_collection.rb +220 -0
- data/lib/shopify_api/rest/resources/2024_07/storefront_access_token.rb +91 -0
- data/lib/shopify_api/rest/resources/2024_07/tender_transaction.rb +97 -0
- data/lib/shopify_api/rest/resources/2024_07/theme.rb +127 -0
- data/lib/shopify_api/rest/resources/2024_07/transaction.rb +188 -0
- data/lib/shopify_api/rest/resources/2024_07/usage_charge.rb +106 -0
- data/lib/shopify_api/rest/resources/2024_07/user.rb +142 -0
- data/lib/shopify_api/rest/resources/2024_07/variant.rb +212 -0
- data/lib/shopify_api/rest/resources/2024_07/webhook.rb +172 -0
- data/lib/shopify_api/utils/attributes_comparator.rb +21 -4
- data/lib/shopify_api/version.rb +1 -1
- data/lib/shopify_api/webhooks/registry.rb +5 -4
- data/sorbet/rbi/shims/hash.rb +3 -0
- metadata +81 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b9b61bc6316148fcd2d905384def067e31b4fca5265bb1327a7502020ee6104
|
4
|
+
data.tar.gz: b3971fadf5e0159a68953fee0883be0a6fb5f366d0a25f39201cc6c134caa0bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 845bcdfac189ab0b17a07540c7cad3ba3621bbc83622d16b32a143f37eaaf9909499beb8791a2f0a0b6726e8283f833c5f1702ae804bbe38393ee5d6fa0fef67
|
7
|
+
data.tar.gz: f0216976a29a1b59ab8bf89316d3ccc9b3cbda4a42b81490cb5e25af19d9cf2b2b196484397816decdde75065f387e1f9bfddd5d82a42c110be8f13654c6f8d9
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Breaking change notice for version 15.0.0
|
2
|
+
|
3
|
+
## Removal of `ShopifyAPI::Webhooks::Handler`
|
4
|
+
|
5
|
+
The `ShopifyAPI::Webhooks::Handler` class has been removed in favor of `ShopifyAPI::Webhooks::WebhookHandler`. The `ShopifyAPI::Webhooks::WebhookHandler` class is now the recommended way to handle webhooks.
|
6
|
+
|
7
|
+
Make a module or class that includes or extends `ShopifyAPI::Webhooks::WebhookHandler` and implement the `handle` method which accepts the following named parameters: data: `WebhookMetadata`.
|
8
|
+
|
9
|
+
In v14, adding new fields to the callback would become a breaking change. To make this code more flexible, handlers will now receive an object that can be typed and extended.
|
10
|
+
|
11
|
+
`data` will have the following keys
|
12
|
+
- `topic`, `String` - The topic of the webhook
|
13
|
+
- `shop`, `String` - The shop domain of the webhook
|
14
|
+
- `body`, `T::Hash[String, T.untyped]`- The body of the webhook
|
15
|
+
- `webhook_id`, `String` - The id of the webhook event to [avoid duplicates](https://shopify.dev/docs/apps/webhooks/best-practices#ignore-duplicates)
|
16
|
+
- `api_version`, `String` - The api version of the webhook
|
17
|
+
|
18
|
+
### New implementation
|
19
|
+
```ruby
|
20
|
+
module WebhookHandler
|
21
|
+
extend ShopifyAPI::Webhooks::WebhookHandler
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def handle_webhook(data:)
|
25
|
+
puts "Received webhook! topic: #{data.topic} shop: #{data.shop} body: #{data.body} webhook_id: #{data.webhook_id} api_version: #{data.api_version"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
### Previous implementation
|
32
|
+
```ruby
|
33
|
+
module WebhookHandler
|
34
|
+
include ShopifyAPI::Webhooks::Handler
|
35
|
+
|
36
|
+
class << self
|
37
|
+
def handle(topic:, shop:, body:)
|
38
|
+
puts "Received webhook! topic: #{topic} shop: #{shop} body: #{body}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
data/CHANGELOG.md
CHANGED
@@ -4,8 +4,22 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
|
|
4
4
|
|
5
5
|
## Unreleased
|
6
6
|
|
7
|
+
## 14.5.0
|
8
|
+
|
9
|
+
- [#1327](https://github.com/Shopify/shopify-api-ruby/pull/1327) Support `?debug=true` parameter in GraphQL client requests
|
10
|
+
- [#1308](https://github.com/Shopify/shopify-api-ruby/pull/1308) Support hash_with_indifferent_access when creating REST objects from Shopify responses. Closes #1296
|
11
|
+
- [#1332](https://github.com/Shopify/shopify-api-ruby/pull/1332) Fixed an issue where `Customer` REST API PUT requests didn't send all of the fields in the `email_marketing_consent` attribute
|
12
|
+
- [#1335](https://github.com/Shopify/shopify-api-ruby/pull/1335) Add back the `current` methods for `Shop` and `RecurringApplicationCharge` resources
|
13
|
+
|
14
|
+
## 14.4.0
|
15
|
+
|
16
|
+
- [#1325](https://github.com/Shopify/shopify-api-ruby/pull/1325) Add support for 2024-07 API version
|
17
|
+
- [#1320](https://github.com/Shopify/shopify-api-ruby/pull/1320) Fix sorbet type on Shop.tax_shipping field
|
18
|
+
|
7
19
|
## 14.3.0
|
20
|
+
|
8
21
|
- [#1312](https://github.com/Shopify/shopify-api-ruby/pull/1312) Use same leeway for `exp` and `nbf` when parsing JWT
|
22
|
+
- [#1313](https://github.com/Shopify/shopify-api-ruby/pull/1313) Fix: Webhook Registry now working with response_as_struct enabled
|
9
23
|
- [#1314](https://github.com/Shopify/shopify-api-ruby/pull/1314)
|
10
24
|
- Add new session util method `SessionUtils::session_id_from_shopify_id_token`
|
11
25
|
- `SessionUtils::current_session_id` now accepts shopify Id token in the format of `Bearer this_token` or just `this_token`
|
@@ -15,9 +29,11 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
|
|
15
29
|
- `expires_at` alias for `exp` - returns the expiration time
|
16
30
|
|
17
31
|
## 14.2.0
|
32
|
+
|
18
33
|
- [#1309](https://github.com/Shopify/shopify-api-ruby/pull/1309) Add `Session#copy_attributes_from` method
|
19
34
|
|
20
35
|
## 14.1.0
|
36
|
+
|
21
37
|
- [#1071](https://github.com/Shopify/shopify-api-ruby/issues/1071) Fix FulfillmentEvent class types
|
22
38
|
- Fix: InventoryItem class `harmonized_system_code` attribute type which can be either integer, string or nil
|
23
39
|
- Fix: Variant class `inventory_quantity` attribute type which can be either integer, string or nil
|
@@ -26,10 +42,12 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
|
|
26
42
|
- [1305](https://github.com/Shopify/shopify-api-ruby/pull/1305/) Adds support for the `2024-04` API version.
|
27
43
|
|
28
44
|
## 14.0.1
|
45
|
+
|
29
46
|
- [#1288](https://github.com/Shopify/shopify-api-ruby/pull/1288) Fix FeatureDeprecatedError being raised without a message.
|
30
47
|
- [1290](https://github.com/Shopify/shopify-api-ruby/pull/1290) Move deprecation of `ShopifyAPI::Webhooks::Handler#handle` to version 15.0.0
|
31
48
|
|
32
49
|
## 14.0.0
|
50
|
+
|
33
51
|
- [#1274](https://github.com/Shopify/shopify-api-ruby/pull/1274) ⚠️ [Breaking] Update sorbet and rbi dependencies. Remove support for ruby 2.7. Minimum required Ruby version is 3.0
|
34
52
|
- [#1282](https://github.com/Shopify/shopify-api-ruby/pull/1282) Fixes a bug where diffing attributes to update not take into account of Array changes and required ids.
|
35
53
|
- [#1254](https://github.com/Shopify/shopify-api-ruby/pull/1254) Introduce token exchange API for fetching access tokens. This feature is currently unstable and cannot be used yet.
|
@@ -37,6 +55,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
|
|
37
55
|
- [#1275](https://github.com/Shopify/shopify-api-ruby/pull/1275) Allow adding custom headers in REST Resource HTTP calls.
|
38
56
|
|
39
57
|
## 13.4.0
|
58
|
+
|
40
59
|
- [#1210](https://github.com/Shopify/shopify-api-ruby/pull/1246) Add context option `response_as_struct` to allow GraphQL API responses to be accessed via dot notation.
|
41
60
|
- [#1257](https://github.com/Shopify/shopify-api-ruby/pull/1257) Add `api_call_limit` and `retry_request_after` to REST resources to expose rate limit information.
|
42
61
|
- [#1257](https://github.com/Shopify/shopify-api-ruby/pull/1257) Added support for the 2024-01 API version. This also includes a fix for the `for_hash` option when creating resources.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -79,6 +79,9 @@ Once your app can perform OAuth, it can now make authenticated Shopify API calls
|
|
79
79
|
|
80
80
|
## Breaking Change Notices
|
81
81
|
|
82
|
+
### Breaking change notice for version 15.0.0
|
83
|
+
See [BREAKING_CHANGES_FOR_V15](BREAKING_CHANGES_FOR_V15.md)
|
84
|
+
|
82
85
|
### Breaking change notice for version 10.0.0
|
83
86
|
See [BREAKING_CHANGES_FOR_V10](BREAKING_CHANGES_FOR_V10.md)
|
84
87
|
|
data/docs/usage/custom_apps.md
CHANGED
@@ -53,6 +53,15 @@ def configure_app
|
|
53
53
|
access_token: "the_token_for_your_custom_app_found_in_admin"
|
54
54
|
)
|
55
55
|
|
56
|
+
ShopifyAPI::Context.setup(
|
57
|
+
api_key: "<api-key>",
|
58
|
+
api_secret_key: "<api-secret-key>",
|
59
|
+
scope: "read_orders,read_products,etc",
|
60
|
+
is_embedded: true, # Set to true if you are building an embedded app
|
61
|
+
api_version: "2024-01", # The version of the API you would like to use
|
62
|
+
is_private: true, # Set to true if you have an existing private app
|
63
|
+
)
|
64
|
+
|
56
65
|
# Activate session to be used in all API calls
|
57
66
|
# session must be type `ShopifyAPI::Auth::Session`
|
58
67
|
ShopifyAPI::Context.activate_session(session)
|
@@ -62,9 +71,31 @@ end
|
|
62
71
|
def make_api_request
|
63
72
|
# 1. Create API client without session information
|
64
73
|
# The graphql_client will use `ShopifyAPI::Context.active_session` when making API calls
|
65
|
-
|
74
|
+
# you can set the api version for your GraphQL client to override the api version in ShopifyAPI::Context
|
75
|
+
graphql_client = ShopifyAPI::Clients::Graphql::Admin.new(api_version: "2024-07")
|
66
76
|
|
67
77
|
# 2. Use API client to make queries
|
78
|
+
# Graphql
|
79
|
+
query = <<~QUERY
|
80
|
+
{
|
81
|
+
products(first: 10) {
|
82
|
+
edges {
|
83
|
+
cursor
|
84
|
+
node {
|
85
|
+
id
|
86
|
+
title
|
87
|
+
onlineStoreUrl
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
QUERY
|
93
|
+
|
94
|
+
response = graphql_client.query(query: query)
|
95
|
+
|
96
|
+
# Use REST resources to make authenticated API call
|
97
|
+
product_count = ShopifyAPI::Product.count
|
98
|
+
|
68
99
|
...
|
69
100
|
end
|
70
101
|
|
data/docs/usage/oauth.md
CHANGED
@@ -5,31 +5,103 @@ Once the library is set up for your project, you'll be able to use it to start a
|
|
5
5
|
To do this, you can follow the steps below.
|
6
6
|
For more information on authenticating a Shopify app please see the [Types of Authentication](https://shopify.dev/docs/apps/auth#types-of-authentication) page.
|
7
7
|
|
8
|
+
#### Table of contents
|
9
|
+
- [Session Persistence](#session-persistence)
|
10
|
+
- [Supported types of OAuth Flow](#supported-types-of-oauth)
|
11
|
+
- [Note about Rails](#note-about-rails)
|
12
|
+
- [Performing OAuth](#performing-oauth-1)
|
13
|
+
- [Token Exchange](#token-exchange)
|
14
|
+
- [Authorization Code Grant Flow](#authorization-code-grant-flow)
|
15
|
+
- [Using OAuth Session to make authenticated API calls](#using-oauth-session-to-make-authenticated-api-calls)
|
16
|
+
|
8
17
|
## Session Persistence
|
9
18
|
Session persistence is deprecated from the `ShopifyAPI` library gem since [version 12.3.0](https://github.com/Shopify/shopify-api-ruby/blob/main/CHANGELOG.md#version-1230). The responsibility of session storage typically is fulfilled by the web framework middleware.
|
10
19
|
This API library's focus is on making requests and facilitate session creation.
|
11
20
|
|
12
21
|
⚠️ If you're not using the [ShopifyApp](https://github.com/Shopify/shopify_app) gem, you may use ShopifyAPI to perform OAuth to create sessions, but you must implement your own session storage method to persist the session information to be used in authenticated API calls.
|
13
22
|
|
23
|
+
## Supported Types of OAuth
|
24
|
+
> [!TIP]
|
25
|
+
> If you are building an embedded app, we **strongly** recommend using [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
|
26
|
+
with [token exchange](#token-exchange) instead of the authorization code grant flow.
|
27
|
+
|
28
|
+
1. [Token Exchange](#token-exchange)
|
29
|
+
- OAuth flow by exchanging the current user's [session token (shopify id token)](https://shopify.dev/docs/apps/auth/session-tokens) for an
|
30
|
+
[access token](https://shopify.dev/docs/apps/auth/access-token-types/online.md).
|
31
|
+
- Recommended and is only available for embedded apps
|
32
|
+
- Doesn't require redirects, which makes authorization faster and prevents flickering when loading the app
|
33
|
+
- Access scope changes are handled by [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
|
34
|
+
2. [Authorization Code Grant Flow](#authorization-code-grant-flow)
|
35
|
+
- OAuth flow that requires the app to redirect the user to Shopify for installation/authorization of the app to access the shop's data.
|
36
|
+
- Suitable for non-embedded apps
|
37
|
+
- Installations, and access scope changes are managed by the app
|
38
|
+
|
14
39
|
## Note about Rails
|
15
40
|
If using in the Rails framework, we highly recommend you use the [shopify_app](https://github.com/Shopify/shopify_app) gem to perform OAuth, you won't have to follow the instructions below to start your own OAuth flow.
|
16
41
|
- See `ShopifyApp`'s [documentation on session storage](https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/sessions.md#sessions)
|
17
42
|
|
18
43
|
If you aren't using Rails, you can look at how the `ShopifyApp` gem handles OAuth flow for further examples:
|
19
|
-
-
|
20
|
-
-
|
21
|
-
-
|
22
|
-
|
44
|
+
- Token Exchange Flow
|
45
|
+
- [Token Exchange](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/auth/token_exchange.rb)
|
46
|
+
- Completes token exchange flow to get online and offline access tokens
|
47
|
+
- Authorization Code Grant Flow
|
48
|
+
- [Session Controller](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/sessions_controller.rb)
|
49
|
+
- Triggering and redirecting user to **begin** OAuth flow
|
50
|
+
- [Callback Controller](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/callback_controller.rb)
|
51
|
+
- Creating / storing sessions to **complete** the OAuth flow
|
23
52
|
|
24
53
|
## Performing OAuth
|
54
|
+
### Token Exchange
|
25
55
|
#### Steps
|
56
|
+
1. Enable [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
|
57
|
+
by configuring your scopes [through the Shopify CLI](https://shopify.dev/docs/apps/tools/cli/configuration).
|
58
|
+
2. [Perform token exchange](#perform-token-exchange) to get an access token.
|
59
|
+
|
60
|
+
#### Perform Token Exchange
|
61
|
+
Use [`ShopifyAPI::Auth::TokenExchange`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/auth/token_exchange.rb) to
|
62
|
+
exchange a [session token](https://shopify.dev/docs/apps/auth/session-tokens) (Shopify Id Token) for an [access token](https://shopify.dev/docs/apps/auth/access-token-types/online.md).
|
63
|
+
|
64
|
+
#### Input
|
65
|
+
| Parameter | Type | Required? | Default Value | Notes |
|
66
|
+
| -------------- | ---------------------- | :-------: | :-----------: | ----------------------------------------------------------------------------------------------------------- |
|
67
|
+
| `shop` | `String` | Yes | - | A Shopify domain name in the form `{exampleshop}.myshopify.com`. |
|
68
|
+
| `session_token` | `String` | Yes| - | The session token (Shopify Id Token) provided by App Bridge in either the request 'Authorization' header or URL param when the app is loaded in Admin. |
|
69
|
+
| `requested_token_type` | `TokenExchange::RequestedTokenType` | Yes | - | The type of token requested. Online: `TokenExchange::RequestedTokenType::ONLINE_ACCESS_TOKEN` or offline: `TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN`. |
|
70
|
+
|
71
|
+
#### Output
|
72
|
+
This method returns the new `ShopifyAPI::Auth::Session` object from the token exchange,
|
73
|
+
your app should store this `Session` object to be used later [when making authenticated API calls](#using-oauth-session-to-make-authenticated-api-calls).
|
74
|
+
|
75
|
+
#### Example
|
76
|
+
```ruby
|
77
|
+
|
78
|
+
# `shop` is the shop domain name - "this-is-my-example-shop.myshopify.com"
|
79
|
+
# `session_token` is the session token provided by App Bridge either in:
|
80
|
+
# - the request 'Authorization' header as `Bearer this-is-the-session_token`
|
81
|
+
# - or as a URL param `id_token=this-is-the-session_token`
|
82
|
+
|
83
|
+
def authenticate(shop, session_token)
|
84
|
+
session = ShopifyAPI::Auth::TokenExchange.exchange_token(
|
85
|
+
shop: shop,
|
86
|
+
session_token: session_token,
|
87
|
+
requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN,
|
88
|
+
# or if you're requesting an online access token:
|
89
|
+
# requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::ONLINE_ACCESS_TOKEN,
|
90
|
+
)
|
91
|
+
|
92
|
+
SessionRepository.store_session(session)
|
93
|
+
end
|
94
|
+
|
95
|
+
```
|
96
|
+
|
97
|
+
### Authorization Code Grant Flow
|
98
|
+
##### Steps
|
26
99
|
1. [Add a route to start OAuth](#1-add-a-route-to-start-oauth)
|
27
100
|
2. [Add an Oauth callback route](#2-add-an-oauth-callback-route)
|
28
101
|
3. [Begin OAuth](#3-begin-oauth)
|
29
102
|
4. [Handle OAuth Callback](#4-handle-oauth-callback)
|
30
|
-
5. [Using OAuth Session to make authenticated API calls](#5-using-oauth-session-to-make-authenticated-api-calls)
|
31
103
|
|
32
|
-
|
104
|
+
#### 1. Add a route to start OAuth
|
33
105
|
Add a route to your app to start the OAuth process.
|
34
106
|
|
35
107
|
```ruby
|
@@ -40,7 +112,7 @@ class ShopifyAuthController < ApplicationController
|
|
40
112
|
end
|
41
113
|
```
|
42
114
|
|
43
|
-
|
115
|
+
#### 2. Add an OAuth callback route
|
44
116
|
After the app is authenticated with Shopify, the Shopify platform will send a request back to your app using this route
|
45
117
|
(which you will provide as the `redirect_path` parameter to `begin_auth` method, in [step 3 - Begin OAuth](#3-begin-oauth)).
|
46
118
|
```ruby
|
@@ -50,7 +122,7 @@ class ShopifyCallbackController < ApplicationController
|
|
50
122
|
end
|
51
123
|
```
|
52
124
|
|
53
|
-
|
125
|
+
#### 3. Begin OAuth
|
54
126
|
Use [`ShopifyAPI::Auth::Oauth.begin_auth`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/auth/oauth.rb#L22) method to start OAuth process for your app.
|
55
127
|
|
56
128
|
#### Input
|
@@ -74,7 +146,7 @@ Use [`ShopifyAPI::Auth::Oauth.begin_auth`](https://github.com/Shopify/shopify-ap
|
|
74
146
|
|`auth_route`|`String`|URI that will be used for redirecting the user to the Shopify Authentication screen|
|
75
147
|
|`cookie`|`ShopifyAPI::Auth::Oauth::SessionCookie`|A session cookie to store on the user's browser. |
|
76
148
|
|
77
|
-
|
149
|
+
##### Example
|
78
150
|
Your app should take the returned values from the `begin_auth` method and:
|
79
151
|
|
80
152
|
1. Set the cookie in the user's browser. We strongly recommend that you use secure, httpOnly cookies for this to help prevent session hijacking.
|
@@ -109,19 +181,19 @@ end
|
|
109
181
|
|
110
182
|
⚠️ You can see a concrete example in the `ShopifyApp` gem's [SessionController](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/sessions_controller.rb).
|
111
183
|
|
112
|
-
|
184
|
+
#### 4. Handle OAuth Callback
|
113
185
|
When the user grants permission to the app in Shopify admin, they'll be redirected back to the app's callback route
|
114
186
|
(configured in [Step 2 - Add an OAuth callback route](#2-add-an-oauth-callback-route)).
|
115
187
|
|
116
188
|
Use [`ShopifyAPI::AuthL::Oauth.validate_auth_callback`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/auth/oauth.rb#L60) method to finalize the OAuth process.
|
117
189
|
|
118
|
-
|
190
|
+
##### Input
|
119
191
|
| Parameter | Type | Notes |
|
120
192
|
| ------------ | --------| ----------------------------------------------------------------------------------------------------------- |
|
121
193
|
| `cookies` | `Hash` | All browser cookies in a hash format with key and value as `String` |
|
122
194
|
| `auth_query` | `ShopifyAPI::Auth::Oauth::AuthQuery`| An `AuthQuery` containing the authorization request information used to validate the request.|
|
123
195
|
|
124
|
-
|
196
|
+
##### Output
|
125
197
|
This method returns a hash containing the new session and a cookie to be set in the browser in form of:
|
126
198
|
```ruby
|
127
199
|
{
|
@@ -134,12 +206,12 @@ This method returns a hash containing the new session and a cookie to be set in
|
|
134
206
|
|`session`|`ShopifyAPI::Auth::Session`|A session object that contains necessary information to identify the session like `shop`, `access_token`, `scope`, etc.|
|
135
207
|
|`cookie` |`ShopifyAPI::Auth::Oauth::SessionCookie`|A session cookie to store on the user's browser. |
|
136
208
|
|
137
|
-
|
209
|
+
##### Example
|
138
210
|
Your app should call `validate_auth_callback` to construct the `Session` object and cookie that will be used later for authenticated API requests.
|
139
211
|
|
140
212
|
1. Call `validate_auth_callback` to construct `Session` and `SessionCookie`.
|
141
213
|
2. Update browser cookies with the new value for the session.
|
142
|
-
3. Store the `Session` object to be used later when making authenticated API calls.
|
214
|
+
3. Store the `Session` object to be used later when [making authenticated API calls](#using-oauth-session-to-make-authenticated-api-calls).
|
143
215
|
- See [Make a GraphQL API call](https://github.com/Shopify/shopify-api-ruby/blob/main/docs/usage/graphql.md), or
|
144
216
|
[Make a REST API call](https://github.com/Shopify/shopify-api-ruby/blob/main/docs/usage/rest.md) for examples on how to use the result `Session` object.
|
145
217
|
|
@@ -182,8 +254,8 @@ end
|
|
182
254
|
|
183
255
|
⚠️ You can see a concrete example in the `ShopifyApp` gem's [CallbackController](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/callback_controller.rb).
|
184
256
|
|
185
|
-
|
186
|
-
Once your OAuth flow is complete, and you have
|
257
|
+
## Using OAuth Session to make authenticated API calls
|
258
|
+
Once your OAuth flow is complete, and you have persisted your `Session` object, you may use that `Session` object to make authenticated API calls.
|
187
259
|
|
188
260
|
Example:
|
189
261
|
```ruby
|
data/docs/usage/webhooks.md
CHANGED
@@ -21,8 +21,9 @@ module WebhookHandler
|
|
21
21
|
extend ShopifyAPI::Webhooks::WebhookHandler
|
22
22
|
|
23
23
|
class << self
|
24
|
-
def
|
25
|
-
puts "Received webhook! topic: #{data.topic} shop: #{data.shop} body: #{data.body} webhook_id: #{data.webhook_id} api_version: #{data.api_version"
|
24
|
+
def handle(data:)
|
25
|
+
puts "Received webhook! topic: #{data.topic} shop: #{data.shop} body: #{data.body} webhook_id: #{data.webhook_id} api_version: #{data.api_version}"
|
26
|
+
perform_later(topic: data.topic, shop_domain: data.shop, webhook: data.body)
|
26
27
|
end
|
27
28
|
end
|
28
29
|
end
|
@@ -5,6 +5,7 @@ module ShopifyAPI
|
|
5
5
|
module AdminVersions
|
6
6
|
SUPPORTED_ADMIN_VERSIONS = T.let([
|
7
7
|
"unstable",
|
8
|
+
"2024-07",
|
8
9
|
"2024-04",
|
9
10
|
"2024-01",
|
10
11
|
"2023-10",
|
@@ -17,7 +18,7 @@ module ShopifyAPI
|
|
17
18
|
"2022-01",
|
18
19
|
], T::Array[String])
|
19
20
|
|
20
|
-
LATEST_SUPPORTED_ADMIN_VERSION = T.let("2024-
|
21
|
+
LATEST_SUPPORTED_ADMIN_VERSION = T.let("2024-07", String)
|
21
22
|
end
|
22
23
|
|
23
24
|
SUPPORTED_ADMIN_VERSIONS = ShopifyAPI::AdminVersions::SUPPORTED_ADMIN_VERSIONS
|
@@ -28,21 +28,32 @@ module ShopifyAPI
|
|
28
28
|
variables: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
|
29
29
|
headers: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
|
30
30
|
tries: Integer,
|
31
|
+
response_as_struct: T.nilable(T::Boolean),
|
32
|
+
debug: T::Boolean,
|
31
33
|
).returns(HttpResponse)
|
32
34
|
end
|
33
|
-
def query(
|
35
|
+
def query(
|
36
|
+
query:,
|
37
|
+
variables: nil,
|
38
|
+
headers: nil,
|
39
|
+
tries: 1,
|
40
|
+
response_as_struct: Context.response_as_struct,
|
41
|
+
debug: false
|
42
|
+
)
|
34
43
|
body = { query: query, variables: variables }
|
44
|
+
search_params = debug ? "?debug=true" : ""
|
45
|
+
|
35
46
|
@http_client.request(
|
36
47
|
HttpRequest.new(
|
37
48
|
http_method: :post,
|
38
|
-
path: "#{@api_version}/graphql.json",
|
49
|
+
path: "#{@api_version}/graphql.json#{search_params}",
|
39
50
|
body: body,
|
40
51
|
query: nil,
|
41
52
|
extra_headers: headers,
|
42
53
|
body_type: "application/json",
|
43
54
|
tries: tries,
|
44
55
|
),
|
45
|
-
response_as_struct:
|
56
|
+
response_as_struct: response_as_struct || false,
|
46
57
|
)
|
47
58
|
end
|
48
59
|
end
|
@@ -45,11 +45,21 @@ module ShopifyAPI
|
|
45
45
|
variables: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
|
46
46
|
headers: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
|
47
47
|
tries: Integer,
|
48
|
+
response_as_struct: T.nilable(T::Boolean),
|
49
|
+
debug: T::Boolean,
|
48
50
|
).returns(HttpResponse)
|
49
51
|
end
|
50
|
-
def query(
|
52
|
+
def query(
|
53
|
+
query:,
|
54
|
+
variables: nil,
|
55
|
+
headers: {},
|
56
|
+
tries: 1,
|
57
|
+
response_as_struct: Context.response_as_struct,
|
58
|
+
debug: false
|
59
|
+
)
|
51
60
|
T.must(headers).merge!({ @storefront_auth_header => @storefront_access_token })
|
52
|
-
super(query: query, variables: variables, headers: headers, tries: tries
|
61
|
+
super(query: query, variables: variables, headers: headers, tries: tries,
|
62
|
+
response_as_struct: response_as_struct, debug: debug)
|
53
63
|
end
|
54
64
|
end
|
55
65
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "active_support/inflector"
|
5
|
+
require "active_support/core_ext/hash/indifferent_access"
|
5
6
|
|
6
7
|
module ShopifyAPI
|
7
8
|
module Rest
|
@@ -16,6 +17,7 @@ module ShopifyAPI
|
|
16
17
|
@paths = T.let([], T::Array[T::Hash[Symbol, T.any(T::Array[Symbol], String, Symbol)]])
|
17
18
|
@custom_prefix = T.let(nil, T.nilable(String))
|
18
19
|
@read_only_attributes = T.let([], T.nilable(T::Array[Symbol]))
|
20
|
+
@atomic_hash_attributes = T.let([], T::Array[Symbol])
|
19
21
|
@aliased_properties = T.let({}, T::Hash[String, String])
|
20
22
|
|
21
23
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
@@ -61,6 +63,9 @@ module ShopifyAPI
|
|
61
63
|
sig { returns(T::Hash[Symbol, T::Class[T.anything]]) }
|
62
64
|
attr_reader :has_one
|
63
65
|
|
66
|
+
sig { returns(T.nilable(T::Array[Symbol])) }
|
67
|
+
attr_reader :atomic_hash_attributes
|
68
|
+
|
64
69
|
sig { returns(T.nilable(T::Hash[T.any(Symbol, String), String])) }
|
65
70
|
attr_accessor :headers
|
66
71
|
|
@@ -248,7 +253,7 @@ module ShopifyAPI
|
|
248
253
|
def create_instances_from_response(response:, session:)
|
249
254
|
objects = []
|
250
255
|
|
251
|
-
body = T.cast(response.body, T::Hash[String, T.untyped])
|
256
|
+
body = T.cast(response.body, T::Hash[String, T.untyped]).with_indifferent_access
|
252
257
|
|
253
258
|
response_names = json_response_body_names
|
254
259
|
|
@@ -417,6 +422,7 @@ module ShopifyAPI
|
|
417
422
|
ShopifyAPI::Utils::AttributesComparator.compare(
|
418
423
|
stringified_updatable_attributes,
|
419
424
|
stringified_new_attributes,
|
425
|
+
atomic_hash_attributes: self.class.atomic_hash_attributes || [],
|
420
426
|
)
|
421
427
|
end
|
422
428
|
|
@@ -55,6 +55,7 @@ module ShopifyAPI
|
|
55
55
|
metafield: Metafield
|
56
56
|
}, T::Hash[Symbol, Class])
|
57
57
|
@has_many = T.let({}, T::Hash[Symbol, Class])
|
58
|
+
@atomic_hash_attributes = [:email_marketing_consent]
|
58
59
|
@paths = T.let([
|
59
60
|
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
|
60
61
|
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
|
@@ -149,6 +149,15 @@ module ShopifyAPI
|
|
149
149
|
T.cast(response, T::Array[RecurringApplicationCharge])
|
150
150
|
end
|
151
151
|
|
152
|
+
sig do
|
153
|
+
params(session: Auth::Session)
|
154
|
+
.returns(T.nilable(RecurringApplicationCharge))
|
155
|
+
end
|
156
|
+
def current(session: ShopifyAPI::Context.active_session)
|
157
|
+
charges = all(session: session)
|
158
|
+
charges.select { |charge| charge.status == "active" }.first
|
159
|
+
end
|
160
|
+
|
152
161
|
end
|
153
162
|
|
154
163
|
sig do
|
@@ -66,7 +66,7 @@ module ShopifyAPI
|
|
66
66
|
@setup_required = T.let(nil, T.nilable(T::Boolean))
|
67
67
|
@shop_owner = T.let(nil, T.nilable(String))
|
68
68
|
@source = T.let(nil, T.nilable(String))
|
69
|
-
@tax_shipping = T.let(nil, T.nilable(
|
69
|
+
@tax_shipping = T.let(nil, T.nilable(T::Boolean))
|
70
70
|
@taxes_included = T.let(nil, T.nilable(T::Boolean))
|
71
71
|
@timezone = T.let(nil, T.nilable(String))
|
72
72
|
@transactional_sms_disabled = T.let(nil, T.nilable(T::Boolean))
|
@@ -179,7 +179,7 @@ module ShopifyAPI
|
|
179
179
|
attr_reader :shop_owner
|
180
180
|
sig { returns(T.nilable(String)) }
|
181
181
|
attr_reader :source
|
182
|
-
sig { returns(T.nilable(
|
182
|
+
sig { returns(T.nilable(T::Boolean)) }
|
183
183
|
attr_reader :tax_shipping
|
184
184
|
sig { returns(T.nilable(T::Boolean)) }
|
185
185
|
attr_reader :taxes_included
|
@@ -216,6 +216,16 @@ module ShopifyAPI
|
|
216
216
|
T.cast(response, T::Array[Shop])
|
217
217
|
end
|
218
218
|
|
219
|
+
sig do
|
220
|
+
params(
|
221
|
+
fields: T.untyped,
|
222
|
+
session: Auth::Session,
|
223
|
+
).returns(T.nilable(Shop))
|
224
|
+
end
|
225
|
+
def current(fields: nil, session: ShopifyAPI::Context.active_session)
|
226
|
+
all(session: session, fields: fields).first
|
227
|
+
end
|
228
|
+
|
219
229
|
end
|
220
230
|
|
221
231
|
end
|
@@ -55,6 +55,7 @@ module ShopifyAPI
|
|
55
55
|
metafield: Metafield
|
56
56
|
}, T::Hash[Symbol, Class])
|
57
57
|
@has_many = T.let({}, T::Hash[Symbol, Class])
|
58
|
+
@atomic_hash_attributes = [:email_marketing_consent]
|
58
59
|
@paths = T.let([
|
59
60
|
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
|
60
61
|
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
|
@@ -149,6 +149,15 @@ module ShopifyAPI
|
|
149
149
|
T.cast(response, T::Array[RecurringApplicationCharge])
|
150
150
|
end
|
151
151
|
|
152
|
+
sig do
|
153
|
+
params(session: Auth::Session)
|
154
|
+
.returns(T.nilable(RecurringApplicationCharge))
|
155
|
+
end
|
156
|
+
def current(session: ShopifyAPI::Context.active_session)
|
157
|
+
charges = all(session: session)
|
158
|
+
charges.select { |charge| charge.status == "active" }.first
|
159
|
+
end
|
160
|
+
|
152
161
|
end
|
153
162
|
|
154
163
|
sig do
|
@@ -66,7 +66,7 @@ module ShopifyAPI
|
|
66
66
|
@setup_required = T.let(nil, T.nilable(T::Boolean))
|
67
67
|
@shop_owner = T.let(nil, T.nilable(String))
|
68
68
|
@source = T.let(nil, T.nilable(String))
|
69
|
-
@tax_shipping = T.let(nil, T.nilable(
|
69
|
+
@tax_shipping = T.let(nil, T.nilable(T::Boolean))
|
70
70
|
@taxes_included = T.let(nil, T.nilable(T::Boolean))
|
71
71
|
@timezone = T.let(nil, T.nilable(String))
|
72
72
|
@transactional_sms_disabled = T.let(nil, T.nilable(T::Boolean))
|
@@ -179,7 +179,7 @@ module ShopifyAPI
|
|
179
179
|
attr_reader :shop_owner
|
180
180
|
sig { returns(T.nilable(String)) }
|
181
181
|
attr_reader :source
|
182
|
-
sig { returns(T.nilable(
|
182
|
+
sig { returns(T.nilable(T::Boolean)) }
|
183
183
|
attr_reader :tax_shipping
|
184
184
|
sig { returns(T.nilable(T::Boolean)) }
|
185
185
|
attr_reader :taxes_included
|
@@ -216,6 +216,16 @@ module ShopifyAPI
|
|
216
216
|
T.cast(response, T::Array[Shop])
|
217
217
|
end
|
218
218
|
|
219
|
+
sig do
|
220
|
+
params(
|
221
|
+
fields: T.untyped,
|
222
|
+
session: Auth::Session,
|
223
|
+
).returns(T.nilable(Shop))
|
224
|
+
end
|
225
|
+
def current(fields: nil, session: ShopifyAPI::Context.active_session)
|
226
|
+
all(session: session, fields: fields).first
|
227
|
+
end
|
228
|
+
|
219
229
|
end
|
220
230
|
|
221
231
|
end
|
@@ -55,6 +55,7 @@ module ShopifyAPI
|
|
55
55
|
metafield: Metafield
|
56
56
|
}, T::Hash[Symbol, Class])
|
57
57
|
@has_many = T.let({}, T::Hash[Symbol, Class])
|
58
|
+
@atomic_hash_attributes = [:email_marketing_consent]
|
58
59
|
@paths = T.let([
|
59
60
|
{http_method: :delete, operation: :delete, ids: [:id], path: "customers/<id>.json"},
|
60
61
|
{http_method: :get, operation: :count, ids: [], path: "customers/count.json"},
|
@@ -149,6 +149,15 @@ module ShopifyAPI
|
|
149
149
|
T.cast(response, T::Array[RecurringApplicationCharge])
|
150
150
|
end
|
151
151
|
|
152
|
+
sig do
|
153
|
+
params(session: Auth::Session)
|
154
|
+
.returns(T.nilable(RecurringApplicationCharge))
|
155
|
+
end
|
156
|
+
def current(session: ShopifyAPI::Context.active_session)
|
157
|
+
charges = all(session: session)
|
158
|
+
charges.select { |charge| charge.status == "active" }.first
|
159
|
+
end
|
160
|
+
|
152
161
|
end
|
153
162
|
|
154
163
|
sig do
|