shopify_api 16.1.0 → 16.3.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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/investigating-github-issues/SKILL.md +171 -0
  3. data/.claude/skills/investigating-github-issues/references/investigation-report-template.md +90 -0
  4. data/.claude/skills/shared/references/version-maintenance-policy.md +20 -0
  5. data/.github/CODEOWNERS +1 -1
  6. data/.github/workflows/gardener-investigate-issue.yml +226 -0
  7. data/.github/workflows/gardener-notify-event.yml +35 -0
  8. data/.github/workflows/gardener-notify-slack.yml +116 -0
  9. data/CHANGELOG.md +11 -0
  10. data/Gemfile.lock +9 -3
  11. data/REST_RESOURCES.md +9 -12
  12. data/docs/usage/oauth.md +4 -5
  13. data/lib/shopify_api/admin_versions.rb +2 -0
  14. data/lib/shopify_api/auth/client_credentials.rb +3 -2
  15. data/lib/shopify_api/auth/refresh_token.rb +3 -2
  16. data/lib/shopify_api/auth/token_exchange.rb +18 -8
  17. data/lib/shopify_api/clients/graphql/storefront.rb +3 -2
  18. data/lib/shopify_api/context.rb +5 -1
  19. data/lib/shopify_api/errors/invalid_shop_error.rb +9 -0
  20. data/lib/shopify_api/errors/rest_resource_not_loaded_error.rb +14 -0
  21. data/lib/shopify_api/rest/resources/2026_04/abandoned_checkout.rb +194 -0
  22. data/lib/shopify_api/rest/resources/2026_04/access_scope.rb +62 -0
  23. data/lib/shopify_api/rest/resources/2026_04/apple_pay_certificate.rb +109 -0
  24. data/lib/shopify_api/rest/resources/2026_04/application_charge.rb +113 -0
  25. data/lib/shopify_api/rest/resources/2026_04/application_credit.rb +95 -0
  26. data/lib/shopify_api/rest/resources/2026_04/article.rb +269 -0
  27. data/lib/shopify_api/rest/resources/2026_04/asset.rb +122 -0
  28. data/lib/shopify_api/rest/resources/2026_04/assigned_fulfillment_order.rb +92 -0
  29. data/lib/shopify_api/rest/resources/2026_04/balance.rb +58 -0
  30. data/lib/shopify_api/rest/resources/2026_04/blog.rb +166 -0
  31. data/lib/shopify_api/rest/resources/2026_04/cancellation_request.rb +87 -0
  32. data/lib/shopify_api/rest/resources/2026_04/carrier_service.rb +120 -0
  33. data/lib/shopify_api/rest/resources/2026_04/checkout.rb +213 -0
  34. data/lib/shopify_api/rest/resources/2026_04/collect.rb +146 -0
  35. data/lib/shopify_api/rest/resources/2026_04/collection.rb +114 -0
  36. data/lib/shopify_api/rest/resources/2026_04/collection_listing.rb +159 -0
  37. data/lib/shopify_api/rest/resources/2026_04/comment.rb +287 -0
  38. data/lib/shopify_api/rest/resources/2026_04/country.rb +141 -0
  39. data/lib/shopify_api/rest/resources/2026_04/currency.rb +61 -0
  40. data/lib/shopify_api/rest/resources/2026_04/custom_collection.rb +191 -0
  41. data/lib/shopify_api/rest/resources/2026_04/customer.rb +328 -0
  42. data/lib/shopify_api/rest/resources/2026_04/deprecated_api_call.rb +61 -0
  43. data/lib/shopify_api/rest/resources/2026_04/discount_code.rb +226 -0
  44. data/lib/shopify_api/rest/resources/2026_04/dispute.rb +115 -0
  45. data/lib/shopify_api/rest/resources/2026_04/dispute_evidence.rb +121 -0
  46. data/lib/shopify_api/rest/resources/2026_04/dispute_file_upload.rb +85 -0
  47. data/lib/shopify_api/rest/resources/2026_04/draft_order.rb +279 -0
  48. data/lib/shopify_api/rest/resources/2026_04/event.rb +152 -0
  49. data/lib/shopify_api/rest/resources/2026_04/fulfillment.rb +235 -0
  50. data/lib/shopify_api/rest/resources/2026_04/fulfillment_event.rb +167 -0
  51. data/lib/shopify_api/rest/resources/2026_04/fulfillment_order.rb +326 -0
  52. data/lib/shopify_api/rest/resources/2026_04/fulfillment_request.rb +116 -0
  53. data/lib/shopify_api/rest/resources/2026_04/fulfillment_service.rb +131 -0
  54. data/lib/shopify_api/rest/resources/2026_04/gift_card.rb +222 -0
  55. data/lib/shopify_api/rest/resources/2026_04/gift_card_adjustment.rb +122 -0
  56. data/lib/shopify_api/rest/resources/2026_04/image.rb +161 -0
  57. data/lib/shopify_api/rest/resources/2026_04/inventory_item.rb +112 -0
  58. data/lib/shopify_api/rest/resources/2026_04/inventory_level.rb +183 -0
  59. data/lib/shopify_api/rest/resources/2026_04/location.rb +171 -0
  60. data/lib/shopify_api/rest/resources/2026_04/locations_for_move.rb +60 -0
  61. data/lib/shopify_api/rest/resources/2026_04/marketing_event.rb +213 -0
  62. data/lib/shopify_api/rest/resources/2026_04/metafield.rb +348 -0
  63. data/lib/shopify_api/rest/resources/2026_04/mobile_platform_application.rb +120 -0
  64. data/lib/shopify_api/rest/resources/2026_04/order.rb +503 -0
  65. data/lib/shopify_api/rest/resources/2026_04/order_risk.rb +148 -0
  66. data/lib/shopify_api/rest/resources/2026_04/page.rb +198 -0
  67. data/lib/shopify_api/rest/resources/2026_04/payment.rb +98 -0
  68. data/lib/shopify_api/rest/resources/2026_04/payment_gateway.rb +147 -0
  69. data/lib/shopify_api/rest/resources/2026_04/payment_transaction.rb +117 -0
  70. data/lib/shopify_api/rest/resources/2026_04/payout.rb +101 -0
  71. data/lib/shopify_api/rest/resources/2026_04/policy.rb +73 -0
  72. data/lib/shopify_api/rest/resources/2026_04/price_rule.rb +227 -0
  73. data/lib/shopify_api/rest/resources/2026_04/product.rb +227 -0
  74. data/lib/shopify_api/rest/resources/2026_04/product_listing.rb +200 -0
  75. data/lib/shopify_api/rest/resources/2026_04/product_resource_feedback.rb +92 -0
  76. data/lib/shopify_api/rest/resources/2026_04/province.rb +136 -0
  77. data/lib/shopify_api/rest/resources/2026_04/recurring_application_charge.rb +184 -0
  78. data/lib/shopify_api/rest/resources/2026_04/redirect.rb +143 -0
  79. data/lib/shopify_api/rest/resources/2026_04/refund.rb +158 -0
  80. data/lib/shopify_api/rest/resources/2026_04/resource_feedback.rb +77 -0
  81. data/lib/shopify_api/rest/resources/2026_04/script_tag.rb +159 -0
  82. data/lib/shopify_api/rest/resources/2026_04/shipping_zone.rb +87 -0
  83. data/lib/shopify_api/rest/resources/2026_04/shop.rb +231 -0
  84. data/lib/shopify_api/rest/resources/2026_04/smart_collection.rb +220 -0
  85. data/lib/shopify_api/rest/resources/2026_04/storefront_access_token.rb +91 -0
  86. data/lib/shopify_api/rest/resources/2026_04/tender_transaction.rb +97 -0
  87. data/lib/shopify_api/rest/resources/2026_04/theme.rb +127 -0
  88. data/lib/shopify_api/rest/resources/2026_04/transaction.rb +194 -0
  89. data/lib/shopify_api/rest/resources/2026_04/usage_charge.rb +106 -0
  90. data/lib/shopify_api/rest/resources/2026_04/user.rb +142 -0
  91. data/lib/shopify_api/rest/resources/2026_04/variant.rb +212 -0
  92. data/lib/shopify_api/rest/resources/2026_04/webhook.rb +173 -0
  93. data/lib/shopify_api/rest/resources/2026_07/abandoned_checkout.rb +194 -0
  94. data/lib/shopify_api/rest/resources/2026_07/access_scope.rb +62 -0
  95. data/lib/shopify_api/rest/resources/2026_07/apple_pay_certificate.rb +109 -0
  96. data/lib/shopify_api/rest/resources/2026_07/application_charge.rb +113 -0
  97. data/lib/shopify_api/rest/resources/2026_07/application_credit.rb +95 -0
  98. data/lib/shopify_api/rest/resources/2026_07/article.rb +269 -0
  99. data/lib/shopify_api/rest/resources/2026_07/asset.rb +122 -0
  100. data/lib/shopify_api/rest/resources/2026_07/assigned_fulfillment_order.rb +92 -0
  101. data/lib/shopify_api/rest/resources/2026_07/balance.rb +58 -0
  102. data/lib/shopify_api/rest/resources/2026_07/blog.rb +166 -0
  103. data/lib/shopify_api/rest/resources/2026_07/cancellation_request.rb +87 -0
  104. data/lib/shopify_api/rest/resources/2026_07/carrier_service.rb +120 -0
  105. data/lib/shopify_api/rest/resources/2026_07/checkout.rb +213 -0
  106. data/lib/shopify_api/rest/resources/2026_07/collect.rb +146 -0
  107. data/lib/shopify_api/rest/resources/2026_07/collection.rb +114 -0
  108. data/lib/shopify_api/rest/resources/2026_07/collection_listing.rb +159 -0
  109. data/lib/shopify_api/rest/resources/2026_07/comment.rb +287 -0
  110. data/lib/shopify_api/rest/resources/2026_07/country.rb +141 -0
  111. data/lib/shopify_api/rest/resources/2026_07/currency.rb +61 -0
  112. data/lib/shopify_api/rest/resources/2026_07/custom_collection.rb +191 -0
  113. data/lib/shopify_api/rest/resources/2026_07/customer.rb +328 -0
  114. data/lib/shopify_api/rest/resources/2026_07/deprecated_api_call.rb +61 -0
  115. data/lib/shopify_api/rest/resources/2026_07/discount_code.rb +226 -0
  116. data/lib/shopify_api/rest/resources/2026_07/dispute.rb +115 -0
  117. data/lib/shopify_api/rest/resources/2026_07/dispute_evidence.rb +121 -0
  118. data/lib/shopify_api/rest/resources/2026_07/dispute_file_upload.rb +85 -0
  119. data/lib/shopify_api/rest/resources/2026_07/draft_order.rb +279 -0
  120. data/lib/shopify_api/rest/resources/2026_07/event.rb +152 -0
  121. data/lib/shopify_api/rest/resources/2026_07/fulfillment.rb +235 -0
  122. data/lib/shopify_api/rest/resources/2026_07/fulfillment_event.rb +167 -0
  123. data/lib/shopify_api/rest/resources/2026_07/fulfillment_order.rb +326 -0
  124. data/lib/shopify_api/rest/resources/2026_07/fulfillment_request.rb +116 -0
  125. data/lib/shopify_api/rest/resources/2026_07/fulfillment_service.rb +131 -0
  126. data/lib/shopify_api/rest/resources/2026_07/gift_card.rb +222 -0
  127. data/lib/shopify_api/rest/resources/2026_07/gift_card_adjustment.rb +122 -0
  128. data/lib/shopify_api/rest/resources/2026_07/image.rb +161 -0
  129. data/lib/shopify_api/rest/resources/2026_07/inventory_item.rb +112 -0
  130. data/lib/shopify_api/rest/resources/2026_07/inventory_level.rb +183 -0
  131. data/lib/shopify_api/rest/resources/2026_07/location.rb +171 -0
  132. data/lib/shopify_api/rest/resources/2026_07/locations_for_move.rb +60 -0
  133. data/lib/shopify_api/rest/resources/2026_07/marketing_event.rb +213 -0
  134. data/lib/shopify_api/rest/resources/2026_07/metafield.rb +348 -0
  135. data/lib/shopify_api/rest/resources/2026_07/mobile_platform_application.rb +120 -0
  136. data/lib/shopify_api/rest/resources/2026_07/order.rb +503 -0
  137. data/lib/shopify_api/rest/resources/2026_07/order_risk.rb +148 -0
  138. data/lib/shopify_api/rest/resources/2026_07/page.rb +198 -0
  139. data/lib/shopify_api/rest/resources/2026_07/payment.rb +98 -0
  140. data/lib/shopify_api/rest/resources/2026_07/payment_gateway.rb +147 -0
  141. data/lib/shopify_api/rest/resources/2026_07/payment_transaction.rb +117 -0
  142. data/lib/shopify_api/rest/resources/2026_07/payout.rb +101 -0
  143. data/lib/shopify_api/rest/resources/2026_07/policy.rb +73 -0
  144. data/lib/shopify_api/rest/resources/2026_07/price_rule.rb +227 -0
  145. data/lib/shopify_api/rest/resources/2026_07/product.rb +227 -0
  146. data/lib/shopify_api/rest/resources/2026_07/product_listing.rb +200 -0
  147. data/lib/shopify_api/rest/resources/2026_07/product_resource_feedback.rb +92 -0
  148. data/lib/shopify_api/rest/resources/2026_07/province.rb +136 -0
  149. data/lib/shopify_api/rest/resources/2026_07/recurring_application_charge.rb +184 -0
  150. data/lib/shopify_api/rest/resources/2026_07/redirect.rb +143 -0
  151. data/lib/shopify_api/rest/resources/2026_07/refund.rb +158 -0
  152. data/lib/shopify_api/rest/resources/2026_07/resource_feedback.rb +77 -0
  153. data/lib/shopify_api/rest/resources/2026_07/script_tag.rb +159 -0
  154. data/lib/shopify_api/rest/resources/2026_07/shipping_zone.rb +87 -0
  155. data/lib/shopify_api/rest/resources/2026_07/shop.rb +231 -0
  156. data/lib/shopify_api/rest/resources/2026_07/smart_collection.rb +220 -0
  157. data/lib/shopify_api/rest/resources/2026_07/storefront_access_token.rb +91 -0
  158. data/lib/shopify_api/rest/resources/2026_07/tender_transaction.rb +97 -0
  159. data/lib/shopify_api/rest/resources/2026_07/theme.rb +127 -0
  160. data/lib/shopify_api/rest/resources/2026_07/transaction.rb +194 -0
  161. data/lib/shopify_api/rest/resources/2026_07/usage_charge.rb +106 -0
  162. data/lib/shopify_api/rest/resources/2026_07/user.rb +142 -0
  163. data/lib/shopify_api/rest/resources/2026_07/variant.rb +212 -0
  164. data/lib/shopify_api/rest/resources/2026_07/webhook.rb +173 -0
  165. data/lib/shopify_api/utils/shop_validator.rb +118 -0
  166. data/lib/shopify_api/version.rb +1 -1
  167. data/lib/shopify_api/webhooks/request.rb +17 -10
  168. data/lib/shopify_api.rb +69 -0
  169. data/shopify_api.gemspec +2 -0
  170. metadata +183 -2
