spree_core 5.4.0.beta7 → 5.4.0.beta8

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: 9a75c8667f68e7a71143adec2c78694c99308dbc73fa9afd625e1c5e339e2fb8
4
- data.tar.gz: '04229df0b73d148776d498157514ed592db0ffb551dc347a2aca782cafb80f01'
3
+ metadata.gz: d8297e501dfb71ba02dbd003c3f2a6fa035585a07cca4cf3f7e039eb92bb55c6
4
+ data.tar.gz: 87546d3659afc8573cf33cd9b3b2ceac6e351f7f8e30969626991c7c39da8773
5
5
  SHA512:
6
- metadata.gz: b237c3ae83b130df90180833997bd0301335689e620e2da1956f67881bb44031962895c5de052d807d886ae00682a35c3b1c6fea5541acdbde4347abd871875a
7
- data.tar.gz: d57fc57c99f2a3466e482965bc1a6bec9a40f91b277669ae2c24dfa93d1b68790f95bb3d50f15f2648abb32b1cf5c3f99cd646bfecf7b34c3d0949417550e25f
6
+ metadata.gz: 8983ae8234112c480dc54e302bd7d56557b11418af19a0ca6d65aa182ed06195c828ae69dc122209c23057149febb28b4c9e410a460e9dba2b09978240788997
7
+ data.tar.gz: e62cdc311fe8a2a256442b38e84df024e5c145ef47dc3c2cf5981e923bf5e3558c3b6937c3a64d101eb0783f5c493fdfbc3e75db004740d45599fa9d8ddfc80a
@@ -114,10 +114,8 @@ module Spree
114
114
 
115
115
  base_url = if options[:relative]
116
116
  ''
117
- elsif store.respond_to?(:formatted_custom_domain) && store.formatted_custom_domain.present?
118
- store.formatted_custom_domain
119
117
  else
120
- store.formatted_url
118
+ store.storefront_url
121
119
  end
122
120
 
123
121
  localize = if options[:locale].present?
@@ -151,15 +149,15 @@ module Spree
151
149
  # we should always try to render image of the default variant
152
150
  # same as it's done on PDP
153
151
  def default_image_for_product(product)
154
- Spree::Deprecation.warn('BaseHelper#default_image_for_product is deprecated and will be removed in Spree 5.5. Please use product.default_image instead')
152
+ Spree::Deprecation.warn('BaseHelper#default_image_for_product is deprecated and will be removed in Spree 6.0. Please use product.primary_media instead')
155
153
 
156
- product.default_image
154
+ product.primary_media
157
155
  end
158
156
 
159
157
  def default_image_for_product_or_variant(product_or_variant)
160
- Spree::Deprecation.warn('BaseHelper#default_image_for_product_or_variant is deprecated and will be removed in Spree 5.5. Please use product_or_variant.default_image instead')
158
+ Spree::Deprecation.warn('BaseHelper#default_image_for_product_or_variant is deprecated and will be removed in Spree 6.0. Please use product_or_variant.primary_media instead')
161
159
 
162
- product_or_variant.default_image
160
+ product_or_variant.primary_media
163
161
  end
164
162
 
165
163
  def base_cache_key
@@ -37,7 +37,7 @@ module Spree
37
37
  # this is only a fail-safe solution if developer didn't set this in environment files
38
38
  # http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views
39
39
  def ensure_default_action_mailer_url_host(store_url = nil)
40
- host_url = store_url.presence || current_store.try(:url_or_custom_domain)
40
+ host_url = store_url.presence || current_store.try(:storefront_url)
41
41
 
42
42
  return if host_url.blank?
43
43
 
@@ -18,6 +18,13 @@ module Spree
18
18
  # Enable lifecycle events for user models
19
19
  publishes_lifecycle_events
20
20
 
21
+ # Password reset token (Rails 7.1+ signed token, no DB column needed)
22
+ # Token auto-invalidates when password changes (salt changes)
23
+ # Expiration is configurable via Spree::Config.customer_password_reset_expires_in (in minutes)
24
+ generates_token_for :password_reset, expires_in: Spree::Config.customer_password_reset_expires_in.minutes do
25
+ password_salt&.last(10) || encrypted_password&.last(10)
26
+ end
27
+
21
28
  # we need to have this callback before any dependent: :destroy associations
22
29
  # https://github.com/rails/rails/issues/3458
23
30
  before_validation :clone_billing_address, if: :use_billing?
@@ -59,6 +66,14 @@ module Spree
59
66
  #
60
67
  attr_accessor :confirm_email, :terms_of_service
61
68
 
69
+ def self.find_by_password_reset_token(token)
70
+ find_by_token_for(:password_reset, token)
71
+ end
72
+
73
+ def self.find_by_password_reset_token!(token)
74
+ find_by_token_for!(:password_reset, token)
75
+ end
76
+
62
77
  def self.multi_search(query)
63
78
  sanitized_query = sanitize_query_for_multi_search(query)
64
79
  return none if query.blank?
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ class AllowedOrigin < Spree.base_class
5
+ has_prefix_id :ao
6
+
7
+ include Spree::SingleStoreResource
8
+
9
+ belongs_to :store, class_name: 'Spree::Store'
10
+
11
+ validates :store, :origin, presence: true
12
+ validates :origin, uniqueness: { scope: [:store_id, *spree_base_uniqueness_scope] }
13
+ validate :origin_must_be_valid_http_url
14
+
15
+ private
16
+
17
+ def origin_must_be_valid_http_url
18
+ return if origin.blank?
19
+
20
+ uri = URI.parse(origin)
21
+
22
+ unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
23
+ errors.add(:origin, :invalid)
24
+ return
25
+ end
26
+
27
+ if uri.host.blank?
28
+ errors.add(:origin, :invalid)
29
+ return
30
+ end
31
+
32
+ # Origins must not have a path, query, or fragment
33
+ if uri.path.present? && uri.path != '/'
34
+ errors.add(:origin, :must_be_origin_only)
35
+ end
36
+
37
+ if uri.query.present? || uri.fragment.present?
38
+ errors.add(:origin, :must_be_origin_only)
39
+ end
40
+ rescue URI::InvalidURIError
41
+ errors.add(:origin, :invalid)
42
+ end
43
+ end
44
+ end
@@ -1,20 +1,42 @@
1
1
  module Spree
2
2
  class Asset < Spree.base_class
3
- has_prefix_id :asset # Stripe: file_
3
+ has_prefix_id :media
4
4
 
5
5
  include Support::ActiveStorage
6
+ include Rails.application.routes.url_helpers
7
+ include Spree::ImageMethods # legacy, will be removed in Spree 6
6
8
  include Spree::Metafields
7
9
  include Spree::Metadata
8
10
 
11
+ # Legacy styles support (was in Spree::Image::Configuration::ActiveStorage)
12
+ # @deprecated Will be removed in Spree 6
13
+ def self.styles
14
+ @styles ||= Spree::Config.product_image_variant_sizes.transform_values do |dimensions|
15
+ "#{dimensions[0]}x#{dimensions[1]}>"
16
+ end
17
+ end
18
+
19
+ def default_style
20
+ :small
21
+ end
22
+
9
23
  publishes_lifecycle_events
10
24
 
11
25
  EXTERNAL_URL_METAFIELD_KEY = 'external.url'
26
+ MEDIA_TYPES = %w[image video external_video].freeze
27
+
28
+ after_initialize { self.media_type ||= 'image' }
12
29
 
13
30
  belongs_to :viewable, polymorphic: true, touch: true
14
31
  acts_as_list scope: [:viewable_id, :viewable_type]
15
32
 
16
33
  delegate :key, :attached?, :variant, :variable?, :blob, :filename, :variation, to: :attachment
17
34
 
35
+ validates :media_type, inclusion: { in: MEDIA_TYPES }
36
+ validates :attachment, attached: true, content_type: Rails.application.config.active_storage.web_image_content_types,
37
+ if: -> { media_type == 'image' }
38
+ validates :external_video_url, presence: true, if: -> { media_type.in?(%w[video external_video]) }
39
+
18
40
  WEBP_SAVER_OPTIONS = {
19
41
  strip: true,
20
42
  quality: 75,
@@ -43,14 +65,46 @@ module Spree
43
65
 
44
66
  default_scope { includes(attachment_attachment: :blob) }
45
67
 
68
+ # STI was disabled in Spree::Image, keep it disabled here
69
+ self.inheritance_column = nil
70
+
46
71
  store_accessor :private_metadata, :session_uploaded_assets_uuid
47
72
  scope :with_session_uploaded_assets_uuid, lambda { |uuid|
48
73
  where(session_id: uuid)
49
74
  }
50
75
  scope :with_external_url, ->(url) { url.present? ? with_metafield_key_value(EXTERNAL_URL_METAFIELD_KEY, url.strip) : none }
51
76
 
77
+ # Callbacks merged from Spree::Image
78
+ after_commit :touch_product_variants, if: :should_touch_product_variants?, on: :update
79
+ after_commit :update_viewable_thumbnail_on_create, on: :create
80
+ after_commit :update_viewable_thumbnail_on_destroy, on: :destroy
81
+ after_commit :update_viewable_thumbnail_on_reorder, on: :update, if: :saved_change_to_position?
82
+ after_commit :update_viewable_thumbnail_on_viewable_change, on: :update, if: :saved_change_to_viewable_id?
83
+
84
+ after_create :increment_viewable_media_count
85
+ after_destroy :decrement_viewable_media_count
86
+
52
87
  def product
53
- @product ||= viewable_type == 'Spree::Variant' ? viewable&.product : nil
88
+ @product ||= case viewable_type
89
+ when 'Spree::Variant' then viewable&.product
90
+ when 'Spree::Product' then viewable
91
+ end
92
+ end
93
+
94
+ def focal_point
95
+ return nil if focal_point_x.nil? || focal_point_y.nil?
96
+
97
+ { x: focal_point_x, y: focal_point_y }
98
+ end
99
+
100
+ def focal_point=(point)
101
+ if point.nil?
102
+ self.focal_point_x = nil
103
+ self.focal_point_y = nil
104
+ else
105
+ self.focal_point_x = point[:x]
106
+ self.focal_point_y = point[:y]
107
+ end
54
108
  end
55
109
 
56
110
  def external_url
@@ -66,7 +120,71 @@ module Spree
66
120
  end
67
121
 
68
122
  def event_prefix
69
- 'asset'
123
+ 'media'
124
+ end
125
+
126
+ # @deprecated
127
+ def styles
128
+ Spree::Deprecation.warn("Asset#styles is deprecated and will be removed in Spree 6.0. Please use active storage variants with cdn_image_url")
129
+
130
+ self.class.styles.map do |_, size|
131
+ width, height = size.chop.split('x').map(&:to_i)
132
+
133
+ {
134
+ url: generate_url(size: size),
135
+ size: size,
136
+ width: width,
137
+ height: height
138
+ }
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def touch_product_variants
145
+ viewable.product.variants.touch_all
146
+ end
147
+
148
+ def should_touch_product_variants?
149
+ viewable.is_a?(Spree::Variant) &&
150
+ viewable.is_master? &&
151
+ viewable.product.has_variants? &&
152
+ saved_change_to_position?
153
+ end
154
+
155
+ def increment_viewable_media_count
156
+ case viewable_type
157
+ when 'Spree::Variant'
158
+ Spree::Variant.increment_counter(:media_count, viewable_id)
159
+ Spree::Product.increment_counter(:media_count, viewable.product_id)
160
+ when 'Spree::Product'
161
+ Spree::Product.increment_counter(:media_count, viewable_id)
162
+ end
163
+ end
164
+
165
+ def decrement_viewable_media_count
166
+ case viewable_type
167
+ when 'Spree::Variant'
168
+ Spree::Variant.decrement_counter(:media_count, viewable_id)
169
+ Spree::Product.decrement_counter(:media_count, viewable.product_id)
170
+ when 'Spree::Product'
171
+ Spree::Product.decrement_counter(:media_count, viewable_id)
172
+ end
70
173
  end
174
+
175
+ def update_viewable_thumbnail
176
+ case viewable_type
177
+ when 'Spree::Variant'
178
+ viewable.update_thumbnail!
179
+ viewable.product.update_thumbnail!
180
+ when 'Spree::Product'
181
+ viewable.update_thumbnail!
182
+ end
183
+ end
184
+
185
+ alias update_viewable_thumbnail_on_create update_viewable_thumbnail
186
+ alias update_viewable_thumbnail_on_destroy update_viewable_thumbnail
187
+ alias update_viewable_thumbnail_on_reorder update_viewable_thumbnail
188
+ alias update_viewable_thumbnail_on_viewable_change update_viewable_thumbnail
71
189
  end
72
190
  end
@@ -1,22 +1,10 @@
1
+ # @deprecated This module is now a no-op. All logic has been moved to Spree::Asset.
2
+ # Will be removed in Spree 6.0.
1
3
  module Spree
2
4
  class Image < Asset
3
5
  module Configuration
4
6
  module ActiveStorage
5
7
  extend ActiveSupport::Concern
6
-
7
- included do
8
- # Returns image styles derived from Spree::Config.product_image_variant_sizes
9
- # Format: { variant_name: 'WxH>' } for API compatibility
10
- def self.styles
11
- @styles ||= Spree::Config.product_image_variant_sizes.transform_values do |dimensions|
12
- "#{dimensions[0]}x#{dimensions[1]}>"
13
- end
14
- end
15
-
16
- def default_style
17
- :small
18
- end
19
- end
20
8
  end
21
9
  end
22
10
  end
@@ -1,82 +1,6 @@
1
+ # Backward compatibility — all logic now lives in Spree::Asset.
2
+ # This class will be removed in Spree 6.0.
1
3
  module Spree
2
4
  class Image < Asset
3
- include Spree::Image::Configuration::ActiveStorage # legacy to be removed in Spree 6
4
- include Rails.application.routes.url_helpers
5
- include Spree::ImageMethods # legacy, will be removed in Spree 6
6
-
7
- validates :attachment, attached: true, content_type: Rails.application.config.active_storage.web_image_content_types
8
-
9
- after_commit :touch_product_variants, if: :should_touch_product_variants?, on: :update
10
- after_commit :update_variant_thumbnail, on: [:create, :destroy]
11
- after_commit :update_variant_thumbnail_on_reorder, on: :update, if: :saved_change_to_position?
12
- after_commit :update_variant_thumbnail_on_viewable_change, on: :update, if: :saved_change_to_viewable_id?
13
-
14
- after_create :increment_viewable_image_count
15
- after_destroy :decrement_viewable_image_count
16
-
17
- # In Rails 5.x class constants are being undefined/redefined during the code reloading process
18
- # in a rails development environment, after which the actual ruby objects stored in those class constants
19
- # are no longer equal (subclass == self) what causes error ActiveRecord::SubclassNotFound
20
- # Invalid single-table inheritance type: Spree::Image is not a subclass of Spree::Image.
21
- # The line below prevents the error.
22
- self.inheritance_column = nil
23
-
24
- # @deprecated
25
- def styles
26
- Spree::Deprecation.warn("Image#styles is deprecated and will be removed in Spree 6.0. Please use active storage variants with cdn_image_url")
27
-
28
- self.class.styles.map do |_, size|
29
- width, height = size.chop.split('x').map(&:to_i)
30
-
31
- {
32
- url: generate_url(size: size),
33
- size: size,
34
- width: width,
35
- height: height
36
- }
37
- end
38
- end
39
-
40
- private
41
-
42
- def touch_product_variants
43
- viewable.product.variants.touch_all
44
- end
45
-
46
- def should_touch_product_variants?
47
- viewable.is_a?(Spree::Variant) &&
48
- viewable.is_master? &&
49
- viewable.product.has_variants? &&
50
- saved_change_to_position?
51
- end
52
-
53
- def increment_viewable_image_count
54
- return unless viewable.is_a?(Spree::Variant)
55
-
56
- Spree::Variant.increment_counter(:image_count, viewable_id)
57
- Spree::Product.increment_counter(:total_image_count, viewable.product_id)
58
- end
59
-
60
- def decrement_viewable_image_count
61
- return unless viewable.is_a?(Spree::Variant)
62
-
63
- Spree::Variant.decrement_counter(:image_count, viewable_id)
64
- Spree::Product.decrement_counter(:total_image_count, viewable.product_id)
65
- end
66
-
67
- def update_variant_thumbnail
68
- return unless viewable.is_a?(Spree::Variant)
69
-
70
- viewable.update_thumbnail!
71
- viewable.product.update_thumbnail!
72
- end
73
-
74
- def update_variant_thumbnail_on_reorder
75
- update_variant_thumbnail
76
- end
77
-
78
- def update_variant_thumbnail_on_viewable_change
79
- update_variant_thumbnail
80
- end
81
5
  end
82
6
  end
@@ -12,6 +12,7 @@ module Spree
12
12
  attr_accessor :password, :password_confirmation
13
13
 
14
14
  validates :email, presence: true, uniqueness: { case_sensitive: false }
15
+ validates :password, confirmation: true, if: :password
15
16
 
16
17
  before_save :encrypt_password, if: :password
17
18
 
@@ -55,10 +55,10 @@ module Spree
55
55
  delegate :name, :description, :brand, :category, to: :product
56
56
 
57
57
  # Returns the thumbnail image for this line item
58
- # Prefers variant thumbnail, falls back to product thumbnail
59
- # @return [Spree::Image, nil]
58
+ # Prefers variant primary media, falls back to product primary media
59
+ # @return [Spree::Asset, nil]
60
60
  def thumbnail
61
- variant.thumbnail || product.thumbnail
61
+ variant.primary_media || product.primary_media
62
62
  end
63
63
  delegate :tax_zone, to: :order
64
64
  delegate :digital?, :can_supply?, to: :variant
@@ -114,10 +114,12 @@ module Spree
114
114
  has_many :orders, through: :line_items
115
115
  has_many :completed_orders, -> { reorder(nil).distinct.complete }, through: :line_items, source: :order
116
116
 
117
+ has_many :media, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'Spree::Asset'
118
+
117
119
  has_many :variant_images, -> { order(:position) }, source: :images, through: :variants_including_master
118
120
  has_many :variant_images_without_master, -> { order(:position) }, source: :images, through: :variants
119
121
 
120
- belongs_to :thumbnail, class_name: 'Spree::Image', optional: true
122
+ belongs_to :primary_media, class_name: 'Spree::Asset', optional: true, foreign_key: :primary_media_id
121
123
 
122
124
  has_many :option_value_variants, class_name: 'Spree::OptionValueVariant', through: :variants
123
125
  has_many :option_values, class_name: 'Spree::OptionValue', through: :variants
@@ -335,17 +337,26 @@ module Spree
335
337
  @default_variant_id ||= default_variant.id
336
338
  end
337
339
 
338
- # Returns true if any variant (including master) has images.
339
- # Uses loaded association when available, otherwise falls back to counter cache.
340
+ # Returns the product's media gallery.
341
+ # Uses product-level media if present, otherwise falls back to variant images.
342
+ # @return [ActiveRecord::Relation]
343
+ def gallery_media
344
+ return media if association(:media).loaded? ? media.any? : media.exists?
345
+
346
+ variant_images
347
+ end
348
+
349
+ # Returns true if the product has any media (product-level or variant-level).
350
+ # Uses counter cache for performance.
340
351
  # @return [Boolean]
341
- def has_variant_images?
352
+ def has_media?
342
353
  return variant_images.any? if association(:variant_images).loaded?
343
354
 
344
- total_image_count.positive?
355
+ media_count.positive?
345
356
  end
346
357
 
347
- # Alias for has_variant_images? for consistency with Variant#has_images?
348
- alias has_images? has_variant_images?
358
+ alias has_images? has_media?
359
+ alias has_variant_images? has_media?
349
360
 
350
361
  # Returns the variant that should be used for displaying images.
351
362
  # Priority: master > default_variant > first variant with images
@@ -354,47 +365,47 @@ module Spree
354
365
  @variant_for_images ||= find_variant_for_images
355
366
  end
356
367
 
357
- # Returns default Image for Product.
358
- # Uses cached thumbnail_id which is updated when images are added/removed/reordered.
359
- # @return [Spree::Image, nil]
368
+ # @deprecated Use #primary_media instead.
360
369
  def default_image
361
- thumbnail
370
+ Spree::Deprecation.warn('Spree::Product#default_image is deprecated and will be removed in Spree 6.0. Please use Spree::Product#primary_media instead.')
371
+ primary_media
362
372
  end
363
373
 
364
- # Backward compatibility for Spree 5.2 and earlier.
365
- # @deprecated Use Spree::Product#default_image instead.
374
+ # @deprecated Use #primary_media instead.
366
375
  def featured_image
367
- Spree::Deprecation.warn('Spree::Product#featured_image is deprecated and will be removed in Spree 5.5. Please use Spree::Product#default_image instead.')
376
+ Spree::Deprecation.warn('Spree::Product#featured_image is deprecated and will be removed in Spree 6.0. Please use Spree::Product#primary_media instead.')
377
+ primary_media
378
+ end
368
379
 
369
- default_image
380
+ # @deprecated Use #primary_media instead.
381
+ def primary_image
382
+ Spree::Deprecation.warn('Spree::Product#primary_image is deprecated and will be removed in Spree 6.0. Please use Spree::Product#primary_media instead.')
383
+ primary_media
370
384
  end
371
385
 
372
- # Returns secondary Image for Product (for hover effects).
373
- # @return [Spree::Image, nil]
386
+ # Returns secondary media for Product (for hover effects).
387
+ # @return [Spree::Asset, nil]
374
388
  def secondary_image
375
389
  variant_for_images&.secondary_image
376
390
  end
377
391
 
378
- # Alias for default_image for consistency.
379
- alias primary_image default_image
380
-
381
- # Returns the image count from the variant used for displaying images.
382
- # @return [Integer]
392
+ # @deprecated Use media_count instead
383
393
  def image_count
384
- variant_for_images&.image_count || 0
394
+ media_count
385
395
  end
386
396
 
387
- # Updates the thumbnail_id to the first image from variant_images.
388
- # Called when images are added, removed, or reordered on any variant.
397
+ # Updates primary_media_id to the first media item.
398
+ # Checks product-level media first, then falls back to variant images.
399
+ # Called when media is added, removed, or reordered.
389
400
  def update_thumbnail!
390
- first_image = variant_images.order(:position).first
391
- update_column(:thumbnail_id, first_image&.id)
401
+ first_media = media.order(:position).first || variant_images.order(:position).first
402
+ update_column(:primary_media_id, first_media&.id)
392
403
  end
393
404
 
394
- # Finds first variant with images using preloaded data when available.
405
+ # Finds first variant with media using preloaded data when available.
395
406
  # @return [Spree::Variant, nil]
396
407
  def find_variant_with_images
397
- return variants.find(&:has_images?) if variants.loaded?
408
+ return variants.find(&:has_media?) if variants.loaded?
398
409
 
399
410
  variants.joins(:images).first
400
411
  end
@@ -630,12 +641,12 @@ module Spree
630
641
 
631
642
  private
632
643
 
633
- # Determines which variant should be used for displaying images.
634
- # Priority: master > default_variant > first variant with images
644
+ # Determines which variant should be used for displaying media.
645
+ # Priority: master > default_variant > first variant with media
635
646
  def find_variant_for_images
636
- return master if master.has_images?
637
- return default_variant if has_variants? && default_variant.has_images?
638
- return find_variant_with_images if has_variant_images?
647
+ return master if master.has_media?
648
+ return default_variant if has_variants? && default_variant.has_media?
649
+ return find_variant_with_images if has_media?
639
650
 
640
651
  nil
641
652
  end
@@ -109,6 +109,7 @@ module Spree
109
109
  has_many :customer_groups, class_name: 'Spree::CustomerGroup', dependent: :destroy, inverse_of: :store
110
110
 
111
111
  has_many :api_keys, class_name: 'Spree::ApiKey', dependent: :destroy
112
+ has_many :allowed_origins, class_name: 'Spree::AllowedOrigin', dependent: :destroy
112
113
 
113
114
  #
114
115
  # Validations
@@ -252,6 +253,36 @@ module Spree
252
253
  formatted_url
253
254
  end
254
255
 
256
+ # Returns the storefront origin URL for use in customer-facing emails and links.
257
+ # Uses the first allowed origin if configured, otherwise falls back to formatted_url.
258
+ #
259
+ # @return [String] e.g. "https://myshop.com"
260
+ def storefront_url
261
+ allowed_origins.order(:created_at).pick(:origin) || formatted_url
262
+ end
263
+
264
+ # Returns true if the given URL's origin matches one of the store's allowed origins.
265
+ # Comparison is port-less: only scheme + host are matched, so storing
266
+ # `http://localhost` will match `http://localhost:3000`, `http://localhost:4000`, etc.
267
+ #
268
+ # @param url [String] the full URL to check
269
+ # @return [Boolean]
270
+ def allowed_origin?(url)
271
+ return false if url.blank?
272
+
273
+ uri = URI.parse(url)
274
+ request_origin = "#{uri.scheme}://#{uri.host}"
275
+
276
+ allowed_origins.pluck(:origin).any? do |stored|
277
+ stored_uri = URI.parse(stored)
278
+ "#{stored_uri.scheme}://#{stored_uri.host}" == request_origin
279
+ rescue URI::InvalidURIError
280
+ false
281
+ end
282
+ rescue URI::InvalidURIError
283
+ false
284
+ end
285
+
255
286
  # Returns the states available for checkout for the store
256
287
  # @param country [Spree::Country] the country to get the states for
257
288
  # @return [Array<Spree::State>]
@@ -48,8 +48,8 @@ module Spree
48
48
  has_many :option_value_variants, class_name: 'Spree::OptionValueVariant'
49
49
  has_many :option_values, through: :option_value_variants, dependent: :destroy, class_name: 'Spree::OptionValue'
50
50
 
51
- has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'Spree::Image'
52
- belongs_to :thumbnail, class_name: 'Spree::Image', optional: true
51
+ has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'Spree::Asset'
52
+ belongs_to :primary_media, class_name: 'Spree::Asset', optional: true, foreign_key: :primary_media_id
53
53
 
54
54
  has_many :prices,
55
55
  class_name: 'Spree::Price',
@@ -279,26 +279,35 @@ module Spree
279
279
  is_master? ? name + ' - Master' : name + ' - ' + options_text
280
280
  end
281
281
 
282
- # Returns true if the variant has images.
282
+ # Returns the variant's media gallery.
283
+ # Currently returns direct images. In 6.0 will use variant_media join table.
284
+ # @return [ActiveRecord::Relation]
285
+ def gallery_media
286
+ images
287
+ end
288
+
289
+ # Returns true if the variant has media.
283
290
  # Uses loaded association when available, otherwise falls back to counter cache.
284
291
  # @return [Boolean]
285
- def has_images?
292
+ def has_media?
286
293
  return images.any? if images.loaded?
287
294
 
288
- image_count.positive?
295
+ media_count.positive?
289
296
  end
290
297
 
291
- # Returns default Image for Variant.
292
- # @return [Spree::Image, nil]
298
+ alias has_images? has_media?
299
+
300
+ # @deprecated Use #primary_media instead.
293
301
  def default_image
294
- thumbnail
302
+ Spree::Deprecation.warn('Spree::Variant#default_image is deprecated and will be removed in Spree 6.0. Please use Spree::Variant#primary_media instead.')
303
+ primary_media
295
304
  end
296
305
 
297
- # Updates the thumbnail_id to the first image by position.
298
- # Called when images are added, removed, or reordered.
306
+ # Updates primary_media_id to the first media item by position.
307
+ # Called when media is added, removed, or reordered.
299
308
  def update_thumbnail!
300
- first_image = images.order(:position).first
301
- update_column(:thumbnail_id, first_image&.id)
309
+ first_media = images.order(:position).first
310
+ update_column(:primary_media_id, first_media&.id)
302
311
  end
303
312
 
304
313
  # Returns first Image for Variant.
@@ -28,6 +28,7 @@ module Spree
28
28
  # add store resources
29
29
  PaymentMethods.call
30
30
  ApiKeys.call
31
+ AllowedOrigins.call
31
32
  end
32
33
  end
33
34
  end
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ module Seeds
3
+ class AllowedOrigins
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call
7
+ store = Spree::Store.default
8
+ return unless store&.persisted?
9
+
10
+ store.allowed_origins.find_or_create_by!(origin: 'http://localhost')
11
+ end
12
+ end
13
+ end
14
+ end
@@ -2,7 +2,7 @@
2
2
  <% height ||= 64 %>
3
3
  <% logo_css ||= '' %>
4
4
 
5
- <%= link_to current_store.url_or_custom_domain, id: 'site-logo', style: "text-decoration: none;" do %>
5
+ <%= link_to current_store.storefront_url, id: 'site-logo', style: "text-decoration: none;" do %>
6
6
  <span style="display: none;"><%= current_store.name %></span>
7
7
  <% if logo.present? && logo.attached? && logo.variable? %>
8
8
  <% aspect_ratio = spree_asset_aspect_ratio(logo) %>
@@ -240,6 +240,10 @@ en:
240
240
  messages:
241
241
  blank: can't be blank
242
242
  models:
243
+ spree/allowed_origin:
244
+ attributes:
245
+ origin:
246
+ must_be_origin_only: must be an origin (scheme and host) without path, query, or fragment
243
247
  spree/calculator/tiered_flat_rate:
244
248
  attributes:
245
249
  base:
@@ -742,6 +746,8 @@ en:
742
746
  all_products: All products
743
747
  all_rights_reserved: All rights reserved
744
748
  all_time: All time
749
+ allowed_origin: Allowed Origin
750
+ allowed_origins: Allowed Origins
745
751
  already_have_account: Already have an account?
746
752
  alt_text: Alternative Text
747
753
  alternative_phone: Alternative Phone
@@ -976,6 +982,14 @@ en:
976
982
  customer_group_rule:
977
983
  choose_customer_groups: 'Select the customer group(s) to which this promotion should apply:'
978
984
  customer_groups: Customer Groups
985
+ customer_mailer:
986
+ password_reset_email:
987
+ action: Reset Password
988
+ expiry_notice: This password reset link will expire shortly. If you did not request this, no action is needed.
989
+ greeting: Hi %{name},
990
+ ignore_notice: If you did not request a password reset, you can safely ignore this email. Your password will not be changed.
991
+ instructions: We received a request to reset your password. Click the button below to choose a new password.
992
+ subject: Password Reset
979
993
  customer_removed_from_group: Customer removed from group
980
994
  customer_return: Customer Return
981
995
  customer_returns: Customer Returns
@@ -1104,6 +1118,7 @@ en:
1104
1118
  blank: can't be blank
1105
1119
  cannot_remove_icon: Cannot remove image
1106
1120
  could_not_create_taxon: Could not create taxon
1121
+ must_be_origin_only: must be an origin (scheme and host) without path, query, or fragment
1107
1122
  no_shipping_methods_available: No shipping methods available for selected location, please change your address and try again.
1108
1123
  store_association_can_not_be_changed: The store association can not be changed
1109
1124
  store_is_already_set: Store is already set
@@ -1418,6 +1433,7 @@ en:
1418
1433
  new: New
1419
1434
  new_address: New address
1420
1435
  new_adjustment: New Adjustment
1436
+ new_allowed_origin: New Allowed Origin
1421
1437
  new_api_key: New API Key
1422
1438
  new_balance: New balance
1423
1439
  new_billing_address: New Billing Address
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpreeAllowedOrigins < ActiveRecord::Migration[7.2]
4
+ def change
5
+ create_table :spree_allowed_origins do |t|
6
+ t.references :store, null: false
7
+ t.string :origin, null: false
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :spree_allowed_origins, [:store_id, :origin], unique: true,
12
+ name: 'index_spree_allowed_origins_on_store_id_and_origin'
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ class AddProductMediaSupport < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :spree_assets, :media_type, :string
4
+ add_column :spree_assets, :focal_point_x, :decimal, precision: 5, scale: 4
5
+ add_column :spree_assets, :focal_point_y, :decimal, precision: 5, scale: 4
6
+ add_column :spree_assets, :external_video_url, :string
7
+
8
+ add_index :spree_assets, :media_type
9
+
10
+ rename_column :spree_variants, :image_count, :media_count
11
+ rename_column :spree_products, :total_image_count, :media_count
12
+ rename_column :spree_variants, :thumbnail_id, :primary_media_id
13
+ rename_column :spree_products, :thumbnail_id, :primary_media_id
14
+
15
+ reversible do |dir|
16
+ dir.up do
17
+ Spree::Asset.unscoped.where(media_type: nil).update_all(media_type: 'image')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -107,6 +107,9 @@ module Spree
107
107
  preference :coupon_codes_web_limit, :integer, default: 500 # number of coupon codes to be generated in the web process, more than this will be generated in a background job
108
108
  preference :coupon_codes_total_limit, :integer, default: 5000 # the maximum number of coupon codes to be generated
109
109
 
110
+ # password reset
111
+ preference :customer_password_reset_expires_in, :integer, default: 15 # password reset token expiration time in minutes
112
+
110
113
  # gift cards
111
114
  preference :gift_card_batch_web_limit, :integer, default: 500 # number of gift card codes to be generated in the web process, more than this will be generated in a background job
112
115
  preference :gift_card_batch_limit, :integer, default: 50_000
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '5.4.0.beta7'.freeze
2
+ VERSION = '5.4.0.beta8'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
@@ -2,6 +2,7 @@ module Spree
2
2
  module PermittedAttributes
3
3
  ATTRIBUTES = [
4
4
  :address_attributes,
5
+ :allowed_origin_attributes,
5
6
  :api_key_attributes,
6
7
  :asset_attributes,
7
8
  :checkout_attributes,
@@ -87,9 +88,12 @@ module Spree
87
88
  state: [:name, :abbr] }
88
89
  ]
89
90
 
91
+ @@allowed_origin_attributes = [:origin]
92
+
90
93
  @@api_key_attributes = [:name, :key_type]
91
94
 
92
- @@asset_attributes = [:type, :viewable_id, :viewable_type, :attachment, :alt, :position]
95
+ @@asset_attributes = [:type, :viewable_id, :viewable_type, :attachment, :alt, :position,
96
+ :media_type, :focal_point_x, :focal_point_y, :external_video_url]
93
97
 
94
98
  @@checkout_attributes = [
95
99
  :coupon_code, :email, :shipping_method_id, :special_instructions, :use_billing, :use_shipping,
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :allowed_origin, class: Spree::AllowedOrigin do
5
+ store
6
+ sequence(:origin) { |n| "https://storefront#{n}.example.com" }
7
+ end
8
+ end
@@ -1,15 +1,12 @@
1
1
  FactoryBot.define do
2
2
  factory :asset, class: Spree::Asset do
3
- viewable_type {}
4
- viewable_id {}
5
- attachment_width { 340 }
6
- attachment_height { 280 }
7
- attachment_file_size { 128 }
8
3
  position { 1 }
9
- attachment_content_type { '.jpg' }
10
- attachment_file_name { 'attachment.jpg' }
11
- type {}
12
- attachment_updated_at {}
13
4
  alt {}
5
+
6
+ after(:build) do |asset|
7
+ if asset.media_type == 'image' && !asset.attachment.attached?
8
+ asset.attachment.attach(io: File.new(Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg'), filename: 'thinking-cat.jpg')
9
+ end
10
+ end
14
11
  end
15
12
  end
@@ -1,5 +1,7 @@
1
1
  FactoryBot.define do
2
- factory :image, class: Spree::Image do
2
+ factory :image, class: Spree::Asset do
3
+ media_type { 'image' }
4
+
3
5
  before(:create) do |image|
4
6
  if image.class.method_defined?(:attachment)
5
7
  image.attachment.attach(io: File.new(Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg'), filename: 'thinking-cat.jpg')
@@ -1,17 +1,17 @@
1
1
  namespace :spree do
2
- namespace :images do
3
- desc 'Backfill thumbnail_id for all variants and products'
4
- task backfill_thumbnails: :environment do
5
- puts 'Backfilling variant thumbnails...'
6
- Spree::Variant.where(thumbnail_id: nil).where.not(image_count: 0).find_each do |variant|
7
- first_image = variant.images.order(:position).first
8
- variant.update_column(:thumbnail_id, first_image.id) if first_image
2
+ namespace :media do
3
+ desc 'Backfill primary_media_id for all variants and products'
4
+ task backfill_primary_media: :environment do
5
+ puts 'Backfilling variant primary_media...'
6
+ Spree::Variant.where(primary_media_id: nil).where.not(media_count: 0).find_each do |variant|
7
+ first_media = variant.gallery_media.first
8
+ variant.update_column(:primary_media_id, first_media.id) if first_media
9
9
  end
10
10
 
11
- puts 'Backfilling product thumbnails...'
12
- Spree::Product.where(thumbnail_id: nil).where.not(total_image_count: 0).find_each do |product|
13
- first_image = product.variant_images.order(:position).first
14
- product.update_column(:thumbnail_id, first_image.id) if first_image
11
+ puts 'Backfilling product primary_media...'
12
+ Spree::Product.where(primary_media_id: nil).where.not(media_count: 0).find_each do |product|
13
+ first_media = product.gallery_media.first
14
+ product.update_column(:primary_media_id, first_media.id) if first_media
15
15
  end
16
16
 
17
17
  puts 'Done!'
@@ -1,14 +1,16 @@
1
1
  namespace :spree do
2
2
  namespace :products do
3
- desc 'Reset counter caches (variant_count, classification_count, total_image_count) on products'
3
+ desc 'Reset counter caches (variant_count, classification_count, media_count) on products'
4
4
  task reset_counter_caches: :environment do |_t, _args|
5
5
  puts 'Resetting product counter caches...'
6
6
 
7
7
  Spree::Product.find_each do |product|
8
+ total_media = product.media.count + product.variant_images.where.not(id: product.media.select(:id)).count
9
+
8
10
  product.update_columns(
9
11
  variant_count: product.variants.count,
10
12
  classification_count: product.classifications.count,
11
- total_image_count: product.variant_images.count,
13
+ media_count: total_media,
12
14
  updated_at: Time.current
13
15
  )
14
16
  print '.'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.0.beta7
4
+ version: 5.4.0.beta8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Schofield
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-03-15 00:00:00.000000000 Z
13
+ date: 2026-03-16 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: i18n-tasks
@@ -899,6 +899,7 @@ files:
899
899
  - app/models/spree/adjustable/adjustments_updater.rb
900
900
  - app/models/spree/adjustable/promotion_accumulator.rb
901
901
  - app/models/spree/adjustment.rb
902
+ - app/models/spree/allowed_origin.rb
902
903
  - app/models/spree/api_key.rb
903
904
  - app/models/spree/asset.rb
904
905
  - app/models/spree/asset/support/active_storage.rb
@@ -1253,6 +1254,7 @@ files:
1253
1254
  - app/services/spree/sample_data/loader.rb
1254
1255
  - app/services/spree/seeds/admin_user.rb
1255
1256
  - app/services/spree/seeds/all.rb
1257
+ - app/services/spree/seeds/allowed_origins.rb
1256
1258
  - app/services/spree/seeds/api_keys.rb
1257
1259
  - app/services/spree/seeds/countries.rb
1258
1260
  - app/services/spree/seeds/default_reimbursement_types.rb
@@ -1445,6 +1447,8 @@ files:
1445
1447
  - db/migrate/20260220000000_create_spree_markets.rb
1446
1448
  - db/migrate/20260226000000_add_locale_to_spree_orders.rb
1447
1449
  - db/migrate/20260226100000_add_token_digest_to_spree_api_keys.rb
1450
+ - db/migrate/20260315000000_create_spree_allowed_origins.rb
1451
+ - db/migrate/20260315100000_add_product_media_support.rb
1448
1452
  - db/sample_data/customers.csv
1449
1453
  - db/sample_data/metafield_definitions.rb
1450
1454
  - db/sample_data/orders.rb
@@ -1530,6 +1534,7 @@ files:
1530
1534
  - lib/spree/testing_support/factories.rb
1531
1535
  - lib/spree/testing_support/factories/address_factory.rb
1532
1536
  - lib/spree/testing_support/factories/adjustment_factory.rb
1537
+ - lib/spree/testing_support/factories/allowed_origin_factory.rb
1533
1538
  - lib/spree/testing_support/factories/api_key_factory.rb
1534
1539
  - lib/spree/testing_support/factories/asset_factory.rb
1535
1540
  - lib/spree/testing_support/factories/calculator_factory.rb
@@ -1666,9 +1671,9 @@ licenses:
1666
1671
  - BSD-3-Clause
1667
1672
  metadata:
1668
1673
  bug_tracker_uri: https://github.com/spree/spree/issues
1669
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.beta7
1674
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.beta8
1670
1675
  documentation_uri: https://docs.spreecommerce.org/
1671
- source_code_uri: https://github.com/spree/spree/tree/v5.4.0.beta7
1676
+ source_code_uri: https://github.com/spree/spree/tree/v5.4.0.beta8
1672
1677
  post_install_message:
1673
1678
  rdoc_options: []
1674
1679
  require_paths: