Skip to content

Commit b903d55

Browse files
authored
Merge branch 'main' into liz/add-release-candidate-api-version
2 parents 59af0b0 + c6029ed commit b903d55

File tree

153 files changed

+35866
-31
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

153 files changed

+35866
-31
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
55

66
- [#1362](https://github.com/Shopify/shopify-api-ruby/pull/1362) Add support for client credentials grant
77
- [#1366](https://github.com/Shopify/shopify-api-ruby/pull/1366) Add support for release candidate API versions
8+
- [#1372](https://github.com/Shopify/shopify-api-ruby/pull/1372) Add support for 2025-04 API version
9+
- [#1369](https://github.com/Shopify/shopify-api-ruby/pull/1369) Make `sub` and `sid` jwt claims optional (Checkout ui extension support)
10+
- [#1370](https://github.com/Shopify/shopify-api-ruby/pull/1370) Add support for Shopify internal hosts
811

912
## 14.8.0
1013

lib/shopify_api/admin_versions.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module AdminVersions
77

88
SUPPORTED_ADMIN_VERSIONS = T.let([
99
"unstable",
10+
"2025-07"
1011
"2025-04",
1112
"2025-01",
1213
"2024-10",
@@ -23,8 +24,8 @@ module AdminVersions
2324
"2022-01",
2425
], T::Array[String])
2526

26-
LATEST_SUPPORTED_ADMIN_VERSION = T.let("2025-01", String)
27-
RELEASE_CANDIDATE_ADMIN_VERSION = T.let("2025-04", String)
27+
LATEST_SUPPORTED_ADMIN_VERSION = T.let("2025-04", String)
28+
RELEASE_CANDIDATE_ADMIN_VERSION = T.let("2025-07", String)
2829
end
2930

3031
SUPPORTED_ADMIN_VERSIONS = ShopifyAPI::AdminVersions::SUPPORTED_ADMIN_VERSIONS

lib/shopify_api/auth/jwt_payload.rb

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ class JwtPayload
1010
JWT_EXPIRATION_LEEWAY = JWT_LEEWAY
1111

1212
sig { returns(String) }
13-
attr_reader :iss, :dest, :aud, :sub, :jti, :sid
13+
attr_reader :iss, :dest, :aud, :jti
1414

1515
sig { returns(Integer) }
1616
attr_reader :exp, :nbf, :iat
1717

18+
sig { returns(T.nilable(String)) }
19+
attr_reader :sub, :sid
20+
1821
alias_method :expire_at, :exp
1922

2023
sig { params(token: String).void }
@@ -30,12 +33,12 @@ def initialize(token)
3033
@iss = T.let(payload_hash["iss"], String)
3134
@dest = T.let(payload_hash["dest"], String)
3235
@aud = T.let(payload_hash["aud"], String)
33-
@sub = T.let(payload_hash["sub"], String)
36+
@sub = T.let(payload_hash["sub"], T.nilable(String))
3437
@exp = T.let(payload_hash["exp"], Integer)
3538
@nbf = T.let(payload_hash["nbf"], Integer)
3639
@iat = T.let(payload_hash["iat"], Integer)
3740
@jti = T.let(payload_hash["jti"], String)
38-
@sid = T.let(payload_hash["sid"], String)
41+
@sid = T.let(payload_hash["sid"], T.nilable(String))
3942

4043
raise ShopifyAPI::Errors::InvalidJwtTokenError,
4144
"Session token had invalid API key" unless @aud == Context.api_key
@@ -47,19 +50,9 @@ def shop
4750
end
4851
alias_method :shopify_domain, :shop
4952

50-
sig { returns(Integer) }
53+
sig { returns(T.nilable(Integer)) }
5154
def shopify_user_id
52-
@sub.to_i
53-
end
54-
55-
# TODO: Remove before releasing v11
56-
sig { params(shop: String).returns(T::Boolean) }
57-
def validate_shop(shop)
58-
Context.logger.warn(
59-
"Deprecation notice: ShopifyAPI::Auth::JwtPayload.validate_shop no longer checks the given shop and always " \
60-
"returns true. It will be removed in v11.",
61-
)
62-
true
55+
@sub.to_i if user_id_sub? && admin_session_token?
6356
end
6457

6558
alias_method :eql?, :==
@@ -86,6 +79,16 @@ def decode_token(token, api_secret_key)
8679
rescue JWT::DecodeError => err
8780
raise ShopifyAPI::Errors::InvalidJwtTokenError, "Error decoding session token: #{err.message}"
8881
end
82+
83+
sig { returns(T::Boolean) }
84+
def admin_session_token?
85+
@iss.end_with?("/admin")
86+
end
87+
88+
sig { returns(T::Boolean) }
89+
def user_id_sub?
90+
@sub&.match?(/\A\d+\z/) || false
91+
end
8992
end
9093
end
9194
end

lib/shopify_api/auth/oauth.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ def begin_auth(shop:, redirect_path:, is_online: true, scope_override: nil)
4646
}
4747

4848
query_string = URI.encode_www_form(query)
49+
auth_route = auth_base_uri(shop) + "/oauth/authorize?#{query_string}"
4950

50-
auth_route = "https://#{shop}/admin/oauth/authorize?#{query_string}"
5151
{ auth_route: auth_route, cookie: cookie }
5252
end
5353

@@ -106,6 +106,20 @@ def validate_auth_callback(cookies:, auth_query:)
106106

107107
{ session: session, cookie: cookie }
108108
end
109+
110+
private
111+
112+
sig { params(shop: String).returns(String) }
113+
def auth_base_uri(shop)
114+
return "https://#{shop}/admin" unless defined?(DevServer)
115+
116+
# For first-party apps in development only, we leverage DevServer to build the admin base URI
117+
admin_web = T.unsafe(Object.const_get("DevServer")).new("web") # rubocop:disable Sorbet/ConstantsFromStrings
118+
admin_host = admin_web.host!(nonstandard_host_prefix: "admin")
119+
shop_name = shop.split(".").first
120+
121+
"https://#{admin_host}/store/#{shop_name}"
122+
end
109123
end
110124
end
111125
end

lib/shopify_api/clients/http_client.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ def request(request, response_as_struct: false)
4040
headers["Content-Type"] = T.must(request.body_type) if request.body_type
4141
headers = headers.merge(T.must(request.extra_headers)) if request.extra_headers
4242

43+
parsed_uri = URI(request_url(request))
44+
45+
headers = append_first_party_development_headers(headers, parsed_uri)
46+
4347
tries = 0
4448
response = HttpResponse.new(code: 0, headers: {}, body: "")
4549
while tries < request.tries
4650
tries += 1
4751
res = T.cast(HTTParty.send(
4852
request.http_method,
49-
request_url(request),
53+
parsed_uri.to_s,
5054
headers: headers,
5155
query: request.query,
5256
body: request.body.class == Hash ? T.unsafe(request.body).to_json : request.body,
@@ -115,6 +119,27 @@ def serialized_error(response)
115119
end
116120
body.to_json
117121
end
122+
123+
private
124+
125+
sig do
126+
params(
127+
headers: T::Hash[T.any(Symbol, String), T.untyped],
128+
parsed_uri: URI::Generic,
129+
).returns(T::Hash[T.any(Symbol, String), T.untyped])
130+
end
131+
def append_first_party_development_headers(headers, parsed_uri)
132+
return headers unless defined?(DevServer)
133+
return headers unless headers["Host"]&.include?(".my.shop.dev") || parsed_uri.host&.include?(".my.shop.dev")
134+
135+
# These headers are only used for first party applications in development mode
136+
headers["x-forwarded-host"] = headers["Host"] || parsed_uri.host
137+
headers["Host"] = T.unsafe(
138+
Object.const_get("DevServer::Core"), # rubocop:disable Sorbet/ConstantsFromStrings
139+
).new.host!(:app)
140+
141+
headers
142+
end
118143
end
119144
end
120145
end
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)