spree_api 5.6.0.rc1 → 5.6.0.rc2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89ddc07a0f4f9334f2ecb79bf2283986e8078e5566d4a7b1c672fadd2f04453d
4
- data.tar.gz: 1db32c6bf66382dc2786d1898aaaf5e6c3e1a04b721404fad423c37cc3e0326b
3
+ metadata.gz: 0b165c94c2975472728b228761cc6cb41f1a677a6368dc9a0be992b5840444cd
4
+ data.tar.gz: 1ee381dfe91a8ccc58e959cec117f1beb992dde08b6c04488e18ea45baf1ec3c
5
5
  SHA512:
6
- metadata.gz: 001b8a751f6cd60f71ee162e4387570f8976bd47a3c6794f6d850ba42fe157c3850a137646fed9f6c22651e9d86bf08bad579f2a6854370442cc9dbec0ae1c3f
7
- data.tar.gz: 6eb41a561383ce8ab469c21c1a341673164fadb3f5e738ccb7b0635bccd7ff56d6d280d30bcd3aa1bea852763b4f13908b96e45b071013736013c759f71f48fe
6
+ metadata.gz: 71038600173f2b56747b328f32fab9030ade806d07f3823e1e3e261098f85bdf24d54b0e8c7eb131e669a1f856bbb84c0d6cd3bba2974eef2739c87b73e27ddd
7
+ data.tar.gz: 8847edd08dbe74a437313d47add4fc557f5930dfdce57cca0b8812249e1f9ea40cc2416aef628e97a13a999777031bd6de381532a4093bceb6311f554854acb8
@@ -83,10 +83,15 @@ module Spree
83
83
  end
84
84
 
85
85
  # PATCH /api/v3/store/carts/:id/associate
86
- # Associates a guest cart with the currently authenticated user
87
- # Requires: JWT authentication + cart ID in URL
86
+ # Associates a guest cart with the currently authenticated user.
87
+ # Requires: JWT authentication + cart ID in URL + possession of the
88
+ # cart's token (x-spree-token). The token is the credential that
89
+ # authorizes claiming the cart, so it is required even when the cart
90
+ # is already the caller's own.
88
91
  def associate
89
92
  @cart = find_cart_for_association
93
+ authorize!(:update, @cart, cart_token)
94
+ require_cart_token!
90
95
 
91
96
  result = Spree.cart_associate_service.call(guest_order: @cart, user: current_user, guest_only: true)
92
97
 
@@ -177,6 +182,14 @@ module Spree
177
182
  def find_cart_for_association
178
183
  current_store.carts.where(user: [nil, current_user]).find_by_prefix_id!(params[:id])
179
184
  end
185
+
186
+ # Claiming a cart requires presenting its token, even when the caller
187
+ # already owns it — the token, not ownership, is the claim credential.
188
+ def require_cart_token!
189
+ valid = cart_token.present? && cart_token == @cart.token
190
+
191
+ raise CanCan::AccessDenied.new(nil, :update, @cart) unless valid
192
+ end
180
193
  end
181
194
  end
182
195
  end
@@ -7,7 +7,7 @@ module Spree
7
7
  rate_limit to: Spree::Api::Config[:rate_limit_register],
8
8
  within: Spree::Api::Config[:rate_limit_window].seconds,
9
9
  store: Rails.cache,
10
- only: [:create, :verify],
10
+ only: [:create, :verify, :request_unsubscribe],
11
11
  with: RATE_LIMIT_RESPONSE
12
12
 
13
13
  # POST /api/v3/store/newsletter_subscribers
@@ -53,6 +53,57 @@ module Spree
53
53
  render json: serialize_resource(subscriber)
54
54
  end
55
55
 