@@ -0,0 +1,116 @@
1
+ name: Gardener - Notify Slack
2
+ # Runs after `Gardener - Notify Event` completes and does the real work:
3
+ # applies the devtools-gardener label and posts a summary to Slack.
4
+ #
5
+ # The workflow_run trigger runs this job in the default-branch context with
6
+ # full GITHUB_TOKEN permissions and Actions secret access — this is what
7
+ # lets it succeed for Dependabot-opened PRs, where the upstream event
8
+ # workflow can't label or reach secrets directly.
9
+ on:
10
+ workflow_run:
11
+ workflows: ['Gardener - Notify Event']
12
+ types: [completed]
13
+
14
+ permissions:
15
+ contents: read
16
+ issues: write
17
+ pull-requests: write
18
+ actions: read
19
+
20
+ jobs:
21
+ notify:
22
+ # `conclusion == success` also covers runs where the capture job was
23
+ # skipped by its `if` gate (no matching label, etc.) — in that case
24
+ # no artifact was uploaded, so the download step below no-ops.
25
+ if: github.event.workflow_run.conclusion == 'success'
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - name: Download event payload
29
+ id: download
30
+ continue-on-error: true
31
+ uses: actions/download-artifact@v4
32
+ with:
33
+ name: gardener-event
34
+ run-id: ${{ github.event.workflow_run.id }}
35
+ github-token: ${{ secrets.GITHUB_TOKEN }}
36
+
37
+ - name: Add devtools-gardener label
38
+ if: steps.download.outcome == 'success'
39
+ env:
40
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41
+ GH_REPO: ${{ github.repository }}
42
+ run: |
43
+ ACTION=$(jq -r '.action' event.json)
44
+ # On `labeled` events the label is already there — skip.
45
+ if [ "$ACTION" != "opened" ]; then
46
+ exit 0
47
+ fi
48
+ NUMBER=$(jq -r '(.issue // .pull_request).number' event.json)
49
+ if jq -e 'has("pull_request")' event.json > /dev/null; then
50
+ gh pr edit "$NUMBER" --add-label devtools-gardener
51
+ else
52
+ gh issue edit "$NUMBER" --add-label devtools-gardener
53
+ fi
54
+
55
+ - name: Post to Slack
56
+ if: steps.download.outcome == 'success'
57
+ continue-on-error: true
58
+ env:
59
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_GARDENER_BOT_TOKEN }}
60
+ SLACK_CHANNEL_ID: ${{ vars.GARDENER_SLACK_CHANNEL_ID }}
61
+ run: |
62
+ KIND=$(jq -r 'if has("pull_request") then "PR" else "Issue" end' event.json)
63
+ # Pull the body out, truncate, then convert GitHub Markdown to
64
+ # Slack mrkdwn. Links and fenced code blocks are stashed before
65
+ # the HTML-escape pass so their contents survive verbatim (a `&`
66
+ # inside a URL must stay raw, and code content shouldn't be
67
+ # mangled). Blockquote `> ` markers are also stashed so the
68
+ # `>` → `>` escape doesn't break them. Everything else is
69
+ # HTML-escaped so user-supplied `<`, `>`, `&` can't collide
70
+ # with Slack link syntax or injected mentions like <!channel>.
71
+ BODY=$(jq -r '(.issue // .pull_request).body // ""' event.json)
72
+ if [ ${#BODY} -gt 1000 ]; then
73
+ BODY="${BODY:0:1000}…"
74
+ fi
75
+ BODY=$(printf '%s' "$BODY" | perl -0777 -pe '
76
+ my @u;
77
+ s{\[([^\]]+)\]\(([^)]+)\)}{push @u, $2; "\x01$#u\x02$1\x03"}ge;
78
+ my @c;
79
+ s{^```[^\n]*\n(.*?)\n```$}{push @c, $1; "\x04$#c\x05"}gems;
80
+ s/^> /\x06/gm;
81
+ s/^#{1,6}\s+(.+)$/*$1*/gm;
82
+ s/\*\*(.+?)\*\*/*$1*/g;
83
+ s/^(\s*)- \[x\]\s+/$1✓ /gm;
84
+ s/^(\s*)[-*]\s+/$1• /gm;
85
+ s/&/&amp;/g;
86
+ s/</&lt;/g;
87
+ s/>/&gt;/g;
88
+ s/\x06/> /g;
89
+ s{\x01(\d+)\x02(.*?)\x03}{"<$u[$1]|$2>"}ge;
90
+ s{\x04(\d+)\x05}{"```\n$c[$1]\n```"}ge;
91
+ ')
92
+ jq \
93
+ --arg channel "$SLACK_CHANNEL_ID" \
94
+ --arg kind "$KIND" \
95
+ --arg body "$BODY" \
96
+ '
97
+ def escape: gsub("&";"&amp;") | gsub("<";"&lt;") | gsub(">";"&gt;");
98
+
99
+ (.issue // .pull_request) as $i
100
+ | ([$i.labels[]?.name | select(. != "devtools-gardener")]
101
+ | map("`\(.)`") | join(" ")) as $labels
102
+ | (if $kind == "PR"
103
+ then " · \($i.changed_files) files, +\($i.additions)/-\($i.deletions)"
104
+ + (if $i.draft then " · draft" else "" end)
105
+ else "" end) as $meta
106
+ | [ "*<\($i.html_url)|\($kind) #\($i.number)>* — \(($i.title | escape))",
107
+ "_opened by \($i.user.login)\($meta)_" ]
108
+ + (if $body != "" then [$body] else [] end)
109
+ + (if $labels != "" then [$labels] else [] end)
110
+ | join("\n") as $msg
111
+ | { channel: $channel, text: "\($kind) #\($i.number): \($i.title)",
112
+ blocks: [{ type: "section", text: { type: "mrkdwn", text: $msg } }] }
113
+ ' event.json | curl -sf -X POST \
114
+ -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
115
+ -H 'Content-type: application/json; charset=utf-8' \
116
+ -d @- https://slack.com/api/chat.postMessage
data/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  Note: For changes to the API, see https://shopify.dev/changelog?filter=api
4
4
  ## Unreleased
5
5
 
6
+ ## 16.3.0 (2026-08-04)
7
+ - [#1443](https://github.com/Shopify/shopify-api-ruby/pull/1443) Add `ShopifyAPI::Utils::ShopValidator` with `sanitize_shop_domain` and `sanitize!`.
8
+ - [#1443](https://github.com/Shopify/shopify-api-ruby/pull/1443) Derive the target shop for `ShopifyAPI::Auth::TokenExchange.exchange_token` from the session token's `dest` claim. The `shop` argument is now deprecated and will be removed in the next major version.
9
+ - [#1454](https://github.com/Shopify/shopify-api-ruby/pull/1454) Add generated REST resource classes for the 2026-07 API version.
10
+ - [#1457](https://github.com/Shopify/shopify-api-ruby/pull/1457) Allow GraphQL Admin API and direct REST clients to target the 2026-10 API version. Generated REST resource classes for 2026-10 are not bundled yet.
11
+ - [#1457](https://github.com/Shopify/shopify-api-ruby/pull/1457) Raise `ShopifyAPI::Errors::RestResourceNotLoadedError` (a `NameError` subclass) with version-specific guidance when generated REST resources aren't bundled, instead of a bare `uninitialized constant` error.
12
+
13
+ ## 16.2.0 (2026-04-13)
14
+ - [#1442](https://github.com/Shopify/shopify-api-ruby/pull/1442) Add support for 2026-04 API version
15
+ - [#1437](https://github.com/Shopify/shopify-api-ruby/pull/1437) Support new `shopify-*` webhook header format
16
+
6
17
  ## 16.1.0 (2026-01-15)
7
18
  - Add support for 2026-01 API version
8
19
 
data/Gemfile.lock CHANGED
@@ -1,12 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shopify_api (16.1.0)
4
+ shopify_api (16.3.0)
5
5
  activesupport
6
+ addressable (~> 2.7)
6
7
  concurrent-ruby
7
8
  hash_diff
8
9
  httparty
9
10
  jwt
11
+ logger
10
12
  oj
11
13
  openssl
12
14
  securerandom
@@ -60,8 +62,9 @@ GEM
60
62
  logger (1.6.1)
61
63
  method_source (1.0.0)
62
64
  mini_mime (1.1.5)
63
- minitest (5.15.0)
64
- mocha (1.13.0)
65
+ minitest (5.27.0)
66
+ mocha (3.0.1)
67
+ ruby2_keywords (>= 0.0.5)
65
68
  multi_xml (0.6.0)
66
69
  mutex_m (0.3.0)
67
70
  netrc (0.11.0)
@@ -109,6 +112,7 @@ GEM
109
112
  rubocop-sorbet (0.6.11)
110
113
  rubocop (>= 0.90.0)
111
114
  ruby-progressbar (1.13.0)
115
+ ruby2_keywords (0.0.5)
112
116
  securerandom (0.3.2)
113
117
  sorbet (0.5.11230)
114
118
  sorbet-static (= 0.5.11230)
@@ -151,6 +155,8 @@ GEM
151
155
  PLATFORMS
152
156
  arm64-darwin-21
153
157
  arm64-darwin-23
158
+ arm64-darwin-24
159
+ arm64-darwin-25
154
160
  universal-darwin-22
155
161
  x86_64-linux
156
162
 
data/REST_RESOURCES.md CHANGED
@@ -9,29 +9,27 @@ This guide provides step-by-step instructions for adding a new API version to th
9
9
  ## Step 1: Update API Version Constants
10
10
 
11
11
  ### Update admin_versions.rb
12
- Edit `lib/shopify_api/admin_versions.rb` to add your new version:
12
+ Edit `lib/shopify_api/admin_versions.rb` to add your new version and the next upcoming release candidate:
13
13
 
14
14
  ```ruby
15
15
  module ShopifyAPI
16
16
  module AdminVersions
17
17
  SUPPORTED_ADMIN_VERSIONS = T.let([
18
18
  "unstable",
19
- "2025-10", # Add new version here
20
- "2025-07",
21
- "2025-04",
19
+ "2026-07", # Next release candidate (always add the next quarterly version)
20
+ "2026-04", # New version being added
21
+ "2026-01",
22
22
  # ... other versions
23
23
  ], T::Array[String])
24
-
25
- LATEST_SUPPORTED_ADMIN_VERSION = T.let("2025-07", String) # Update if this is the latest stable
26
- RELEASE_CANDIDATE_ADMIN_VERSION = T.let("2025-10", String) # Update if this is RC
27
24
  end
28
25
  end
29
26
  ```
30
27
 
31
28
  **Version Management:**
32
- - Add new versions to the top of `SUPPORTED_ADMIN_VERSIONS`
33
- - Update `LATEST_SUPPORTED_ADMIN_VERSION` when the version is stable
34
- - Update `RELEASE_CANDIDATE_ADMIN_VERSION` for release candidates
29
+ - Add new versions to the top of `SUPPORTED_ADMIN_VERSIONS` (below "unstable")
30
+ - Also add the **next quarterly version** as the upcoming release candidate
31
+ - Shopify API versions follow a quarterly cadence: 01, 04, 07, 10
32
+ - For example, when adding 2026-04, also add 2026-07
35
33
 
36
34
  ## Step 2: Create Directory Structure
37
35
 
@@ -147,8 +145,7 @@ bundle exec rake test TEST=test/rest/{NEW_VERSION}/*
147
145
  ## Checklist
148
146
 
149
147
  - [ ] Added new version to `SUPPORTED_ADMIN_VERSIONS` in `admin_versions.rb`
150
- - [ ] Updated `LATEST_SUPPORTED_ADMIN_VERSION` if applicable
151
- - [ ] Updated `RELEASE_CANDIDATE_ADMIN_VERSION` if applicable
148
+ - [ ] Added next quarterly version as release candidate to `SUPPORTED_ADMIN_VERSIONS`
152
149
  - [ ] Created `lib/shopify_api/rest/resources/{NEW_VERSION}/` directory
153
150
  - [ ] Created `test/rest/{NEW_VERSION}/` directory
154
151
  - [ ] Copied all resource files from previous version
data/docs/usage/oauth.md CHANGED
@@ -72,9 +72,9 @@ exchange a [session token](https://shopify.dev/docs/apps/auth/session-tokens) (S
72
72
  #### Input
73
73
  | Parameter | Type | Required? | Default Value | Notes |
74
74
  | -------------- | ---------------------- | :-------: | :-----------: | ----------------------------------------------------------------------------------------------------------- |
75
- | `shop` | `String` | Yes | - | A Shopify domain name in the form `{exampleshop}.myshopify.com`. |
76
- | `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. |
75
+ | `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. Its `dest` claim determines which shop receives the token exchange request. |
77
76
  | `requested_token_type` | `TokenExchange::RequestedTokenType` | Yes | - | The type of token requested. Online: `TokenExchange::RequestedTokenType::ONLINE_ACCESS_TOKEN` or offline: `TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN`. |
77
+ | `shop` | `String` | No | `nil` | **Deprecated**, will be removed in v17.0.0. Ignored for the request host; the shop always comes from the session token `dest` claim. If passed, logs a deprecation warning. |
78
78
 
79
79
  #### Output
80
80
  This method returns the new `ShopifyAPI::Auth::Session` object from the token exchange,
@@ -83,14 +83,13 @@ your app should store this `Session` object to be used later [when making authen
83
83
  #### Example
84
84
  ```ruby
85
85
 
86
- # `shop` is the shop domain name - "this-is-my-example-shop.myshopify.com"
87
86
  # `session_token` is the session token provided by App Bridge either in:
88
87
  # - the request 'Authorization' header as `Bearer this-is-the-session_token`
89
88
  # - or as a URL param `id_token=this-is-the-session_token`
89
+ # The shop is taken from the token's `dest` claim (see session token documentation).
90
90
 
91
- def authenticate(shop, session_token)
91
+ def authenticate(session_token)
92
92
  session = ShopifyAPI::Auth::TokenExchange.exchange_token(
93
- shop: shop,
94
93
  session_token: session_token,
95
94
  requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN,
96
95
  # or if you're requesting an online access token:
@@ -5,6 +5,8 @@ module ShopifyAPI
5
5
  module AdminVersions
6
6
  SUPPORTED_ADMIN_VERSIONS = T.let([
7
7
  "unstable",
8
+ "2026-10",
9
+ "2026-07",
8
10
  "2026-04",
9
11
  "2026-01",
10
12
  "2025-10",
@@ -22,7 +22,8 @@ module ShopifyAPI
22
22
  "ShopifyAPI::Context not setup, please call ShopifyAPI::Context.setup"
23
23
  end
24
24
 
25
- shop_session = ShopifyAPI::Auth::Session.new(shop: shop)
25
+ validated_shop = Utils::ShopValidator.sanitize!(shop)
26
+ shop_session = ShopifyAPI::Auth::Session.new(shop: validated_shop)
26
27
  body = {
27
28
  client_id: ShopifyAPI::Context.api_key,
28
29
  client_secret: ShopifyAPI::Context.api_secret_key,
@@ -42,7 +43,7 @@ module ShopifyAPI
42
43
  response_hash = T.cast(response.body, T::Hash[String, T.untyped]).to_h
43
44
 
44
45
  Session.from(
45
- shop: shop,
46
+ shop: validated_shop,
46
47
  access_token_response: Oauth::AccessTokenResponse.from_hash(response_hash),
47
48
  )
48
49
  end
@@ -21,7 +21,8 @@ module ShopifyAPI
21
21
  "ShopifyAPI::Context not setup, please call ShopifyAPI::Context.setup"
22
22
  end
23
23
 
24
- shop_session = ShopifyAPI::Auth::Session.new(shop:)
24
+ validated_shop = Utils::ShopValidator.sanitize!(shop)
25
+ shop_session = ShopifyAPI::Auth::Session.new(shop: validated_shop)
25
26
  body = {
26
27
  client_id: ShopifyAPI::Context.api_key,
27
28
  client_secret: ShopifyAPI::Context.api_secret_key,
@@ -47,7 +48,7 @@ module ShopifyAPI
47
48
  session_params = T.cast(response.body, T::Hash[String, T.untyped]).to_h
48
49
 
49
50
  Session.from(
50
- shop:,
51
+ shop: validated_shop,
51
52
  access_token_response: Oauth::AccessTokenResponse.from_hash(session_params),
52
53
  )
53
54
  end
@@ -21,12 +21,12 @@ module ShopifyAPI
21
21
 
22
22
  sig do
23
23
  params(
24
- shop: String,
25
24
  session_token: String,
26
25
  requested_token_type: RequestedTokenType,
26
+ shop: T.nilable(String),
27
27
  ).returns(ShopifyAPI::Auth::Session)
28
28
  end
29
- def exchange_token(shop:, session_token:, requested_token_type:)
29
+ def exchange_token(session_token:, requested_token_type:, shop: nil)
30
30
  unless ShopifyAPI::Context.setup?
31
31
  raise ShopifyAPI::Errors::ContextNotSetupError,
32
32
  "ShopifyAPI::Context not setup, please call ShopifyAPI::Context.setup"
@@ -36,10 +36,19 @@ module ShopifyAPI
36
36
  raise ShopifyAPI::Errors::UnsupportedOauthError,
37
37
  "Cannot perform OAuth Token Exchange for non embedded apps." unless ShopifyAPI::Context.embedded?
38
38
 
39
- # Validate the session token content
40
- ShopifyAPI::Auth::JwtPayload.new(session_token)
39
+ # Validate the session token and use the shop from the token's `dest` claim
40
+ jwt_payload = ShopifyAPI::Auth::JwtPayload.new(session_token)
41
+ dest_shop = jwt_payload.shop
42
+
43
+ if shop
44
+ ShopifyAPI::Logger.deprecated(
45
+ "The `shop` parameter for `exchange_token` is deprecated and will be removed in v17. " \
46
+ "The shop is now always taken from the session token's `dest` claim.",
47
+ "17.0.0",
48
+ )
49
+ end
41
50
 
42
- shop_session = ShopifyAPI::Auth::Session.new(shop: shop)
51
+ shop_session = ShopifyAPI::Auth::Session.new(shop: dest_shop)
43
52
  body = {
44
53
  client_id: ShopifyAPI::Context.api_key,
45
54
  client_secret: ShopifyAPI::Context.api_secret_key,
@@ -74,7 +83,7 @@ module ShopifyAPI
74
83
  session_params = T.cast(response.body, T::Hash[String, T.untyped]).to_h
75
84
 
76
85
  Session.from(
77
- shop: shop,
86
+ shop: dest_shop,
78
87
  access_token_response: Oauth::AccessTokenResponse.from_hash(session_params),
79
88
  )
80
89
  end
@@ -91,7 +100,8 @@ module ShopifyAPI
91
100
  "ShopifyAPI::Context not setup, please call ShopifyAPI::Context.setup"
92
101
  end
93
102
 
94
- shop_session = ShopifyAPI::Auth::Session.new(shop: shop)
103
+ validated_shop = Utils::ShopValidator.sanitize!(shop)
104
+ shop_session = ShopifyAPI::Auth::Session.new(shop: validated_shop)
95
105
  body = {
96
106
  client_id: ShopifyAPI::Context.api_key,
97
107
  client_secret: ShopifyAPI::Context.api_secret_key,
@@ -120,7 +130,7 @@ module ShopifyAPI
120
130
  session_params = T.cast(response.body, T::Hash[String, T.untyped]).to_h
121
131
 
122
132
  Session.from(
123
- shop: shop,
133
+ shop: validated_shop,
124
134
  access_token_response: Oauth::AccessTokenResponse.from_hash(session_params),
125
135
  )
126
136
  end
@@ -19,9 +19,10 @@ module ShopifyAPI
19
19
  raise ArgumentError, "Storefront client requires either private_token or public_token to be provided"
20
20
  end
21
21
 
22
+ validated_shop = Utils::ShopValidator.sanitize!(shop)
22
23
  session = Auth::Session.new(
23
- id: shop,
24
- shop: shop,
24
+ id: validated_shop,
25
+ shop: validated_shop,
25
26
  access_token: "",
26
27
  is_online: false,
27
28
  )
@@ -114,7 +114,11 @@ module ShopifyAPI
114
114
 
115
115
  unless Dir.exist?(path)
116
116
  unless @notified_missing_resources_folder.key?(api_version)
117
- @logger.warn("Cannot autoload REST resources for API version '#{version_folder_name}', folder is missing")
117
+ @logger.warn(
118
+ "shopify_api #{ShopifyAPI::VERSION} does not bundle REST resources for API version " \
119
+ "'#{api_version}', so REST resource classes are unavailable. The GraphQL Admin API and " \
120
+ "the REST client are unaffected.",
121
+ )
118
122
  @notified_missing_resources_folder[api_version] = true
119
123
  end
120
124
 
@@ -0,0 +1,9 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ShopifyAPI
5
+ module Errors
6
+ class InvalidShopError < StandardError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module ShopifyAPI
5
+ module Errors
6
+ # Raised when a REST resource class is referenced but this gem version does
7
+ # not bundle REST resources for the active API version.
8
+ #
9
+ # Subclasses NameError so existing `rescue NameError` handling keeps working.
10
+ # Construct it with the missing constant name - `new(message, name)` - so
11
+ # NameError#name keeps returning it, matching the NameError Ruby would raise.
12
+ class RestResourceNotLoadedError < NameError; end
13
+ end
14
+ end
@@ -0,0 +1,194 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ ########################################################################################################################
5
+ # This file is auto-generated. If you have an issue, please create a GitHub issue. #
6
+ ########################################################################################################################
7
+
8
+ module ShopifyAPI
9
+ class AbandonedCheckout < ShopifyAPI::Rest::Base
10
+ extend T::Sig
11
+
12
+ @prev_page_info = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
13
+ @next_page_info = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
14
+
15
+ @api_call_limit = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
16
+ @retry_request_after = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
17
+
18
+ sig { params(session: T.nilable(ShopifyAPI::Auth::Session), from_hash: T.nilable(T::Hash[T.untyped, T.untyped])).void }
19
+ def initialize(session: ShopifyAPI::Context.active_session, from_hash: nil)
20
+
21
+ @abandoned_checkout_url = T.let(nil, T.nilable(String))
22
+ @billing_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped]))
23
+ @buyer_accepts_marketing = T.let(nil, T.nilable(T::Boolean))
24
+ @buyer_accepts_sms_marketing = T.let(nil, T.nilable(T::Boolean))
25
+ @cart_token = T.let(nil, T.nilable(String))
26
+ @closed_at = T.let(nil, T.nilable(String))
27
+ @completed_at = T.let(nil, T.nilable(String))
28
+ @created_at = T.let(nil, T.nilable(String))
29
+ @currency = T.let(nil, T.nilable(Currency))
30
+ @customer = T.let(nil, T.nilable(Customer))
31
+ @customer_locale = T.let(nil, T.nilable(String))
32
+ @device_id = T.let(nil, T.nilable(Integer))
33
+ @discount_codes = T.let(nil, T.nilable(T::Array[T.untyped]))
34
+ @email = T.let(nil, T.nilable(String))
35
+ @gateway = T.let(nil, T.nilable(String))
36
+ @id = T.let(nil, T.nilable(Integer))
37
+ @landing_site = T.let(nil, T.nilable(String))
38
+ @line_items = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped]))
39
+ @location_id = T.let(nil, T.nilable(Integer))
40
+ @note = T.let(nil, T.nilable(String))
41
+ @phone = T.let(nil, T.nilable(String))
42
+ @presentment_currency = T.let(nil, T.nilable(String))
43
+ @referring_site = T.let(nil, T.nilable(String))
44
+ @shipping_address = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped]))
45
+ @shipping_lines = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped]))
46
+ @sms_marketing_phone = T.let(nil, T.nilable(String))
47
+ @source_name = T.let(nil, T.nilable(String))
48
+ @subtotal_price = T.let(nil, T.nilable(String))
49
+ @tax_lines = T.let(nil, T.nilable(T::Hash[T.untyped, T.untyped]))
50
+ @taxes_included = T.let(nil, T.nilable(T::Boolean))
51
+ @token = T.let(nil, T.nilable(String))
52
+ @total_discounts = T.let(nil, T.nilable(String))
53
+ @total_duties = T.let(nil, T.nilable(String))
54
+ @total_line_items_price = T.let(nil, T.nilable(String))
55
+ @total_price = T.let(nil, T.nilable(String))
56
+ @total_tax = T.let(nil, T.nilable(String))
57
+ @total_weight = T.let(nil, T.nilable(Integer))
58
+ @updated_at = T.let(nil, T.nilable(String))
59
+ @user_id = T.let(nil, T.nilable(Integer))
60
+
61
+ super(session: session, from_hash: from_hash)
62
+ end
63
+
64
+ @has_one = T.let({
65
+ currency: Currency,
66
+ customer: Customer
67
+ }, T::Hash[Symbol, Class])
68
+ @has_many = T.let({
69
+ discount_codes: DiscountCode
70
+ }, T::Hash[Symbol, Class])
71
+ @paths = T.let([
72
+ {http_method: :get, operation: :checkouts, ids: [], path: "checkouts.json"},
73
+ {http_method: :get, operation: :checkouts, ids: [], path: "checkouts.json"}
74
+ ], T::Array[T::Hash[String, T.any(T::Array[Symbol], String, Symbol)]])
75
+
76
+ sig { returns(T.nilable(String)) }
77
+ attr_reader :abandoned_checkout_url
78
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
79
+ attr_reader :billing_address
80
+ sig { returns(T.nilable(T::Boolean)) }
81
+ attr_reader :buyer_accepts_marketing
82
+ sig { returns(T.nilable(T::Boolean)) }
83
+ attr_reader :buyer_accepts_sms_marketing
84
+ sig { returns(T.nilable(String)) }
85
+ attr_reader :cart_token
86
+ sig { returns(T.nilable(String)) }
87
+ attr_reader :closed_at
88
+ sig { returns(T.nilable(String)) }
89
+ attr_reader :completed_at
90
+ sig { returns(T.nilable(String)) }
91
+ attr_reader :created_at
92
+ sig { returns(T.nilable(Currency)) }
93
+ attr_reader :currency
94
+ sig { returns(T.nilable(Customer)) }
95
+ attr_reader :customer
96
+ sig { returns(T.nilable(String)) }
97
+ attr_reader :customer_locale
98
+ sig { returns(T.nilable(Integer)) }
99
+ attr_reader :device_id
100
+ sig { returns(T.nilable(T::Array[DiscountCode])) }
101
+ attr_reader :discount_codes
102
+ sig { returns(T.nilable(String)) }
103
+ attr_reader :email
104
+ sig { returns(T.nilable(String)) }
105
+ attr_reader :gateway
106
+ sig { returns(T.nilable(Integer)) }
107
+ attr_reader :id
108
+ sig { returns(T.nilable(String)) }
109
+ attr_reader :landing_site
110
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
111
+ attr_reader :line_items
112
+ sig { returns(T.nilable(Integer)) }
113
+ attr_reader :location_id
114
+ sig { returns(T.nilable(String)) }
115
+ attr_reader :note
116
+ sig { returns(T.nilable(String)) }
117
+ attr_reader :phone
118
+ sig { returns(T.nilable(String)) }
119
+ attr_reader :presentment_currency
120
+ sig { returns(T.nilable(String)) }
121
+ attr_reader :referring_site
122
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
123
+ attr_reader :shipping_address
124
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
125
+ attr_reader :shipping_lines
126
+ sig { returns(T.nilable(String)) }
127
+ attr_reader :sms_marketing_phone
128
+ sig { returns(T.nilable(String)) }
129
+ attr_reader :source_name
130
+ sig { returns(T.nilable(String)) }
131
+ attr_reader :subtotal_price
132
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
133
+ attr_reader :tax_lines
134
+ sig { returns(T.nilable(T::Boolean)) }
135
+ attr_reader :taxes_included
136
+ sig { returns(T.nilable(String)) }
137
+ attr_reader :token
138
+ sig { returns(T.nilable(String)) }
139
+ attr_reader :total_discounts
140
+ sig { returns(T.nilable(String)) }
141
+ attr_reader :total_duties
142
+ sig { returns(T.nilable(String)) }
143
+ attr_reader :total_line_items_price
144
+ sig { returns(T.nilable(String)) }
145
+ attr_reader :total_price
146
+ sig { returns(T.nilable(String)) }
147
+ attr_reader :total_tax
148
+ sig { returns(T.nilable(Integer)) }
149
+ attr_reader :total_weight
150
+ sig { returns(T.nilable(String)) }
151
+ attr_reader :updated_at
152
+ sig { returns(T.nilable(Integer)) }
153
+ attr_reader :user_id
154
+
155
+ class << self
156
+ sig do
157
+ params(
158
+ since_id: T.untyped,
159
+ created_at_min: T.untyped,
160
+ created_at_max: T.untyped,
161
+ updated_at_min: T.untyped,
162
+ updated_at_max: T.untyped,
163
+ status: T.untyped,
164
+ limit: T.untyped,
165
+ session: Auth::Session,
166
+ kwargs: T.untyped
167
+ ).returns(T.untyped)
168
+ end
169
+ def checkouts(
170
+ since_id: nil,
171
+ created_at_min: nil,
172
+ created_at_max: nil,
173
+ updated_at_min: nil,
174
+ updated_at_max: nil,
175
+ status: nil,
176
+ limit: nil,
177
+ session: ShopifyAPI::Context.active_session,
178
+ **kwargs
179
+ )
180
+ request(
181
+ http_method: :get,
182
+ operation: :checkouts,
183
+ session: session,
184
+ ids: {},
185
+ params: {since_id: since_id, created_at_min: created_at_min, created_at_max: created_at_max, updated_at_min: updated_at_min, updated_at_max: updated_at_max, status: status, limit: limit}.merge(kwargs).compact,
186
+ body: {},
187
+ entity: nil,
188
+ )
189
+ end
190
+
191
+ end
192
+
193
+ end
194
+ end