spree_multi_store 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE.md +14 -0
- data/README.md +50 -0
- data/Rakefile +23 -0
- data/app/controllers/spree/admin/custom_domains_controller.rb +21 -0
- data/app/controllers/spree/admin/multi_store/base_controller_decorator.rb +13 -0
- data/app/controllers/spree/admin/multi_store/stores_controller.rb +35 -0
- data/app/finders/spree/stores/find_current.rb +28 -0
- data/app/helpers/spree/admin/multi_store_helper.rb +11 -0
- data/app/models/concerns/spree/multi_store_resource.rb +33 -0
- data/app/models/concerns/spree/store/multi_store_class_overrides.rb +15 -0
- data/app/models/concerns/spree/store/multi_store_methods.rb +108 -0
- data/app/models/concerns/spree/store/multi_store_overrides.rb +39 -0
- data/app/models/spree/custom_domain.rb +61 -0
- data/app/models/spree/multi_store/payment_method_decorator.rb +11 -0
- data/app/models/spree/multi_store/product_decorator.rb +11 -0
- data/app/models/spree/multi_store/promotion_decorator.rb +11 -0
- data/app/models/spree/multi_store/store_decorator.rb +13 -0
- data/app/views/spree/admin/custom_domains/_custom_domain.html.erb +11 -0
- data/app/views/spree/admin/custom_domains/_custom_domains.html.erb +19 -0
- data/app/views/spree/admin/custom_domains/_form.html.erb +7 -0
- data/app/views/spree/admin/custom_domains/edit.html.erb +1 -0
- data/app/views/spree/admin/custom_domains/index.html.erb +65 -0
- data/app/views/spree/admin/custom_domains/new.html.erb +1 -0
- data/app/views/spree/admin/multi_store/stores/new.html.erb +122 -0
- data/app/views/spree/admin/multi_store/stores/new.turbo_stream.erb +1 -0
- data/app/views/spree/admin/products/form/_stores.html.erb +29 -0
- data/app/views/spree/admin/shared/sidebar/_store_dropdown.html.erb +55 -0
- data/config/initializers/spree_multi_store.rb +19 -0
- data/config/initializers/spree_multi_store_navigation.rb +14 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +6 -0
- data/lib/spree/multi_store/engine.rb +22 -0
- data/lib/spree/multi_store/version.rb +5 -0
- data/lib/spree/multi_store.rb +3 -0
- data/lib/spree_multi_store.rb +1 -0
- metadata +134 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e04a1b3c2537beca45a3f71d694ab398b4beb627a8b1a7df529d2219720053fd
|
|
4
|
+
data.tar.gz: 3faad6a5bf518fa4a3939bee2475ee0799a5d545fed6dedd0b72521ef9c0ffae
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '0508c322500915fcd34e1c8f1d004020d2075c7abd27615b5b607a08374ced3eb074a64ab4fe1fbe4a764f4d46ebd64d65be80dc0b08c2147115a7c7f601eda5'
|
|
7
|
+
data.tar.gz: 4746391eeae2143b67e1fca2d70fac6853b00db0c8acc172f66fba4e7d84b94a2d1dd48cabbe8f6f723e63231f3d1ea4f04b46fd0217c3b8b151f648bfc4fdba
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright (c) 2026 Vendo Connect Inc. Vendo Sp. z o.o.
|
|
2
|
+
|
|
3
|
+
This program is free software: you can redistribute it and/or modify
|
|
4
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
5
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
(at your option) any later version.
|
|
7
|
+
|
|
8
|
+
This program is distributed in the hope that it will be useful,
|
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
GNU Affero General Public License for more details.
|
|
12
|
+
|
|
13
|
+
You should have received a copy of the GNU Affero General Public License
|
|
14
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
data/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Spree Multi-Store
|
|
2
|
+
|
|
3
|
+
Multi-store switching, custom domains, and store resolution for [Spree Commerce](https://spreecommerce.org).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-Store Management** — create and manage multiple storefronts from a single Spree installation
|
|
8
|
+
- **Custom Domains** — map custom domain names to individual stores
|
|
9
|
+
- **Store Resolution** — automatically detect the current store based on request URL or custom domain
|
|
10
|
+
- **Resource Scoping** — scope products, promotions, and payment methods to specific stores
|
|
11
|
+
- **Store Creation Wizard** — admin UI for creating new stores with product/payment method imports
|
|
12
|
+
|
|
13
|
+
# Multi-store vs Multi-tenant
|
|
14
|
+
|
|
15
|
+
* Multi-Store setup is recommended for running multiple brands or multiple language versions of the same store
|
|
16
|
+
* Multi-Tenant allows you to create a SaaS application with multiple tenants, each of them with their own Spree instance
|
|
17
|
+
|
|
18
|
+
> [!TIP]
|
|
19
|
+
> For a full [Multi-Tenant](https://spreecommerce.org/multi-tenant-white-label-ecommerce/) solution please [contact us](https://spreecommerce.org/get-started/) to obtain commercial license.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Add to your Gemfile:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'spree_multi_store'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then run:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bundle install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
Set your root domain for automatic subdomain-based store URLs:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# config/initializers/spree.rb
|
|
41
|
+
Spree.root_domain = ENV.fetch('SPREE_ROOT_DOMAIN', 'lvh.me')
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
Copyright (c) 2026 Vendo Connect Inc., Vendo Sp. z o.o.
|
|
47
|
+
|
|
48
|
+
Licensed under the [GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)](LICENSE.md).
|
|
49
|
+
|
|
50
|
+
For commercial licensing options, contact [sales@getvendo.com](mailto:sales@getvendo.com).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'bundler'
|
|
2
|
+
Bundler::GemHelper.install_tasks
|
|
3
|
+
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
require 'spree/testing_support/extension_rake'
|
|
6
|
+
|
|
7
|
+
RSpec::Core::RakeTask.new
|
|
8
|
+
|
|
9
|
+
task :default do
|
|
10
|
+
if Dir['spec/dummy'].empty?
|
|
11
|
+
Rake::Task[:test_app].invoke
|
|
12
|
+
Dir.chdir('../../')
|
|
13
|
+
end
|
|
14
|
+
Rake::Task[:spec].invoke
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc 'Generates a dummy app for testing'
|
|
18
|
+
task :test_app do
|
|
19
|
+
ENV['LIB_NAME'] = 'spree_multi_store'
|
|
20
|
+
Rake::Task['extension:test_app'].execute(
|
|
21
|
+
install_admin: true
|
|
22
|
+
)
|
|
23
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class CustomDomainsController < ResourceController
|
|
4
|
+
include Spree::Admin::SettingsConcern
|
|
5
|
+
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def collection_url
|
|
9
|
+
spree.admin_custom_domains_path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def location_after_save
|
|
13
|
+
spree.admin_custom_domains_path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def permitted_resource_params
|
|
17
|
+
params.require(:custom_domain).permit(Spree::PermittedAttributes.custom_domain_attributes)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module MultiStore
|
|
4
|
+
class StoresController < Spree::Admin::BaseController
|
|
5
|
+
def new
|
|
6
|
+
@store = Spree::Store.new(default_country_iso: 'US')
|
|
7
|
+
render :new, layout: 'spree/admin_wizard'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
@store = Spree::Store.new(permitted_store_params)
|
|
12
|
+
@store.mail_from_address = current_store.mail_from_address
|
|
13
|
+
|
|
14
|
+
if @store.save
|
|
15
|
+
# Move/copy all existing users (staff) to the new store
|
|
16
|
+
current_store.role_users.each do |role_user|
|
|
17
|
+
@store.add_user(role_user.user, role_user.role)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
flash[:success] = flash_message_for(@store, :successfully_created)
|
|
21
|
+
redirect_to spree.admin_getting_started_url(host: @store.url), allow_other_host: true
|
|
22
|
+
else
|
|
23
|
+
render :new, status: :unprocessable_content, layout: 'spree/admin_wizard'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def permitted_store_params
|
|
30
|
+
params.require(:store).permit(permitted_store_attributes)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Stores
|
|
3
|
+
class FindCurrent
|
|
4
|
+
def initialize(scope: nil, url: nil)
|
|
5
|
+
@scope = scope || Spree::Store
|
|
6
|
+
@url = url
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def execute
|
|
10
|
+
store = by_url(scope) || scope.default
|
|
11
|
+
return if store.nil?
|
|
12
|
+
|
|
13
|
+
Spree::Current.store = store
|
|
14
|
+
store
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
protected
|
|
18
|
+
|
|
19
|
+
attr_reader :scope, :url
|
|
20
|
+
|
|
21
|
+
def by_url(scope)
|
|
22
|
+
return if url.blank?
|
|
23
|
+
|
|
24
|
+
scope.by_custom_domain(url).or(scope.by_url(url)).first
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module MultiStoreHelper
|
|
4
|
+
def available_stores
|
|
5
|
+
scope = Spree::Store.accessible_by(current_ability, :manage).includes(:logo_attachment)
|
|
6
|
+
scope = scope.includes(:default_custom_domain) if Spree::Store.reflect_on_association(:default_custom_domain)
|
|
7
|
+
@available_stores ||= scope
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MultiStoreResource
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
scope :for_store, ->(store) { joins(:stores).where(Store.table_name => { id: store.id }) }
|
|
7
|
+
|
|
8
|
+
before_validation :set_default_store, if: :new_record?
|
|
9
|
+
|
|
10
|
+
validate :must_have_one_store, unless: :disable_store_presence_validation?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
protected
|
|
14
|
+
|
|
15
|
+
def must_have_one_store
|
|
16
|
+
return if stores.any?
|
|
17
|
+
|
|
18
|
+
errors.add(:stores, Spree.t(:must_have_one_store))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_default_store
|
|
22
|
+
return if disable_store_presence_validation?
|
|
23
|
+
return if stores.any?
|
|
24
|
+
|
|
25
|
+
stores << Spree::Store.default
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# this can be overridden on model basis
|
|
29
|
+
def disable_store_presence_validation?
|
|
30
|
+
Spree::Config[:disable_store_presence_validation]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Store::MultiStoreClassOverrides
|
|
3
|
+
def current(url = nil)
|
|
4
|
+
if url.present?
|
|
5
|
+
Spree.current_store_finder.new(url: url).execute
|
|
6
|
+
else
|
|
7
|
+
Spree::Current.store
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def available_locales
|
|
12
|
+
Spree::Store.all.map(&:supported_locales_list).flatten.uniq
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Store::MultiStoreMethods
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
RESERVED_CODES = %w(
|
|
6
|
+
admin default app api www cdn files assets checkout account auth login user
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
has_many :custom_domains, class_name: 'Spree::CustomDomain', dependent: :destroy
|
|
11
|
+
has_one :default_custom_domain, -> { where(default: true) }, class_name: 'Spree::CustomDomain'
|
|
12
|
+
|
|
13
|
+
attribute :import_products_from_store_id, :string, default: nil
|
|
14
|
+
attribute :import_payment_methods_from_store_id, :string, default: nil
|
|
15
|
+
|
|
16
|
+
attr_accessor :skip_validate_not_last
|
|
17
|
+
|
|
18
|
+
validates :code, uniqueness: { case_sensitive: false, conditions: -> { with_deleted } },
|
|
19
|
+
exclusion: Spree::Store::MultiStoreMethods::RESERVED_CODES
|
|
20
|
+
|
|
21
|
+
before_validation :set_url
|
|
22
|
+
after_create :import_products_from_store, if: -> { import_products_from_store_id.present? }
|
|
23
|
+
after_create :import_payment_methods_from_store, if: -> { import_payment_methods_from_store_id.present? }
|
|
24
|
+
after_commit :handle_code_changes, on: :update, if: -> { code_previously_changed? }
|
|
25
|
+
before_destroy :validate_not_last, unless: :skip_validate_not_last
|
|
26
|
+
before_destroy :pass_default_flag_to_other_store
|
|
27
|
+
|
|
28
|
+
scope :by_custom_domain, ->(url) { left_joins(:custom_domains).where("#{Spree::CustomDomain.table_name}.url" => url) }
|
|
29
|
+
scope :by_url, ->(url) { where(url: url).or(where("#{table_name}.url like ?", "%#{url}%")) }
|
|
30
|
+
|
|
31
|
+
# Re-configure FriendlyId to use :history for code tracking across renames
|
|
32
|
+
friendly_id :slug_candidates, use: [:slugged, :history], slug_column: :code, routes: :normal
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def formatted_custom_domain
|
|
36
|
+
return unless default_custom_domain
|
|
37
|
+
|
|
38
|
+
@formatted_custom_domain ||= if Rails.env.development? || Rails.env.test?
|
|
39
|
+
URI::Generic.build(
|
|
40
|
+
scheme: Rails.application.routes.default_url_options[:protocol] || 'http',
|
|
41
|
+
host: default_custom_domain.url,
|
|
42
|
+
port: Rails.application.routes.default_url_options[:port]
|
|
43
|
+
).to_s
|
|
44
|
+
else
|
|
45
|
+
URI::HTTPS.build(host: default_custom_domain.url).to_s
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def can_be_deleted?
|
|
50
|
+
self.class.where.not(id: id).any?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def import_products_from_store
|
|
54
|
+
store = Spree::Store.find(import_products_from_store_id)
|
|
55
|
+
product_ids = store.products.pluck(:id)
|
|
56
|
+
|
|
57
|
+
return if product_ids.empty?
|
|
58
|
+
|
|
59
|
+
Spree::StoreProduct.insert_all(product_ids.map { |product_id| { store_id: id, product_id: product_id } })
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def import_payment_methods_from_store
|
|
63
|
+
store = Spree::Store.find(import_payment_methods_from_store_id)
|
|
64
|
+
payment_method_ids = store.payment_method_ids
|
|
65
|
+
|
|
66
|
+
return if payment_method_ids.empty?
|
|
67
|
+
|
|
68
|
+
Spree::StorePaymentMethod.insert_all(payment_method_ids.map { |payment_method_id| { store_id: id, payment_method_id: payment_method_id } })
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def validate_not_last
|
|
74
|
+
unless can_be_deleted?
|
|
75
|
+
errors.add(:base, :cannot_destroy_only_store)
|
|
76
|
+
throw(:abort)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def pass_default_flag_to_other_store
|
|
81
|
+
if default? && can_be_deleted?
|
|
82
|
+
self.class.where.not(id: id).first.update!(default: true)
|
|
83
|
+
self.default = false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def handle_code_changes
|
|
88
|
+
# hook for custom logic on code changes
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Auto-assign internal URL for stores based on code + root domain
|
|
92
|
+
def set_url
|
|
93
|
+
return if url_changed?
|
|
94
|
+
return unless code_changed?
|
|
95
|
+
return unless Spree.root_domain.present?
|
|
96
|
+
|
|
97
|
+
self.url = [code, Spree.root_domain].join('.')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def slug_candidates
|
|
101
|
+
[]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def should_generate_new_friendly_id?
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Store::MultiStoreOverrides
|
|
3
|
+
def url_or_custom_domain
|
|
4
|
+
default_custom_domain&.url || url
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def formatted_url_or_custom_domain
|
|
8
|
+
formatted_custom_domain || formatted_url
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def can_be_deleted?
|
|
12
|
+
self.class.where.not(id: id).any?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Override core's simple set_default_code with full code generation logic
|
|
18
|
+
def set_default_code
|
|
19
|
+
self.code = if code.present?
|
|
20
|
+
code.parameterize.strip
|
|
21
|
+
elsif name.present?
|
|
22
|
+
name.parameterize.strip
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
return if self.code.blank?
|
|
26
|
+
|
|
27
|
+
# ensure code is unique
|
|
28
|
+
self.code = [name.parameterize, rand(9999)].join('-') while Spree::Store.with_deleted.where(code: self.code).exists?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def should_generate_new_friendly_id?
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def slug_candidates
|
|
36
|
+
[]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class CustomDomain < Spree::Base
|
|
3
|
+
has_prefix_id :domain
|
|
4
|
+
|
|
5
|
+
include Spree::SingleStoreResource
|
|
6
|
+
include Spree::Metafields
|
|
7
|
+
include Spree::Metadata
|
|
8
|
+
|
|
9
|
+
normalizes :url, with: ->(value) { value&.to_s&.squish&.presence }
|
|
10
|
+
|
|
11
|
+
#
|
|
12
|
+
# Associations
|
|
13
|
+
#
|
|
14
|
+
belongs_to :store, class_name: 'Spree::Store', inverse_of: :custom_domains, touch: true
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Validations
|
|
18
|
+
#
|
|
19
|
+
validates :url, presence: true, uniqueness: true, format: {
|
|
20
|
+
with: %r{\A(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z}i
|
|
21
|
+
}, length: { in: 1..63 }
|
|
22
|
+
validate :url_is_valid
|
|
23
|
+
|
|
24
|
+
#
|
|
25
|
+
# Callbacks
|
|
26
|
+
#
|
|
27
|
+
before_validation :sanitize_url
|
|
28
|
+
after_save :ensure_has_one_default
|
|
29
|
+
after_validation :ensure_default, on: :create
|
|
30
|
+
|
|
31
|
+
def url_is_valid
|
|
32
|
+
return if url.blank?
|
|
33
|
+
parts = url.split('.')
|
|
34
|
+
|
|
35
|
+
errors.add(:url, 'use domain or subdomain') if parts.size > 4 || parts.size < 2
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ensure_default
|
|
39
|
+
self.default = store.custom_domains.count.zero?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ensure_has_one_default
|
|
43
|
+
store.custom_domains.where.not(id: id).update_all(default: false) if default?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def active?
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def name
|
|
51
|
+
url
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# remove https:// and http:// from the url
|
|
57
|
+
def sanitize_url
|
|
58
|
+
self.url = url&.gsub(%r{https?://}, '')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MultiStore
|
|
3
|
+
module StoreDecorator
|
|
4
|
+
def self.prepended(base)
|
|
5
|
+
base.include Spree::Store::MultiStoreMethods
|
|
6
|
+
base.singleton_class.prepend Spree::Store::MultiStoreClassOverrides
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Store.prepend(MultiStore::StoreDecorator)
|
|
12
|
+
Store.prepend(Store::MultiStoreOverrides)
|
|
13
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<tr id="<%= spree_dom_id custom_domain %>">
|
|
2
|
+
<td><%= custom_domain.url %></td>
|
|
3
|
+
<td>
|
|
4
|
+
<%= active_badge(custom_domain.active?) %>
|
|
5
|
+
</td>
|
|
6
|
+
|
|
7
|
+
<td><%= active_badge(custom_domain.default?) %></td>
|
|
8
|
+
<td class="actions">
|
|
9
|
+
<%= link_to_edit(custom_domain, no_text: true, url: spree.edit_admin_custom_domain_path(custom_domain), data: { turbo: false }) %>
|
|
10
|
+
</td>
|
|
11
|
+
</tr>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<% if current_store.custom_domains.any? %>
|
|
2
|
+
<div class="table-responsive rounded-lg mt-4 border">
|
|
3
|
+
<table class="table">
|
|
4
|
+
<thead>
|
|
5
|
+
<tr>
|
|
6
|
+
<th scope="col"><%= sort_link @search, :name, Spree.t(:name) %></th>
|
|
7
|
+
<th scope="col"><%= Spree.t(:active) %>?</th>
|
|
8
|
+
<th scope="col"><%= Spree.t(:default) %>?</th>
|
|
9
|
+
<th scope="col"></th>
|
|
10
|
+
</tr>
|
|
11
|
+
</thead>
|
|
12
|
+
<tbody>
|
|
13
|
+
<%= render collection: current_store.custom_domains, partial: 'spree/admin/custom_domains/custom_domain', cached: spree_base_cache_scope %>
|
|
14
|
+
</tbody>
|
|
15
|
+
</table>
|
|
16
|
+
</div>
|
|
17
|
+
<% else %>
|
|
18
|
+
<%= render 'spree/admin/shared/no_resource_found' %>
|
|
19
|
+
<% end %>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<div class="card mb-6">
|
|
2
|
+
<div class="card-body">
|
|
3
|
+
<%= f.spree_text_field :url, label: Spree.t(:domain), prepend: 'https://', autofocus: f.object.new_record?, required: true %>
|
|
4
|
+
|
|
5
|
+
<%= f.spree_check_box :default, help: 'We will use this domain in emails to customers.' %>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render 'spree/admin/shared/edit_resource' %>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<%= content_for(:page_title) do %>
|
|
2
|
+
<%= Spree.t(:domains) %>
|
|
3
|
+
<% end %>
|
|
4
|
+
|
|
5
|
+
<% content_for :page_actions do %>
|
|
6
|
+
<%= render_admin_partials(:custom_domains_actions_partials) %>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<%= render_admin_partials(:custom_domains_header_partials) %>
|
|
10
|
+
|
|
11
|
+
<div class="card-lg p-6">
|
|
12
|
+
<h5 class="mb-2">Internal URL</h5>
|
|
13
|
+
<div class="grid grid-cols-12 gap-6 mb-6">
|
|
14
|
+
<div class="col-span-12 lg:col-span-4">
|
|
15
|
+
<p class="text-gray-600">
|
|
16
|
+
This is your internal Admin URL.
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="col-span-12 lg:col-span-7 lg:col-start-6">
|
|
20
|
+
<%= form_for current_store, url: spree.admin_store_path, data: { turbo: false, controller: 'enable-button', 'enable-button-disable-when-not-changed-value': true } do |f| %>
|
|
21
|
+
<% if Spree.root_domain.present? %>
|
|
22
|
+
<div class="flex items-center gap-6">
|
|
23
|
+
<div class="input-group pr-2 grow <% if current_store.custom_domains.any? %>disabled<% end %>">
|
|
24
|
+
<span class="text-gray-400 pl-3 pr-0">https://</span>
|
|
25
|
+
<%= f.text_field :code, class: 'border-0 focus:ring-0 focus:outline-none grow rounded-lg text-base pl-0', data: { enable_button_target: 'input' }, required: true, disabled: current_store.custom_domains.any? %>
|
|
26
|
+
<span>.<%= Spree.root_domain %></span>
|
|
27
|
+
|
|
28
|
+
<%= clipboard_component(current_store.formatted_url) %>
|
|
29
|
+
</div>
|
|
30
|
+
<% unless current_store.custom_domains.any? %>
|
|
31
|
+
<%= turbo_save_button_tag %>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
<% else %>
|
|
35
|
+
<div class="flex items-center gap-6">
|
|
36
|
+
<div class="input-group pr-2 grow">
|
|
37
|
+
<span class="text-gray-400 pl-3 pr-0">https://</span>
|
|
38
|
+
<%= f.text_field :url, class: 'border-0 focus:ring-0 focus:outline-none grow rounded-lg text-base pl-0', required: true, data: { enable_button_target: 'input' } %>
|
|
39
|
+
<%= clipboard_component(current_store.formatted_url) %>
|
|
40
|
+
</div>
|
|
41
|
+
<%= turbo_save_button_tag %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<hr class="my-12" />
|
|
48
|
+
<h5 class="mb-2"><%= Spree.t(:custom_domains) %></h5>
|
|
49
|
+
<div class="grid grid-cols-12 gap-6">
|
|
50
|
+
<div class="col-span-12 lg:col-span-4">
|
|
51
|
+
<p class="text-gray-600">
|
|
52
|
+
Connect your domain or subdomain to your storefront.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="col-span-12 lg:col-span-7 lg:col-start-6">
|
|
56
|
+
<div class="text-right">
|
|
57
|
+
<%= link_to Spree.t(:new_domain), spree.new_admin_custom_domain_path, class: "btn btn-primary" %>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<%= turbo_frame_tag 'admin_custom_domains_index' do %>
|
|
61
|
+
<%= render 'custom_domains' %>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render 'spree/admin/shared/new_resource' %>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<%= form_for @store, url: spree.admin_stores_path, data: { turbo: false, controller: 'enable-button' } do |f| %>
|
|
2
|
+
<div class="container mx-auto px-4 pt-8">
|
|
3
|
+
<h1 class="mb-6"><%= Spree.t(:new_store) %></h1>
|
|
4
|
+
|
|
5
|
+
<%= render partial: 'spree/admin/shared/error_messages', locals: { target: @store } %>
|
|
6
|
+
<div class="grid grid-cols-12 gap-16">
|
|
7
|
+
<div class="col-span-12 lg:col-span-6">
|
|
8
|
+
<div class="form-group">
|
|
9
|
+
<%= f.label :name %>
|
|
10
|
+
<%= f.text_field :name, class: 'form-input',
|
|
11
|
+
placeholder: Spree.t(:store_name),
|
|
12
|
+
required: true,
|
|
13
|
+
autofocus: true,
|
|
14
|
+
data: {
|
|
15
|
+
enable_button_target: 'input',
|
|
16
|
+
} %>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="form-group">
|
|
20
|
+
<%= f.label :default_country_iso, Spree.t(:country) %>
|
|
21
|
+
<%= f.collection_select :default_country_iso, Spree::Country.order(:name), :iso, :name, { }, { data: { controller: 'autocomplete-select', enable_button_target: 'input' } } %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<% if available_stores.any? %>
|
|
25
|
+
<div class="form-group">
|
|
26
|
+
<%= f.label :import_products_from_store_id, Spree.t(:import_products_from) %>
|
|
27
|
+
<%= f.select :import_products_from_store_id,
|
|
28
|
+
options_for_select(available_stores.pluck(:name, :id)),
|
|
29
|
+
{ include_blank: Spree.t(:do_not_import) },
|
|
30
|
+
{
|
|
31
|
+
data: { controller: 'autocomplete-select' }
|
|
32
|
+
} %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="form-group">
|
|
36
|
+
<%= f.label :import_payment_methods_from_store_id, Spree.t(:import_payment_methods_from) %>
|
|
37
|
+
<%= f.select :import_payment_methods_from_store_id,
|
|
38
|
+
options_for_select(available_stores.pluck(:name, :id)),
|
|
39
|
+
{ include_blank: Spree.t(:do_not_import) },
|
|
40
|
+
{
|
|
41
|
+
data: { controller: 'autocomplete-select' }
|
|
42
|
+
} %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="col-span-12 lg:col-span-5 lg:col-start-8">
|
|
47
|
+
<h5 class="mb-2">What will be shared with this new store:</h5>
|
|
48
|
+
<ul class="list-none p-0">
|
|
49
|
+
<li>
|
|
50
|
+
<%= icon('check', class: 'text-green-700') %>
|
|
51
|
+
<%= Spree.t(:products) %>, <%= Spree.t(:stock_locations) %> & <%= Spree.t(:inventory) %><br>
|
|
52
|
+
<em class="ml-5 text-gray-600 text-sm">you can select which ones will be available for this store</em>
|
|
53
|
+
</li>
|
|
54
|
+
<li>
|
|
55
|
+
<%= icon('check', class: 'text-green-700') %>
|
|
56
|
+
<%= Spree.t(:customers) %>
|
|
57
|
+
</li>
|
|
58
|
+
<li>
|
|
59
|
+
<%= icon('check', class: 'text-green-700') %>
|
|
60
|
+
<%= Spree.t(:shipping_methods) %>
|
|
61
|
+
</li>
|
|
62
|
+
<li>
|
|
63
|
+
<%= icon('check', class: 'text-green-700') %>
|
|
64
|
+
<%= Spree.t(:payment_methods) %><br>
|
|
65
|
+
<em class="ml-5 text-gray-600 text-sm">you can select which ones will be available for this store</em>
|
|
66
|
+
</li>
|
|
67
|
+
<li>
|
|
68
|
+
<%= icon('check', class: 'text-green-700') %>
|
|
69
|
+
Admin user privileges
|
|
70
|
+
</li>
|
|
71
|
+
</ul>
|
|
72
|
+
|
|
73
|
+
<h5 class="mt-6 mb-2">What will not be shared with this new store:</h5>
|
|
74
|
+
<ul class="list-none p-0">
|
|
75
|
+
<li>
|
|
76
|
+
<%= icon('x', class: 'text-danger') %>
|
|
77
|
+
<%= Spree.t(:orders) %>
|
|
78
|
+
</li>
|
|
79
|
+
<li>
|
|
80
|
+
<%= icon('x', class: 'text-danger') %>
|
|
81
|
+
<%= Spree.t(:shipments) %>
|
|
82
|
+
</li>
|
|
83
|
+
<li>
|
|
84
|
+
<%= icon('x', class: 'text-danger') %>
|
|
85
|
+
<%= Spree.t(:payments) %>
|
|
86
|
+
</li>
|
|
87
|
+
<li>
|
|
88
|
+
<%= icon('x', class: 'text-danger') %>
|
|
89
|
+
<%= Spree.t(:refunds) %>
|
|
90
|
+
</li>
|
|
91
|
+
<li>
|
|
92
|
+
<%= icon('x', class: 'text-danger') %>
|
|
93
|
+
<%= Spree.t(:store_credits) %>
|
|
94
|
+
</li>
|
|
95
|
+
<li>
|
|
96
|
+
<%= icon('x', class: 'text-danger') %>
|
|
97
|
+
<%= Spree.t(:gift_cards) %>
|
|
98
|
+
</li>
|
|
99
|
+
<li>
|
|
100
|
+
<%= icon('x', class: 'text-danger') %>
|
|
101
|
+
<%= Spree.t(:blogs_posts) %>
|
|
102
|
+
</li>
|
|
103
|
+
<li>
|
|
104
|
+
<%= icon('x', class: 'text-danger') %>
|
|
105
|
+
<%= Spree.t(:markets) %>
|
|
106
|
+
</li>
|
|
107
|
+
<li>
|
|
108
|
+
<%= icon('x', class: 'text-danger') %>
|
|
109
|
+
<%= Spree.t(:themes) %> & <%= Spree.t(:pages) %>
|
|
110
|
+
</li>
|
|
111
|
+
<li>
|
|
112
|
+
<%= icon('x', class: 'text-danger') %>
|
|
113
|
+
<%= Spree.t(:integrations) %> eg. Google Analytics, Meta Pixel, etc.
|
|
114
|
+
</li>
|
|
115
|
+
</ul>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<footer id="footer" class="fixed bottom-0 left-0 w-full border-t border-gray-200 px-6 py-4 bg-gray-25 flex items-center justify-end gap-4 z-[1000]">
|
|
120
|
+
<%= turbo_save_button_tag Spree.t('actions.create'), data: { admin_target: 'save' } %>
|
|
121
|
+
</footer>
|
|
122
|
+
<% end %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render template: 'spree/admin/stores/new', formats: [:html] %>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<% if available_stores.count > 1 %>
|
|
2
|
+
<div class="card mb-6">
|
|
3
|
+
<div class="card-header">
|
|
4
|
+
<h5 class="card-title">
|
|
5
|
+
<%= Spree.t(:stores) %>
|
|
6
|
+
</h5>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="card-body">
|
|
9
|
+
<div class="form-group">
|
|
10
|
+
<% if can? :modify, Spree::StoreProduct %>
|
|
11
|
+
<p class="form-text"><%= Spree.t('admin.products.stores.choose_stores') %>.</p>
|
|
12
|
+
|
|
13
|
+
<%= f.collection_check_boxes :store_ids, available_stores, :id, :name do |b| %>
|
|
14
|
+
<div class="custom-control form-checkbox mb-1">
|
|
15
|
+
<%= b.check_box(class: 'custom-control-input') %>
|
|
16
|
+
<%= b.label(class: 'custom-control-label') %>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% elsif @product.stores.any? %>
|
|
20
|
+
<ul class="text_list">
|
|
21
|
+
<% @product.stores.pluck(:name).each do |store_name| %>
|
|
22
|
+
<li><%= store_name %></li>
|
|
23
|
+
<% end %>
|
|
24
|
+
</ul>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<div class="store-dropdown" id="store-menu">
|
|
2
|
+
<%= dropdown do %>
|
|
3
|
+
<%= dropdown_toggle class: 'store-dropdown-button dropdown-toggle' do %>
|
|
4
|
+
<%= store_admin_icon(current_store, height: 32, width: 32) %>
|
|
5
|
+
<span class="store-name hidden lg:block">
|
|
6
|
+
<%= truncate(current_store.name, length: 10) %>
|
|
7
|
+
</span>
|
|
8
|
+
<% end %>
|
|
9
|
+
<%= dropdown_menu do %>
|
|
10
|
+
<%= link_to current_store.formatted_url_or_custom_domain, class: 'dropdown-item', target: '_blank' do %>
|
|
11
|
+
<%= icon 'eye' %>
|
|
12
|
+
<%= Spree.t(:view_store) %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= link_to spree.edit_admin_theme_path(current_store.default_theme), class: 'dropdown-item', data: { turbo_prefetch: false } do %>
|
|
15
|
+
<%= icon 'tools' %>
|
|
16
|
+
<%= Spree.t('admin.edit_theme') %>
|
|
17
|
+
<% end if current_store.respond_to?(:default_theme) && current_store.default_theme && can?(:manage, Spree::Theme) %>
|
|
18
|
+
|
|
19
|
+
<% if available_stores.count > 1 %>
|
|
20
|
+
<div class="dropdown-divider"></div>
|
|
21
|
+
<h6 class="dropdown-header">
|
|
22
|
+
<%= Spree.t(:switch_store) %>
|
|
23
|
+
</h6>
|
|
24
|
+
<div class="store-chooser">
|
|
25
|
+
<% available_stores.each do |store| %>
|
|
26
|
+
<% if store.id == current_store.id %>
|
|
27
|
+
<div class="store-chooser-item active">
|
|
28
|
+
<%= store_admin_icon(store, height: 32, width: 32) %>
|
|
29
|
+
<span class="flex flex-col mr-2">
|
|
30
|
+
<%= store.name %>
|
|
31
|
+
<small class="text-gray-600">
|
|
32
|
+
<%= store.url_or_custom_domain %>
|
|
33
|
+
</small>
|
|
34
|
+
</span>
|
|
35
|
+
<%= icon('check', class: 'ml-auto') %>
|
|
36
|
+
</div>
|
|
37
|
+
<% else %>
|
|
38
|
+
<%= link_to spree.admin_dashboard_url(host: store.url), class: 'store-chooser-item' do %>
|
|
39
|
+
<%= store_admin_icon(store, height: 32, width: 32) %>
|
|
40
|
+
<span class="flex flex-col">
|
|
41
|
+
<%= store.name %>
|
|
42
|
+
<small class="text-gray-600">
|
|
43
|
+
<%= store.url_or_custom_domain %>
|
|
44
|
+
</small>
|
|
45
|
+
</span>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% end %>
|
|
49
|
+
</div>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
52
|
+
<% end %>
|
|
53
|
+
|
|
54
|
+
<%= render 'spree/admin/shared/new_item_dropdown' %>
|
|
55
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Rails.application.config.after_initialize do
|
|
2
|
+
Spree::Dependencies.current_store_finder = 'Spree::Stores::FindCurrent'
|
|
3
|
+
|
|
4
|
+
Spree.metafields.enabled_resources << Spree::CustomDomain
|
|
5
|
+
|
|
6
|
+
Spree::PermittedAttributes.store_attributes.push(
|
|
7
|
+
:import_products_from_store_id,
|
|
8
|
+
:import_payment_methods_from_store_id
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
if defined?(Spree::Admin)
|
|
12
|
+
Spree.admin.partials.product_form_sidebar << 'spree/admin/products/form/stores'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Multi-store setup
|
|
16
|
+
# You need to set a wildcard `root_domain` on the store to enable multi-store setup
|
|
17
|
+
# all new stores will be created in a subdomain of the root domain, eg. store1.localhost, store2.localhost, etc.
|
|
18
|
+
Spree.root_domain = ENV.fetch('SPREE_ROOT_DOMAIN', 'localhost')
|
|
19
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Rails.application.config.after_initialize do
|
|
2
|
+
next unless defined?(Spree::Admin)
|
|
3
|
+
|
|
4
|
+
settings_nav = Spree.admin.navigation.settings
|
|
5
|
+
|
|
6
|
+
# Domains
|
|
7
|
+
settings_nav.add :domains,
|
|
8
|
+
label: :domains,
|
|
9
|
+
url: :admin_custom_domains_path,
|
|
10
|
+
icon: 'world-www',
|
|
11
|
+
position: 60,
|
|
12
|
+
active: -> { controller_name == 'custom_domains' },
|
|
13
|
+
if: -> { can?(:manage, Spree::CustomDomain) }
|
|
14
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MultiStore
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
isolate_namespace Spree
|
|
5
|
+
engine_name 'spree_multi_store'
|
|
6
|
+
|
|
7
|
+
# Rails 7.1 introduced a new feature that raises an error if a callback action is missing.
|
|
8
|
+
# We need to disable it as we use a lot of concerns that add callback actions.
|
|
9
|
+
initializer 'spree.multi_store.disable_raise_on_missing_callback_actions' do |app|
|
|
10
|
+
app.config.action_controller.raise_on_missing_callback_actions = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.activate
|
|
14
|
+
Dir.glob(File.join(File.dirname(__FILE__), '../../../app/**/*_decorator*.rb')) do |c|
|
|
15
|
+
Rails.configuration.cache_classes ? require(c) : load(c)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
config.to_prepare(&method(:activate).to_proc)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'spree/multi_store'
|
metadata
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: spree_multi_store
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vendo Connect Inc.
|
|
8
|
+
- Vendo Sp. z o.o.
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: spree
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 5.4.0.beta
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 5.4.0.beta
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: spree_admin
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 5.4.0.beta
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 5.4.0.beta
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: spree_dev_tools
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
description: Adds multi-store switching, custom domains, and store resolution to Spree
|
|
56
|
+
Commerce
|
|
57
|
+
email: hello@spreecommerce.org
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- LICENSE.md
|
|
63
|
+
- README.md
|
|
64
|
+
- Rakefile
|
|
65
|
+
- app/controllers/spree/admin/custom_domains_controller.rb
|
|
66
|
+
- app/controllers/spree/admin/multi_store/base_controller_decorator.rb
|
|
67
|
+
- app/controllers/spree/admin/multi_store/stores_controller.rb
|
|
68
|
+
- app/finders/spree/stores/find_current.rb
|
|
69
|
+
- app/helpers/spree/admin/multi_store_helper.rb
|
|
70
|
+
- app/models/concerns/spree/multi_store_resource.rb
|
|
71
|
+
- app/models/concerns/spree/store/multi_store_class_overrides.rb
|
|
72
|
+
- app/models/concerns/spree/store/multi_store_methods.rb
|
|
73
|
+
- app/models/concerns/spree/store/multi_store_overrides.rb
|
|
74
|
+
- app/models/spree/custom_domain.rb
|
|
75
|
+
- app/models/spree/multi_store/payment_method_decorator.rb
|
|
76
|
+
- app/models/spree/multi_store/product_decorator.rb
|
|
77
|
+
- app/models/spree/multi_store/promotion_decorator.rb
|
|
78
|
+
- app/models/spree/multi_store/store_decorator.rb
|
|
79
|
+
- app/views/spree/admin/custom_domains/_custom_domain.html.erb
|
|
80
|
+
- app/views/spree/admin/custom_domains/_custom_domains.html.erb
|
|
81
|
+
- app/views/spree/admin/custom_domains/_form.html.erb
|
|
82
|
+
- app/views/spree/admin/custom_domains/edit.html.erb
|
|
83
|
+
- app/views/spree/admin/custom_domains/index.html.erb
|
|
84
|
+
- app/views/spree/admin/custom_domains/new.html.erb
|
|
85
|
+
- app/views/spree/admin/multi_store/stores/new.html.erb
|
|
86
|
+
- app/views/spree/admin/multi_store/stores/new.turbo_stream.erb
|
|
87
|
+
- app/views/spree/admin/products/form/_stores.html.erb
|
|
88
|
+
- app/views/spree/admin/shared/sidebar/_store_dropdown.html.erb
|
|
89
|
+
- config/initializers/spree_multi_store.rb
|
|
90
|
+
- config/initializers/spree_multi_store_navigation.rb
|
|
91
|
+
- config/locales/en.yml
|
|
92
|
+
- config/routes.rb
|
|
93
|
+
- lib/spree/multi_store.rb
|
|
94
|
+
- lib/spree/multi_store/engine.rb
|
|
95
|
+
- lib/spree/multi_store/version.rb
|
|
96
|
+
- lib/spree_multi_store.rb
|
|
97
|
+
homepage: https://github.com/spree/spree_multi_store
|
|
98
|
+
licenses:
|
|
99
|
+
- AGPL-3.0-or-later
|
|
100
|
+
metadata:
|
|
101
|
+
bug_tracker_uri: https://github.com/spree/spree_multi_store/issues
|
|
102
|
+
changelog_uri: https://github.com/spree/spree_multi_store/releases/tag/v1.0.0
|
|
103
|
+
documentation_uri: https://docs.spreecommerce.org/
|
|
104
|
+
source_code_uri: https://github.com/spree/spree_multi_store/tree/v1.0.0
|
|
105
|
+
post_install_message: |
|
|
106
|
+
--------------------------------------------------------------
|
|
107
|
+
Thank you for installing Spree Multi-Store!
|
|
108
|
+
|
|
109
|
+
This gem is licensed under AGPL-3.0-or-later.
|
|
110
|
+
To obtain a commercial license for your project,
|
|
111
|
+
check out Spree Enterprise:
|
|
112
|
+
|
|
113
|
+
https://spreecommerce.org/enterprise/
|
|
114
|
+
|
|
115
|
+
--------------------------------------------------------------
|
|
116
|
+
rdoc_options: []
|
|
117
|
+
require_paths:
|
|
118
|
+
- lib
|
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '3.2'
|
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - ">="
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '0'
|
|
129
|
+
requirements:
|
|
130
|
+
- none
|
|
131
|
+
rubygems_version: 4.0.2
|
|
132
|
+
specification_version: 4
|
|
133
|
+
summary: Multi-store switching and resolution for Spree Commerce
|
|
134
|
+
test_files: []
|