56
+ # DELETE /api/v3/store/newsletter_subscribers/:id
57
+ # Accepts either of:
58
+ # - Authorization: Bearer <jwt> — logged-in customer destroying their own subscription
59
+ # - ?token=<unsubscribe-token> — bearer from `subscriber.generate_token_for(:unsubscribe)`,
60
+ # typically clicked from an unsubscribe email
61
+ def destroy
62
+ subscriber = find_owned_subscriber || find_subscriber_by_unsubscribe_token
63
+
64
+ if subscriber.blank?
65
+ return render_error(
66
+ code: ERROR_CODES[:invalid_token],
67
+ message: Spree.t(:newsletter_unsubscribe_token_invalid, scope: :api),
68
+ status: :unprocessable_content
69
+ )
70
+ end
71
+
72
+ linked_user = subscriber.user
73
+ subscriber.destroy!
74
+
75
+ # Keep accepts_email_marketing in sync, but only when no subscriptions remain.
76
+ if linked_user&.accepts_email_marketing? && Spree::NewsletterSubscriber.where(user_id: linked_user.id).none?
77
+ linked_user.update(accepts_email_marketing: false)
78
+ end
79
+
80
+ head :no_content
81
+ end
82
+
83
+ # POST /api/v3/store/newsletter_subscribers/request_unsubscribe
84
+ # Publishes a `newsletter_subscriber.unsubscribe_requested` event carrying the unsubscribe token.
85
+ # Always returns 202 to prevent email enumeration.
86
+ def request_unsubscribe
87
+ subscriber = Spree::NewsletterSubscriber.for_store(current_store).find_by(email: params[:email])
88
+
89
+ if subscriber
90
+ payload = {
91
+ id: subscriber.prefixed_id,
92
+ email: subscriber.email,
93
+ unsubscribe_token: subscriber.generate_token_for(:unsubscribe),
94
+ store_id: current_store.prefixed_id,
95
+ customer_id: subscriber.user&.prefixed_id
96
+ }
97
+
98
+ redirect_url = validated_redirect_url
99
+ payload[:redirect_url] = redirect_url if redirect_url.present?
100
+
101
+ subscriber.publish_event('newsletter_subscriber.unsubscribe_requested', payload)
102
+ end
103
+
104
+ head :accepted
105
+ end
106
+
56
107
  protected
57
108
 
58
109
  def serializer_class
@@ -61,6 +112,22 @@ module Spree
61
112
 
62
113
  private
63
114
 
115
+ def find_owned_subscriber
116
+ return unless current_user
117
+
118
+ Spree::NewsletterSubscriber.
119
+ for_store(current_store).
120
+ accessible_by(current_ability, :destroy).
121
+ find_by_prefix_id(params[:id])
122
+ end
123
+
124
+ def find_subscriber_by_unsubscribe_token
125
+ return if params[:token].blank?
126
+
127
+ subscriber = Spree::NewsletterSubscriber.for_store(current_store).find_by_token_for(:unsubscribe, params[:token])
128
+ subscriber if subscriber&.prefixed_id == params[:id]
129
+ end
130
+
64
131
  # Drop redirect_url when it isn't in the store's allow-list — secure-by-default,
65
132
  # mirrors password_resets. Returning nil omits it from the webhook payload rather
66
133
  # than rejecting the request, so callers can't probe the allow-list via 4xx errors.
@@ -85,6 +85,16 @@ module Spree
85
85
  one :bill_address, key: :default_billing_address, resource: proc { Spree.api.admin_address_serializer }, if: proc { expand?('default_billing_address') }
86
86
  one :ship_address, key: :default_shipping_address, resource: proc { Spree.api.admin_address_serializer }, if: proc { expand?('default_shipping_address') }
87
87
 
88
+ # Override the inherited always-on store association to gate it behind
89
+ # expand? — the admin customers index serializes many rows and an
90
+ # always-on association would fire one query per row.
91
+ one :newsletter_subscriber,
92
+ resource: proc { Spree.api.admin_newsletter_subscriber_serializer },
93
+ if: proc { expand?('newsletter_subscriber') } do |user, params|
94
+ store = params&.dig(:store) || Spree::Current.store
95
+ user.newsletter_subscriber(store)
96
+ end
97
+
88
98
  many :orders,
89
99
  resource: proc { Spree.api.admin_order_serializer },
90
100
  if: proc { expand?('orders') }
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Api
5
+ module V3
6
+ module Admin
7
+ # Admin API Newsletter Subscriber Serializer
8
+ # Back-office surface, nested under the admin customer serializer. Extends the
9
+ # customer-facing serializer so public fields stay in sync.
10
+ class NewsletterSubscriberSerializer < V3::NewsletterSubscriberSerializer
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -8,7 +8,8 @@ module Spree
8
8
  full_name: :string,
9
9
  phone: [:string, nullable: true], accepts_email_marketing: :boolean,
10
10
  available_store_credit_total: :string, display_available_store_credit_total: :string,
11
- default_billing_address: { nullable: true }, default_shipping_address: { nullable: true }
11
+ default_billing_address: { nullable: true }, default_shipping_address: { nullable: true },
12
+ newsletter_subscriber: { nullable: true }
12
13
 
13
14
  attributes :email, :first_name, :last_name, :phone, :accepts_email_marketing
14
15
 
@@ -31,6 +32,11 @@ module Spree
31
32
  many :addresses, resource: proc { Spree.api.address_serializer }
32
33
  one :bill_address, key: :default_billing_address, resource: proc { Spree.api.address_serializer }
33
34
  one :ship_address, key: :default_shipping_address, resource: proc { Spree.api.address_serializer }
35
+
36
+ one :newsletter_subscriber, resource: proc { Spree.api.newsletter_subscriber_serializer } do |user, params|
37
+ store = params&.dig(:store) || Spree::Current.store
38
+ user.newsletter_subscriber(store)
39
+ end
34
40
  end
35
41
  end
36
42
  end
@@ -13,6 +13,7 @@ en:
13
13
  guest_checkout_not_allowed: You must be signed in to complete checkout.
14
14
  storefront_login_required: Authentication required to access this store.
15
15
  negative_quantity: quantity is negative
16
+ newsletter_unsubscribe_token_invalid: Newsletter unsubscribe token is invalid or has expired.
16
17
  newsletter_verification_token_invalid: Newsletter verification token is invalid or has expired.
17
18
  order:
18
19
  could_not_transition: The order could not be transitioned. Please fix the errors and try again.
data/config/routes.rb CHANGED
@@ -59,10 +59,15 @@ Spree::Core::Engine.add_routes do
59
59
  # Customers
60
60
  resources :customers, only: [:create]
61
61
 
62
- # Newsletter Subscriptions (guest-accessible: subscribe + verify by token)
63
- resources :newsletter_subscribers, only: [:create] do
62
+ # Newsletter Subscriptions
63
+ # - create + verify + request_unsubscribe are guest-accessible
64
+ # - destroy accepts either an unsubscribe token (from email links) or JWT auth
65
+ # A signed-in customer reads their own subscription (and its id) off the
66
+ # `newsletter_subscriber` association on GET /customers/me.
67
+ resources :newsletter_subscribers, only: [:create, :destroy] do
64
68
  collection do
65
69
  post :verify
70
+ post :request_unsubscribe
66
71
  end
67
72
  end
68
73
 
@@ -172,6 +172,7 @@ module Spree
172
172
  admin_state_serializer: 'Spree::Api::V3::Admin::StateSerializer',
173
173
  admin_discount_serializer: 'Spree::Api::V3::Admin::DiscountSerializer',
174
174
  admin_customer_serializer: 'Spree::Api::V3::Admin::CustomerSerializer',
175
+ admin_newsletter_subscriber_serializer: 'Spree::Api::V3::Admin::NewsletterSubscriberSerializer',
175
176
  admin_order_serializer: 'Spree::Api::V3::Admin::OrderSerializer',
176
177
  admin_product_serializer: 'Spree::Api::V3::Admin::ProductSerializer',
177
178
  admin_variant_serializer: 'Spree::Api::V3::Admin::VariantSerializer',
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0.rc1
4
+ version: 5.6.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-15 00:00:00.000000000 Z
11
+ date: 2026-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 5.6.0.rc1
75
+ version: 5.6.0.rc2
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 5.6.0.rc1
82
+ version: 5.6.0.rc2
83
83
  description: Spree's API
84
84
  email:
85
85
  - hello@spreecommerce.org
@@ -248,6 +248,7 @@ files:
248
248
  - app/serializers/spree/api/v3/admin/line_item_serializer.rb
249
249
  - app/serializers/spree/api/v3/admin/market_serializer.rb
250
250
  - app/serializers/spree/api/v3/admin/media_serializer.rb
251
+ - app/serializers/spree/api/v3/admin/newsletter_subscriber_serializer.rb
251
252
  - app/serializers/spree/api/v3/admin/option_type_serializer.rb
252
253
  - app/serializers/spree/api/v3/admin/option_value_serializer.rb
253
254
  - app/serializers/spree/api/v3/admin/order_serializer.rb
@@ -379,9 +380,9 @@ licenses:
379
380
  - BSD-3-Clause
380
381
  metadata:
381
382
  bug_tracker_uri: https://github.com/spree/spree/issues
382
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc1
383
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc2
383
384
  documentation_uri: https://docs.spreecommerce.org/
384
- source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc1
385
+ source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc2
385
386
  post_install_message:
386
387
  rdoc_options: []
387
388
  require_paths: