├── .github └── stale.yml ├── templates ├── app │ ├── controllers │ │ ├── concerns │ │ │ ├── .keep │ │ │ └── taxonomies.rb │ │ ├── application_controller.rb │ │ ├── checkout_sessions_controller.rb │ │ ├── user_confirmations_controller.rb │ │ ├── store_devise_controller.rb │ │ ├── checkout_guest_sessions_controller.rb │ │ ├── orders_controller.rb │ │ ├── locale_controller.rb │ │ ├── home_controller.rb │ │ ├── solidus_paypal_commerce_platform │ │ │ ├── paypal_orders_controller.rb │ │ │ ├── shipping_rates_controller.rb │ │ │ ├── payments_controller.rb │ │ │ └── wizard_controller.rb │ │ ├── taxons_controller.rb │ │ ├── store_controller.rb │ │ ├── coupon_codes_controller.rb │ │ ├── autocomplete_results_controller.rb │ │ ├── user_registrations_controller.rb │ │ ├── user_passwords_controller.rb │ │ ├── cart_line_items_controller.rb │ │ ├── user_sessions_controller.rb │ │ ├── users_controller.rb │ │ └── products_controller.rb │ ├── views │ │ ├── shared │ │ │ ├── auth │ │ │ │ └── unauthorized.html.erb │ │ │ ├── cart │ │ │ │ └── _link_to_cart.html.erb │ │ │ ├── search │ │ │ │ ├── _filters.html.erb │ │ │ │ └── _search_bar.html.erb │ │ │ ├── _error_messages.html.erb │ │ │ ├── navigation │ │ │ │ ├── _taxonomies.html.erb │ │ │ │ └── _theme_switcher.html.erb │ │ │ └── _call_to_action.html.erb │ │ ├── orders │ │ │ ├── payment_info │ │ │ │ ├── _check.html.erb │ │ │ │ ├── _default.html.erb │ │ │ │ └── _gateway.html.erb │ │ │ ├── _order_items.html.erb │ │ │ ├── _coupon_code.html.erb │ │ │ ├── _item_info.html.erb │ │ │ ├── _payment_info.html.erb │ │ │ ├── show.html.erb │ │ │ ├── payment │ │ │ │ └── _paypal_commerce_platform.html.erb │ │ │ ├── _order_shipments.html.erb │ │ │ ├── _order_item.html.erb │ │ │ ├── _address_overview.html.erb │ │ │ └── _order_details.html.erb │ │ ├── checkouts │ │ │ ├── payment │ │ │ │ ├── _check.html.erb │ │ │ │ ├── _paypal_commerce_platform.html.erb │ │ │ │ └── _gateway.html.erb │ │ │ ├── _terms_and_conditions.en.html.erb │ │ │ ├── steps │ │ │ │ ├── delivery_step │ │ │ │ │ ├── _unshippable_items.html.erb │ │ │ │ │ ├── _variants_list.html.erb │ │ │ │ │ ├── _shipping_methods.html.erb │ │ │ │ │ ├── _proposed_shipment.erb │ │ │ │ │ └── _shipment_items.html.erb │ │ │ │ ├── _delivery_step.html.erb │ │ │ │ ├── _confirm_step.html.erb │ │ │ │ └── _address_step.html.erb │ │ │ ├── _checkout_header.html.erb │ │ │ ├── existing_payment │ │ │ │ └── _gateway.html.erb │ │ │ ├── _checkout_step.html.erb │ │ │ └── edit.html.erb │ │ ├── carts │ │ │ ├── _cart_header.html.erb │ │ │ ├── _cart_adjustment.html.erb │ │ │ ├── _cart_empty.html.erb │ │ │ ├── _cart_amount_row.html.erb │ │ │ ├── _cart_secondary_actions.html.erb │ │ │ ├── _cart_footer.html.erb │ │ │ ├── _cart_item_remove.html.erb │ │ │ ├── _cart_items.html.erb │ │ │ ├── show.html.erb │ │ │ ├── _cart_adjustments.html.erb │ │ │ └── _cart_item.html.erb │ │ ├── products │ │ │ ├── _product_info.html.erb │ │ │ ├── _products_by_taxon.html.erb │ │ │ ├── _products_grid.html.erb │ │ │ ├── _product_image.html.erb │ │ │ ├── _product_taxons.html.erb │ │ │ ├── _featured_product_card.html.erb │ │ │ ├── _product_properties.html.erb │ │ │ ├── payment │ │ │ │ └── _paypal_commerce_platform.html.erb │ │ │ ├── _product_promotions.html.erb │ │ │ ├── _product_header.html.erb │ │ │ ├── index.html.erb │ │ │ ├── _products.html.erb │ │ │ ├── show.html.erb │ │ │ └── _product_thumbnails.html.erb │ │ ├── user_mailer │ │ │ ├── confirmation_instructions.text.erb │ │ │ └── reset_password_instructions.text.erb │ │ ├── solidus_paypal_commerce_platform │ │ │ ├── cart │ │ │ │ └── _cart_buttons.html.erb │ │ │ ├── product │ │ │ │ └── _product_buttons.html.erb │ │ │ ├── payments │ │ │ │ └── _payment.html.erb │ │ │ └── shared │ │ │ │ └── _javascript_sdk_tag.html.erb │ │ ├── address │ │ │ └── _form_hidden.html.erb │ │ ├── checkout_sessions │ │ │ └── new.html.erb │ │ ├── cart_line_items │ │ │ ├── _product_availability.html.erb │ │ │ ├── _product_submit.html.erb │ │ │ ├── _form.html.erb │ │ │ ├── _product_variants.html.erb │ │ │ └── product_selection │ │ │ │ └── _option_type.html.erb │ │ ├── home │ │ │ ├── index.html.erb │ │ │ ├── _featured_products.html.erb │ │ │ ├── _featured_product_banner.html.erb │ │ │ ├── _collection.erb │ │ │ └── _collections_with_call_to_action.html.erb │ │ ├── autocomplete_results │ │ │ ├── index.turbo_stream.erb │ │ │ └── _autocomplete_results.html.erb │ │ ├── users │ │ │ ├── _users_menu.erb │ │ │ └── edit.html.erb │ │ ├── taxons │ │ │ └── show.html.erb │ │ ├── spree │ │ │ └── admin │ │ │ │ └── user_sessions │ │ │ │ └── new.html.erb │ │ ├── user_passwords │ │ │ ├── new.html.erb │ │ │ └── edit.html.erb │ │ ├── checkout_guest_sessions │ │ │ └── _form.html.erb │ │ ├── layouts │ │ │ ├── _top_bar.html.erb │ │ │ ├── _footer.html.erb │ │ │ └── storefront.html.erb │ │ ├── user_registrations │ │ │ └── new.html.erb │ │ └── user_sessions │ │ │ └── new.html.erb │ ├── assets │ │ ├── stylesheets │ │ │ ├── screen.scss │ │ │ ├── fonts │ │ │ │ ├── inter │ │ │ │ │ ├── inter-v18-latin-500.woff2 │ │ │ │ │ ├── inter-v18-latin-600.woff2 │ │ │ │ │ └── inter-v18-latin-regular.woff2 │ │ │ │ ├── source-serif │ │ │ │ │ ├── source-serif-4-v8-latin-500.woff2 │ │ │ │ │ └── source-serif-4-v8-latin-regular.woff2 │ │ │ │ └── _fonts.scss │ │ │ └── solidus_starter_frontend.css │ │ ├── config │ │ │ └── solidus_starter_frontend_manifest.js │ │ ├── images │ │ │ └── logo_small_no_label.svg │ │ └── javascripts │ │ │ ├── solidus_starter_frontend.js │ │ │ ├── utils.js.erb │ │ │ ├── checkout.js │ │ │ └── product.js │ ├── helpers │ │ ├── taxons_helper.rb │ │ ├── orders_helper.rb │ │ ├── checkout_helper.rb │ │ ├── taxon_filters_helper.rb │ │ ├── product_options_helper.rb │ │ └── layout_helper.rb │ ├── javascript │ │ ├── application.js │ │ └── controllers │ │ │ ├── locale_selector_controller.js │ │ │ ├── application.js │ │ │ ├── drawer_controller.js │ │ │ ├── cart_page_controller.js │ │ │ ├── top_bar_controller.js │ │ │ ├── index.js │ │ │ ├── theme_switcher_controller.js │ │ │ ├── checkout_payment_controller.js │ │ │ └── search_controller.js │ ├── overrides │ │ ├── taxon_custom_queries.rb │ │ └── product_featured_similar_products.rb │ ├── components │ │ ├── product_card_component.rb │ │ ├── image_component.rb │ │ ├── link_to_cart_component.rb │ │ └── filter_component.rb │ └── mailers │ │ └── user_mailer.rb ├── vendor │ └── assets │ │ ├── images │ │ └── spree │ │ │ └── frontend │ │ │ └── .gitkeep │ │ ├── stylesheets │ │ └── spree │ │ │ └── frontend │ │ │ └── all.css │ │ └── javascripts │ │ └── spree │ │ └── frontend │ │ └── all.js ├── public │ ├── storefront_favicon.ico │ └── storefront_favicon.svg ├── spec │ ├── support │ │ └── solidus_starter_frontend │ │ │ ├── system │ │ │ └── assets.rb │ │ │ ├── database_cleaner.rb │ │ │ ├── shared_contexts │ │ │ ├── checkout_setup.rb │ │ │ ├── featured_products.rb │ │ │ └── locales.rb │ │ │ └── capybara.rb │ ├── helpers │ │ ├── taxon_filters_helper_spec.rb │ │ ├── taxons_helper_spec.rb │ │ ├── spree │ │ │ └── base_helper_spec.rb │ │ └── checkout_helper_spec.rb │ ├── requests │ │ ├── taxons_spec.rb │ │ ├── cart_line_items_ability_spec.rb │ │ ├── carts_ability_spec.rb │ │ ├── home_spec.rb │ │ ├── checkout_with_views_spec.rb │ │ ├── current_order_tracking_spec.rb │ │ ├── products_spec.rb │ │ ├── orders_ability_spec.rb │ │ ├── user_update_spec.rb │ │ └── locale_spec.rb │ ├── system │ │ ├── locale_spec.rb │ │ ├── currency_spec.rb │ │ ├── authentication │ │ │ ├── sign_out_spec.rb │ │ │ ├── confirmation_spec.rb │ │ │ ├── change_email_spec.rb │ │ │ ├── sign_up_spec.rb │ │ │ ├── sign_in_spec.rb │ │ │ └── password_reset_spec.rb │ │ ├── caching │ │ │ ├── taxons_spec.rb │ │ │ └── products_spec.rb │ │ ├── checkout_unshippable_spec.rb │ │ ├── promotion_code_invalidation_spec.rb │ │ ├── automatic_promotion_adjustments_spec.rb │ │ ├── order_spec.rb │ │ └── first_order_promotion_spec.rb │ ├── components │ │ ├── product_card_component_spec.rb │ │ ├── filter_component_spec.rb │ │ ├── link_to_cart_component_spec.rb │ │ └── image_component_spec.rb │ ├── controllers │ │ ├── checkout_sessions_controller_spec.rb │ │ ├── taxons_controller_spec.rb │ │ ├── user_passwords_controller_spec.rb │ │ └── spree │ │ │ └── base_controller_spec.rb │ └── mailers │ │ └── user_mailer_spec.rb └── config │ └── initializers │ └── solidus_auth_devise_unauthorized_redirect.rb ├── .rspec ├── .dockerdev ├── .psqlrc └── Dockerfile ├── .eslintrc ├── bin ├── rails ├── setup ├── dev ├── rspec ├── sync ├── rails-sandbox ├── rake ├── guard └── _guard-core ├── Guardfile ├── Procfile.dev ├── .gem_release.yml ├── .scss-lint.yml ├── Rakefile ├── codecov.yml ├── Gemfile ├── .gitignore ├── .hound.yml ├── .rubocop.yml ├── .erb-lint.yml ├── coverage.rb └── docker-compose.yml /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /templates/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /templates/app/views/shared/auth/unauthorized.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/vendor/assets/images/spree/frontend/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerdev/.psqlrc: -------------------------------------------------------------------------------- 1 | \set HISTFILE ~/history/psql_history 2 | -------------------------------------------------------------------------------- /templates/app/assets/stylesheets/screen.scss: -------------------------------------------------------------------------------- 1 | @import 'fonts'; 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | exec "#{__dir__}/rails-sandbox", *ARGV 4 | -------------------------------------------------------------------------------- /templates/app/views/orders/payment_info/_check.html.erb: -------------------------------------------------------------------------------- 1 | <%= t('spree.check') %> 2 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/payment/_check.html.erb: -------------------------------------------------------------------------------- 1 | <%= payment_method.description %> 2 | -------------------------------------------------------------------------------- /templates/app/views/orders/payment_info/_default.html.erb: -------------------------------------------------------------------------------- 1 | <%= payment.payment_method.name %> 2 | -------------------------------------------------------------------------------- /templates/app/views/shared/cart/_link_to_cart.html.erb: -------------------------------------------------------------------------------- 1 | <%= render(LinkToCartComponent.new) %> 2 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/_terms_and_conditions.en.html.erb: -------------------------------------------------------------------------------- 1 |

Put your terms and conditions here

2 | -------------------------------------------------------------------------------- /templates/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :shell do 2 | watch %r{^templates/(.*)$} do 3 | system("#{__dir__}/bin/sync") 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: env RUBY_DEBUG_OPEN=true bin/rails server 2 | watch: bin/guard -i 3 | css: bin/rails tailwindcss:watch 4 | -------------------------------------------------------------------------------- /templates/public/storefront_favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio/solidus_starter_frontend/HEAD/templates/public/storefront_favicon.ico -------------------------------------------------------------------------------- /templates/app/helpers/taxons_helper.rb: -------------------------------------------------------------------------------- 1 | module TaxonsHelper 2 | def taxon_seo_url(taxon) 3 | nested_taxons_path(taxon.permalink) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | gem install bundler --conservative 7 | bundle update 8 | bin/rake clobber 9 | -------------------------------------------------------------------------------- /.gem_release.yml: -------------------------------------------------------------------------------- 1 | bump: 2 | recurse: false 3 | file: 'lib/solidus_starter_frontend/version.rb' 4 | message: Bump SolidusStarterFrontend to %{version} 5 | tag: true 6 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_header.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= t('spree.shopping_cart') %> 4 |

5 |
6 | -------------------------------------------------------------------------------- /templates/app/views/orders/payment_info/_gateway.html.erb: -------------------------------------------------------------------------------- 1 | <% source = payment.source %> 2 | 3 | <% if source.last_digits %> 4 | <%= t('spree.ending_in') %> <%= source.last_digits %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /templates/app/helpers/orders_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrdersHelper 4 | def order_just_completed?(order) 5 | flash[:order_completed] && order.present? 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /templates/app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | -------------------------------------------------------------------------------- /templates/app/views/products/_product_info.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= product_description(product) rescue t('spree.product_has_no_description') %> 3 |

4 | -------------------------------------------------------------------------------- /templates/app/assets/config/solidus_starter_frontend_manifest.js: -------------------------------------------------------------------------------- 1 | //= link solidus_starter_frontend.js 2 | //= link solidus_starter_frontend.css 3 | //= link tailwind.css 4 | //= link_tree ../../javascript .js 5 | -------------------------------------------------------------------------------- /templates/app/assets/stylesheets/fonts/inter/inter-v18-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio/solidus_starter_frontend/HEAD/templates/app/assets/stylesheets/fonts/inter/inter-v18-latin-500.woff2 -------------------------------------------------------------------------------- /templates/app/assets/stylesheets/fonts/inter/inter-v18-latin-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio/solidus_starter_frontend/HEAD/templates/app/assets/stylesheets/fonts/inter/inter-v18-latin-600.woff2 -------------------------------------------------------------------------------- /templates/app/assets/stylesheets/fonts/inter/inter-v18-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio/solidus_starter_frontend/HEAD/templates/app/assets/stylesheets/fonts/inter/inter-v18-latin-regular.woff2 -------------------------------------------------------------------------------- /templates/app/controllers/checkout_sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CheckoutSessionsController < CheckoutBaseController 4 | def new 5 | @user = Spree::User.new 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /templates/app/views/user_mailer/confirmation_instructions.text.erb: -------------------------------------------------------------------------------- 1 | Welcome <%= @email %>! 2 | 3 | You can confirm your account email through the link below: 4 | 5 | <%= link_to 'Confirm my account', @confirmation_url %> -------------------------------------------------------------------------------- /.scss-lint.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | LeadingZero: 3 | enabled: true 4 | style: exclude_zero # or 'include_zero' 5 | VendorPrefix: 6 | enabled: false 7 | NestingDepth: 8 | enabled: true 9 | max_depth: 6 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/clean' 2 | 3 | ::CLOBBER.include "#{__dir__}/sandbox" 4 | 5 | task :spec do 6 | require 'bundler' 7 | Bundler.with_unbundled_env { sh 'bin/rails spec' } 8 | end 9 | 10 | task default: :spec 11 | -------------------------------------------------------------------------------- /templates/app/assets/stylesheets/fonts/source-serif/source-serif-4-v8-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio/solidus_starter_frontend/HEAD/templates/app/assets/stylesheets/fonts/source-serif/source-serif-4-v8-latin-500.woff2 -------------------------------------------------------------------------------- /templates/app/assets/stylesheets/fonts/source-serif/source-serif-4-v8-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio/solidus_starter_frontend/HEAD/templates/app/assets/stylesheets/fonts/source-serif/source-serif-4-v8-latin-regular.woff2 -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 1% 7 | base: auto 8 | patch: 9 | default: 10 | target: auto 11 | threshold: 1% 12 | base: auto 13 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/steps/delivery_step/_unshippable_items.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= t('spree.unshippable_items') %> 4 |

5 | 6 | <%= render "checkouts/steps/delivery_step/variants_list", variants: items %> 7 |
8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'rails', ENV['RAILS_VERSION'] || '~> 7.2.0' 6 | 7 | group :development do 8 | gem 'guard' 9 | gem 'guard-shell' 10 | 11 | gem 'codecov' 12 | gem 'simplecov', '~> 0.22' 13 | end 14 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if ! gem list foreman -i --silent; then 4 | echo "Installing foreman..." 5 | gem install foreman 6 | fi 7 | 8 | if ! test -d sandbox; then 9 | bin/sandbox 10 | fi 11 | 12 | export PORT=3000 13 | exec foreman start -f Procfile.dev "$@" 14 | -------------------------------------------------------------------------------- /templates/app/javascript/controllers/locale_selector_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ['selector'] 5 | 6 | submitForm() { 7 | this.selectorTarget.form.submit() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | \#* 3 | *~ 4 | .#* 5 | .DS_Store 6 | .idea 7 | .project 8 | .sass-cache 9 | coverage 10 | tmp 11 | nbproject 12 | pkg 13 | *.swp 14 | spec/dummy 15 | spec/examples.txt 16 | /sandbox 17 | /sandbox-generated 18 | .rvmrc 19 | .ruby-version 20 | .ruby-gemset 21 | .env 22 | Gemfile.lock 23 | -------------------------------------------------------------------------------- /templates/app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } 10 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/_checkout_header.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= t('spree.checkout') %> 4 |

5 |
6 | <%= checkout_progress %> 7 |
8 |
9 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | erblint: 2 | enabled: true 3 | config_file: .erb-lint.yml 4 | 5 | rubocop: 6 | version: 0.75.0 7 | config_file: .rubocop.yml 8 | 9 | scss-lint: 10 | enabled: true 11 | version: 0.57.1 12 | config_file: .scss-lint.yml 13 | 14 | eslint: 15 | enabled: true 16 | config_file: .eslintrc 17 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | File.exist?("#{__dir__}/../sandbox") or system("#{__dir__}/sandbox") 4 | system "#{__dir__}/sync" 5 | 6 | Dir.chdir "#{__dir__}/../sandbox" do 7 | command = ['bundle', 'exec', 'rspec', *ARGV] 8 | warn "$ cd sandbox; #{command.join(' ')}; cd -" 9 | exit system *command 10 | end 11 | -------------------------------------------------------------------------------- /templates/app/controllers/concerns/taxonomies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Taxonomies 4 | extend ActiveSupport::Concern 5 | included do 6 | helper_method :taxonomies 7 | end 8 | 9 | protected 10 | 11 | def taxonomies 12 | @taxonomies ||= Spree::Taxonomy.includes(root: :children) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/existing_payment/_gateway.html.erb: -------------------------------------------------------------------------------- 1 | <%= wallet_payment_source.payment_source.name %>
2 | <%= wallet_payment_source.payment_source.cc_type %> 3 | <%= wallet_payment_source.payment_source.display_number %>
4 | (expires: <%= wallet_payment_source.payment_source.month %>/<%= wallet_payment_source.payment_source.year %>) 5 | -------------------------------------------------------------------------------- /templates/app/controllers/user_confirmations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserConfirmationsController < Devise::ConfirmationsController 4 | protected 5 | 6 | def after_confirmation_path_for(resource_name, resource) 7 | signed_in?(resource_name) ? signed_in_root_path(resource) : login_path 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /templates/app/views/solidus_paypal_commerce_platform/cart/_cart_buttons.html.erb: -------------------------------------------------------------------------------- 1 | <% Spree::PaymentMethod.available_to_users.select{ |payment_method| payment_method.try(:display_on_cart) }.each do |payment_method| %> 2 | <%= render partial: "spree/orders/payment/#{payment_method.cart_partial_name}", locals: {payment_method: payment_method} %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - .rubocop_todo.yml 3 | 4 | AllCops: 5 | Exclude: 6 | - bin/* 7 | - sandbox/**/* 8 | - spec/dummy/**/* 9 | - vendor/**/* 10 | 11 | Documentation: 12 | Enabled: false 13 | 14 | Metrics/BlockLength: 15 | Exclude: 16 | - spec/**/* 17 | 18 | Metrics/LineLength: 19 | Enabled: true 20 | Max: 175 21 | -------------------------------------------------------------------------------- /templates/spec/support/solidus_starter_frontend/system/assets.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.when_first_matching_example_defined(type: :system) do 3 | config.before(:suite) do 4 | system 'bin/rails tailwindcss:build' or abort 'Failed to build Tailwind CSS' 5 | Rails.application.precompiled_assets 6 | end 7 | end 8 | end 9 | 10 | -------------------------------------------------------------------------------- /templates/app/javascript/controllers/drawer_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["drawer"]; 5 | 6 | toggle() { 7 | this.drawerTarget.classList.toggle("-translate-x-full"); 8 | document.getElementById("overlay").classList.toggle("hidden"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/app/overrides/taxon_custom_queries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TaxonCustomQueries 4 | def featured(limit = 3) 5 | all_products.featured.order('RANDOM()').limit(limit) 6 | end 7 | 8 | def all_products_except(product_ids) 9 | all_products.where.not(id: product_ids) 10 | end 11 | 12 | Spree::Taxon.prepend self 13 | end 14 | -------------------------------------------------------------------------------- /templates/app/views/solidus_paypal_commerce_platform/product/_product_buttons.html.erb: -------------------------------------------------------------------------------- 1 | <% Spree::PaymentMethod.available_to_users.select{ |payment_method| payment_method.try(:display_on_product_page) }.each do |payment_method| %> 2 | <%= render partial: "spree/products/payment/#{payment_method.product_page_partial_name}", locals: {payment_method: payment_method} %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /templates/app/helpers/checkout_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CheckoutHelper 4 | def partial_name_with_fallback(prefix, partial_name, fallback_name = 'default') 5 | if lookup_context.find_all("#{prefix}/_#{partial_name}").any? 6 | "#{prefix}/#{partial_name}" 7 | else 8 | "#{prefix}/#{fallback_name}" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /bin/sync: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # We don't run rsync with --delete because this would delete files in the 4 | # sandbox that are not in the templates. These would be files installed by 5 | # `rails new` and by `solidus:install` prior to installing the frontend 6 | # files from the templates. 7 | rsync \ 8 | --archive \ 9 | --verbose \ 10 | templates/ \ 11 | sandbox 12 | 13 | -------------------------------------------------------------------------------- /templates/app/views/products/_products_by_taxon.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= link_to products_by_taxon.name, taxon_seo_url(products_by_taxon) %> 4 | 5 | 6 | <%= render 'products/products', products: taxon_preview(products_by_taxon), taxon: products_by_taxon %> 7 |
8 | -------------------------------------------------------------------------------- /templates/app/views/solidus_paypal_commerce_platform/payments/_payment.html.erb: -------------------------------------------------------------------------------- 1 | <% if payment.source.respond_to?(:paypal_funding_source) %> 2 |
3 | <%= t('paypal_funding', funding: payment.source.display_paypal_funding_source) %> 4 | <% end %> 5 | <% if payment.source.respond_to?(:paypal_email) %> 6 |
7 | <%= t('payment_email', email: payment.source.paypal_email) %> 8 | <% end %> 9 | -------------------------------------------------------------------------------- /templates/app/helpers/taxon_filters_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spree/core/product_filters' 4 | 5 | module TaxonFiltersHelper 6 | def applicable_filters_for(_taxon) 7 | [:brand_filter, :price_filter].map do |filter_name| 8 | Spree::Core::ProductFilters.send(filter_name) if Spree::Core::ProductFilters.respond_to?(filter_name) 9 | end.compact 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /bin/rails-sandbox: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | app_root = 'sandbox' 4 | 5 | unless File.exist? "#{app_root}/bin/rails" 6 | warn "Creating the #{app_root} app..." 7 | Dir.chdir "#{__dir__}/.." do 8 | system "#{__dir__}/sandbox" or begin 9 | warn "Automatic creation of the #{app_root} app failed" 10 | exit 1 11 | end 12 | end 13 | end 14 | 15 | Dir.chdir app_root 16 | exec 'bin/rails', *ARGV 17 | -------------------------------------------------------------------------------- /templates/spec/helpers/taxon_filters_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe TaxonFiltersHelper, type: :helper do 6 | let(:taxon) { nil } 7 | subject { applicable_filters_for(taxon) } 8 | 9 | it "returns the price/brand filters" do 10 | expect(subject.map { |y| y[:name] }).to eq ['Brands', 'Price Range'] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /templates/vendor/assets/stylesheets/spree/frontend/all.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll automatically include all the stylesheets available in this directory 3 | * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at 4 | * the top of the compiled file, but it's generally better to create a new file per style scope. 5 | * 6 | *= require_self 7 | *= require_tree . 8 | */ 9 | -------------------------------------------------------------------------------- /templates/app/javascript/controllers/cart_page_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ['updateButton'] 5 | 6 | setQuantityToZero(e) { 7 | this.element.querySelector(`#${e.params.quantityId}`).value = '0' 8 | } 9 | 10 | disableUpdateButton() { 11 | this.updateButtonTarget.setAttribute('disabled', true) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /templates/app/views/orders/_order_items.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | base_class = "order-items".freeze 3 | 4 | # Optional props 5 | classes = local_assigns.fetch(:classes, []) 6 | 7 | # Classes 8 | class_names = classes.push(base_class).join(" ") 9 | %> 10 | 11 |
12 | <% order.line_items.each do |item| %> 13 | <%= render 'orders/order_item', item: item %> 14 | <% end %> 15 |
16 | -------------------------------------------------------------------------------- /templates/app/views/user_mailer/reset_password_instructions.text.erb: -------------------------------------------------------------------------------- 1 | A request to reset your password has been made. 2 | If you did not make this request, simply ignore this email. 3 | 4 | If you did make this request just click the link below: 5 | 6 | <%= @edit_password_reset_url %> 7 | 8 | If the above URL does not work try copying and pasting it into your browser. 9 | If you continue to have problems please feel free to contact us. 10 | 11 | -------------------------------------------------------------------------------- /templates/app/overrides/product_featured_similar_products.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProductFeaturedSimilarProducts 4 | def self.prepended(base) 5 | base.scope :featured, -> { where(featured: true) } 6 | end 7 | 8 | def similar_products(limit = 3) 9 | taxons.map do |taxon| 10 | taxon.all_products_except(self.id) 11 | end.flatten.uniq.first(limit) 12 | end 13 | 14 | Spree::Product.prepend self 15 | end 16 | -------------------------------------------------------------------------------- /templates/app/controllers/store_devise_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class StoreDeviseController < ApplicationController 4 | helper 'spree/base', 'spree/store' 5 | 6 | include Spree::Core::ControllerHelpers::Auth 7 | include Spree::Core::ControllerHelpers::Common 8 | include Spree::Core::ControllerHelpers::Order 9 | include Spree::Core::ControllerHelpers::Store 10 | include Taxonomies 11 | 12 | layout 'storefront' 13 | end 14 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_adjustment.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | type = local_assigns.fetch(:type, t('spree.adjustment')) 3 | label = local_assigns.fetch(:label, '') 4 | 5 | adjustments_sum = adjustments.sum(&:amount) 6 | %> 7 | 8 | <% if adjustments_sum != 0 %> 9 | <%= render( 10 | "carts/cart_amount_row", 11 | type: type, 12 | label: label, 13 | amount: Spree::Money.new(adjustments_sum, currency: @order.currency) 14 | ) %> 15 | <% end %> 16 | -------------------------------------------------------------------------------- /templates/app/views/solidus_paypal_commerce_platform/shared/_javascript_sdk_tag.html.erb: -------------------------------------------------------------------------------- 1 | <% currency = @order ? @order.currency : current_pricing_options.currency %> 2 | 3 | 9 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_empty.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= t('spree.your_cart_is_empty') %> 4 |

5 | 6 |

7 | You have no items in your shopping cart. 8 |

9 | 10 | <%= render partial: "shared/call_to_action", :locals => { :label => t('spree.continue_shopping'), :url => products_path } %> 11 |
12 | -------------------------------------------------------------------------------- /templates/app/views/address/_form_hidden.html.erb: -------------------------------------------------------------------------------- 1 | <%= form.hidden_field :name %> 2 | <%= form.hidden_field :company %> 3 | <%= form.hidden_field :address1 %> 4 | <%= form.hidden_field :address2 %> 5 | <%= form.hidden_field :city %> 6 | <%= form.hidden_field :country_id %> 7 | <%= form.hidden_field :state_id %> 8 | <%= form.hidden_field :state_name %> 9 | <%= form.hidden_field :zipcode %> 10 | <%= form.hidden_field :phone %> 11 | <%= form.hidden_field :alternative_phone %> 12 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_amount_row.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | type = local_assigns.fetch(:type, t('spree.adjustment')) 3 | label = local_assigns.fetch(:label, '') 4 | %> 5 | 6 |
7 |
8 | <%= type %>: 9 | <%= label %> 10 |
11 |
<%= amount %>
12 |
13 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/steps/delivery_step/_variants_list.html.erb: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /templates/app/views/products/_products_grid.html.erb: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /templates/app/views/products/_product_image.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= render(ImageComponent.new( 4 | image: product.gallery.images.first, 5 | size: :product, 6 | itemprop: "image", 7 | data: { js: 'product-main-image' }, 8 | class: 'object-contain' 9 | )) %> 10 | 11 |
12 | -------------------------------------------------------------------------------- /templates/app/views/products/_product_taxons.html.erb: -------------------------------------------------------------------------------- 1 | <% if !product.taxons.blank? %> 2 |
3 |

4 | <%= t('spree.look_for_similar_items') %>: 5 |

6 | 7 | 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /templates/vendor/assets/javascripts/spree/frontend/all.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into including all the files listed below. 2 | // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically 3 | // be included in the compiled file accessible from http://example.com/assets/application.js 4 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 5 | // the compiled file. 6 | // 7 | //= require rails-ujs 8 | //= require_tree . 9 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/steps/delivery_step/_shipping_methods.html.erb: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /templates/spec/helpers/taxons_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe TaxonsHelper, type: :helper do 6 | describe '#taxon_seo_url' do 7 | let(:taxonomy) { create(:taxonomy, name: 'Categories') } 8 | let(:taxon) { create(:taxon, name: 'Clothing', taxonomy: taxonomy) } 9 | 10 | it 'is the nested taxons path for the taxon' do 11 | expect(taxon_seo_url(taxon)).to eq("/t/categories/clothing") 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /templates/app/javascript/controllers/top_bar_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ['cartLink'] 5 | static values = { 6 | cartUrl: { type: String, default: Solidus.pathFor('cart_link') } 7 | } 8 | 9 | async updateCartLink() { 10 | const response = await fetch(this.cartUrlValue) 11 | this.cartLinkTarget.innerHTML = await response.text() 12 | } 13 | 14 | connect() { 15 | this.updateCartLink() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /templates/spec/requests/taxons_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Taxon', type: :request, with_signed_in_user: true do 6 | let(:user) { create(:admin_user) } 7 | 8 | it "provides the current user to the searcher class" do 9 | taxon = create(:taxon, permalink: "test") 10 | get nested_taxons_path(taxon.permalink) 11 | 12 | expect(assigns[:searcher].current_user).to eq user 13 | expect(response.status).to eq(200) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_secondary_actions.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | <%= order_form.button( 7 | t("spree.update"), 8 | class: 'body-md cursor-pointer', 9 | id: 'update-button', 10 | 'data-cart-page-target': 'updateButton', 11 | ) %> 12 |
13 |
14 | -------------------------------------------------------------------------------- /templates/spec/helpers/spree/base_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe Spree::BaseHelper, type: :helper do 6 | # Regression test for https://github.com/spree/spree/issues/2759 7 | it "nested_taxons_path works with a Taxon object" do 8 | taxonomy = create(:taxonomy, name: 'smartphone') 9 | taxon = create(:taxon, taxonomy: taxonomy, name: "iphone") 10 | 11 | expect(nested_taxons_path(taxon)).to eq("/t/smartphone/iphone") 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /templates/app/controllers/checkout_guest_sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CheckoutGuestSessionsController < CheckoutBaseController 4 | def create 5 | if params[:order][:email] =~ Devise.email_regexp && current_order.update(email: params[:order][:email]) 6 | redirect_to checkout_path 7 | else 8 | flash[:registration_error] = t(:email_is_invalid, scope: [:errors, :messages]) 9 | @user = Spree::User.new 10 | render template: 'checkout_sessions/new' 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /templates/app/views/checkout_sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'shared/error_messages', target: @user %> 2 | 3 |
4 |
5 | <%= render template: 'user_sessions/new' %> 6 |
7 | 8 | <% if Spree::Config[:allow_guest_checkout] %> 9 |
10 | <%= render 'checkout_guest_sessions/form' %> 11 |
12 | <% end %> 13 |
14 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/_checkout_step.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for order, url: update_checkout_path(order.state), html: { id: "checkout_form_#{order.state}" } do |form| %> 2 | <% if order.state == "address" || !order.email? %> 3 |
4 | <%= form.label :email, 'Customer email' %> 5 | <%= form.email_field :email, required: true, placeholder: 'name@example.com' %> 6 |
7 | <% end %> 8 | 9 | <%= render "checkouts/steps/#{order.state}_step", form: form, differentiator: @differentiator %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /templates/app/views/shared/search/_filters.html.erb: -------------------------------------------------------------------------------- 1 | <% filters = applicable_filters_for(taxon) %> 2 | 3 | <% unless filters.empty? %> 4 | <%= form_tag '', method: :get, id: 'sidebar_products_search', class: "mt-8" do %> 5 | <%= hidden_field_tag 'per_page', params[:per_page] %> 6 | 7 | <% filters.each do |filter| %> 8 | <%= render(FilterComponent.new(filter: filter, search_params: params[:search])) %> 9 | <% end %> 10 | 11 | <%= button_tag t("spree.search"), class: 'button-primary button-primary--full-width mt-6' %> 12 | <% end %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /templates/spec/support/solidus_starter_frontend/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.use_transactional_fixtures = false 3 | config.before :suite do 4 | DatabaseCleaner.clean_with :truncation 5 | end 6 | 7 | # Around each spec check if it is a Javascript test and switch between using 8 | # database transactions or not where necessary. 9 | config.around(:each) do |example| 10 | DatabaseCleaner.strategy = RSpec.current_example.metadata[:js] ? :truncation : :transaction 11 | DatabaseCleaner.cleaning { example.run } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /templates/app/views/shared/_error_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% if target && target.errors.any? %> 2 |
3 |
4 |

<%= t('spree.errors_prohibited_this_record_from_being_saved', count: target.errors.count) %>:

5 |

<%= t('spree.there_were_problems_with_the_following_fields') %>:

6 | 11 |
12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_footer.html.erb: -------------------------------------------------------------------------------- 1 | <% order = order_form.object %> 2 | 3 | 17 | -------------------------------------------------------------------------------- /templates/spec/support/solidus_starter_frontend/shared_contexts/checkout_setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'checkout setup' do 4 | let!(:store) { create(:store) } 5 | let!(:country) { create(:country, states_required: true) } 6 | let!(:state) { create(:state, country: country) } 7 | let!(:shipping_method) { create(:shipping_method) } 8 | let!(:stock_location) { create(:stock_location) } 9 | let!(:mug) { create(:product, name: "Solidus mug set") } 10 | let!(:payment_method) { create(:check_payment_method) } 11 | let!(:zone) { create(:zone) } 12 | end 13 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_item_remove.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= order_form.submit( 5 | 'Remove', 6 | class: "underline text-body-sm cursor-pointer", 7 | id: "delete_#{dom_id(line_item)}", 8 | data: { disable_with: 'Remove' }, 9 | 'data-action': 'click->cart-page#setQuantityToZero', 10 | # FIXME: `index: nil` is required because otherwise rails will double the index 11 | 'data-cart-page-quantity-id-param': item_form.field_id(:quantity, index: nil), 12 | ) %> -------------------------------------------------------------------------------- /.erb-lint.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | Rubocop: 3 | enabled: true 4 | rubocop_config: 5 | inherit_from: 6 | - .rubocop.yml 7 | Layout/InitialIndentation: 8 | Enabled: false 9 | Layout/TrailingBlankLines: 10 | Enabled: false 11 | Layout/TrailingWhitespace: 12 | Enabled: false 13 | Naming/FileName: 14 | Enabled: false 15 | Style/FrozenStringLiteralComment: 16 | Enabled: false 17 | Metrics/LineLength: 18 | Enabled: false 19 | Lint/UselessAssignment: 20 | Enabled: false 21 | Rails/OutputSafety: 22 | Enabled: false 23 | -------------------------------------------------------------------------------- /templates/public/storefront_favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /templates/app/assets/images/logo_small_no_label.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /templates/app/views/cart_line_items/_product_availability.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | base_class = "product-availability".freeze 3 | 4 | # Optional props 5 | classes = local_assigns.fetch(:classes, []) 6 | 7 | # Classes 8 | class_names = classes.push(base_class).join(" ") 9 | %> 10 | 11 | <% if product.master.can_supply? %> 12 | 13 | <% elsif product.variants.empty? %> 14 | 19 | <%= t('spree.out_of_stock') %> 20 | 21 | <% end %> 22 | -------------------------------------------------------------------------------- /templates/app/assets/javascripts/solidus_starter_frontend.js: -------------------------------------------------------------------------------- 1 | // Placeholder manifest file. 2 | // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js' 3 | 4 | // spree/frontend/all points to the 5 | // `vendor/assets/javascripts/spree/frontend/all.js` file generated by 6 | // `solidus:install`. See `setup_assets` at 7 | // https://github.com/solidusio/solidus/blob/main/core/lib/generators/solidus/install/install_generator.rb 8 | //= require spree/frontend/all 9 | 10 | //= require utils 11 | //= require checkout 12 | //= require product 13 | //= require product_selection_component 14 | -------------------------------------------------------------------------------- /templates/app/controllers/orders_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OrdersController < StoreController 4 | helper 'spree/products', 'orders' 5 | 6 | respond_to :html 7 | 8 | before_action :store_guest_token 9 | 10 | def show 11 | @order = Spree::Order.find_by!(number: params[:id]) 12 | authorize! :show, @order, cookies.signed[:guest_token] 13 | end 14 | 15 | private 16 | 17 | def accurate_title 18 | t('spree.order_number', number: @order.number) 19 | end 20 | 21 | def store_guest_token 22 | cookies.permanent.signed[:guest_token] = params[:token] if params[:token] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_items.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | order = order_form.object 3 | 4 | is_order_empty = ( 5 | order.adjustments.nonzero.exists? || 6 | order.line_item_adjustments.nonzero.exists? || 7 | order.shipment_adjustments.nonzero.exists? || 8 | order.shipments.any? 9 | ) 10 | %> 11 | 12 |
13 | <%= render( 14 | partial: 'carts/cart_item', 15 | collection: order.line_items, 16 | as: :line_item, 17 | locals: { order_form: order_form } 18 | ) %> 19 | 20 | <%# if is_order_empty %> 21 | <%#= render 'carts/cart_adjustments' %> 22 | <%# end %> 23 |
24 | -------------------------------------------------------------------------------- /templates/app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'featured_products', { products: @featured_products } if @featured_products.try(:any?) %> 2 | <%= render 'collection', { title: "Shop Collection", products: @collection_products } if @collection_products.try(:any?) %> 3 | <%= render 'collections_with_call_to_action', { products: @cta_collection_products[0..1] } if @cta_collection_products.try(:any?) %> 4 | <%= render 'featured_product_banner', { product: @cta_collection_products[2] } if @cta_collection_products && @cta_collection_products.size > 2 %> 5 | <%= render 'collection', { title: "New Arrivals", products: @new_arrivals } if @new_arrivals.try(:any?) %> 6 | -------------------------------------------------------------------------------- /templates/spec/support/solidus_starter_frontend/shared_contexts/featured_products.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context "featured products" do 4 | before(:each) do 5 | create(:store) 6 | categories = create(:taxonomy, name: 'Categories') 7 | categories_root = categories.root 8 | clothing_taxon = create(:taxon, name: 'Clothing', parent_id: categories_root.id, taxonomy: categories) 9 | image = create(:image) 10 | variant = create(:variant, images: [image, image]) 11 | 12 | create(:custom_product, name: 'Solidus hoodie', price: '29.99', taxons: [clothing_taxon], variants: [variant]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /templates/app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap under controllers/* 2 | 3 | import { application } from "controllers/application" 4 | 5 | // Eager load all controllers defined in the import map under controllers/**/*_controller 6 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 7 | eagerLoadControllersFrom("controllers", application) 8 | 9 | // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) 10 | // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" 11 | // lazyLoadControllersFrom("controllers", application) 12 | -------------------------------------------------------------------------------- /templates/spec/support/solidus_starter_frontend/shared_contexts/locales.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context 'fr locale' do 4 | before do 5 | I18n.available_locales = [:en, :fr] 6 | I18n.backend.store_translations(:fr, spree: { 7 | i18n: { this_file_language: "Français" }, 8 | cart: 'Panier', 9 | shopping_cart: 'Panier', 10 | locale_changed: 'Paramètres régionaux changés', 11 | your_cart_is_empty: 'Votre panier est vide' 12 | }) 13 | end 14 | 15 | after do 16 | I18n.available_locales = [:en] 17 | I18n.locale = :en # reset locale after each spec. 18 | I18n.reload! 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /templates/spec/system/locale_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'setting locale', type: :system do 6 | include_context 'fr locale' 7 | 8 | let!(:store) { create(:store) } 9 | 10 | context 'shopping cart link and page', :js do 11 | it 'should be in french' do 12 | visit root_path 13 | 14 | expect(page).to have_link('Cart') 15 | select('Français', from: 'Language:') 16 | expect(page).to have_content('Paramètres régionaux changés') 17 | click_link 'Panier' 18 | expect(page).to have_content('Votre panier est vide') 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /templates/spec/helpers/checkout_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe CheckoutHelper, type: :helper do 6 | describe '#partial_name_with_fallback' do 7 | it "uses the partial_name if it exists" do 8 | expect( 9 | partial_name_with_fallback('orders/payment_info', 'gateway', 'default') 10 | ).to eq('orders/payment_info/gateway') 11 | end 12 | 13 | it "uses the fallback_name if it's missing" do 14 | expect( 15 | partial_name_with_fallback('orders/payment_info', 'foo', 'default') 16 | ).to eq('orders/payment_info/default') 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /templates/app/views/autocomplete_results/index.turbo_stream.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_stream.replace 'autocomplete-results' do %> 2 | 5 | <% end %> 6 | 7 | <%= turbo_stream.replace 'autocomplete-results-mobile' do %> 8 |
9 | <%= render 'autocomplete_results', results: @results %> 10 |
11 | <% end %> 12 | -------------------------------------------------------------------------------- /templates/app/views/home/_featured_products.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= render 'products/featured_product_card', { product: products[0], size: :big } %> 5 |
6 |
7 | <% products.each_with_index do |product, index| %> 8 | <% if index > 0 %> 9 |
10 | <%= render 'products/featured_product_card', { product: product, size: :small } %> 11 |
12 | <% end %> 13 | <% end %> 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /templates/app/controllers/locale_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class LocaleController < StoreController 4 | def set 5 | requested_locale = params[:switch_to_locale] || params[:locale] 6 | 7 | if locale_is_available?(requested_locale) 8 | I18n.locale = requested_locale 9 | session[set_user_language_locale_key] = requested_locale 10 | flash.notice = t('spree.locale_changed') 11 | else 12 | flash[:error] = t('spree.locale_not_changed') 13 | end 14 | 15 | redirect_to root_path 16 | end 17 | 18 | private 19 | 20 | def locale_is_available?(locale) 21 | locale && Spree.i18n_available_locales.include?(locale.to_sym) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /templates/app/javascript/controllers/theme_switcher_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["themeSwitch"] 5 | 6 | toggleTheme() { 7 | const lsTheme = localStorage.getItem('theme') || 'light' 8 | let currentTheme 9 | 10 | if (lsTheme === 'light') { 11 | currentTheme = 'dark' 12 | document.documentElement.classList.remove('light') 13 | } else { 14 | currentTheme = 'light' 15 | document.documentElement.classList.remove('dark') 16 | } 17 | 18 | document.documentElement.classList.add(currentTheme) 19 | localStorage.setItem('theme', currentTheme) 20 | } 21 | } -------------------------------------------------------------------------------- /templates/app/views/products/_featured_product_card.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% image = product.gallery.images.second || product.gallery.images.first %> 3 | <%= render(ImageComponent.new( 4 | image: image, 5 | size: size, 6 | class: 'w-full object-cover' 7 | )) %> 8 |
9 | <%= product.name %> 10 | <%= render partial: "shared/call_to_action", :locals => { :label => "Shop now", :url => product } %> 11 |
12 |
13 | -------------------------------------------------------------------------------- /templates/app/assets/javascripts/utils.js.erb: -------------------------------------------------------------------------------- 1 | window.Solidus = {}; 2 | 3 | Solidus.mountedAt = () => { 4 | return '<%= Rails.application.routes.url_helpers.spree_path(trailing_slash: true) %>'; 5 | }; 6 | 7 | Solidus.pathFor = (path) => { 8 | const hostname = window.location.hostname; 9 | const protocol = window.location.protocol; 10 | const port = window.location.port; 11 | const locationOrigin = protocol + '//' + hostname + (port ? ':' + port : ''); 12 | return locationOrigin + Solidus.mountedAt() + path; 13 | }; 14 | 15 | Solidus.routes = { 16 | states_search: Solidus.pathFor('api/states'), 17 | apply_coupon_code: (order_id) => Solidus.pathFor('api/orders/' + order_id + '/coupon_codes') 18 | }; 19 | -------------------------------------------------------------------------------- /templates/app/views/cart_line_items/_product_submit.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render 'cart_line_items/product_availability', product: product %> 3 | 4 |
5 |
6 | <%= number_field_tag "quantity", 1, step: 1, class: 'min-w-[100px]', :onchange => "addToCartButtonPriceChangeHandler('#{product.price}')" %> 7 |
8 | <% cta_label = content_tag(:span, "#{t('spree.add_to_cart')} — ") + content_tag(:span, display_price(product), id: 'cta_price') %> 9 | <%= render partial: "shared/call_to_action", :locals => { :label => cta_label, :id => 'add-to-cart-button', :url => nil } %> 10 |
11 |
12 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render "shared/error_messages", target: @order %> 3 | <%= render "checkout_header" %> 4 | 5 |
6 |
"> 7 | <%= render "checkout_step", order: @order %> 8 |
9 | 10 | <% if @order.state != "confirm" %> 11 |
12 | <%= render "checkout_summary", order: @order %> 13 |
14 | <% end %> 15 |
16 |
17 | -------------------------------------------------------------------------------- /templates/spec/system/currency_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Switching currencies in backend', type: :system do 6 | include_context 'featured products' 7 | 8 | before do 9 | create(:store) 10 | create(:base_product, name: "Solidus mug set") 11 | end 12 | 13 | # Regression test for https://github.com/spree/spree/issues/2340 14 | it "does not cause current_order to become nil" do 15 | visit products_path 16 | click_link "Solidus mug set" 17 | click_button "Add To Cart" 18 | # Now that we have an order... 19 | stub_spree_preferences(currency: "AUD") 20 | visit root_path 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /templates/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HomeController < StoreController 4 | helper 'spree/products' 5 | respond_to :html 6 | 7 | def index 8 | @searcher = build_searcher(params.merge(include_images: true)) 9 | @products = @searcher.retrieve_products 10 | 11 | # Split products into groups of 3 for the homepage blocks. 12 | # You probably want to remove this logic and use your own! 13 | homepage_groups = @products.in_groups_of(3, false) 14 | @featured_products = homepage_groups[0] 15 | @collection_products = homepage_groups[1] 16 | @cta_collection_products = homepage_groups[2] 17 | @new_arrivals = homepage_groups[3] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /templates/config/initializers/solidus_auth_devise_unauthorized_redirect.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.to_prepare do 2 | Spree::BaseController.unauthorized_redirect = -> do 3 | if spree_current_user 4 | flash[:error] = I18n.t('spree.authorization_failure') 5 | 6 | if Spree::Auth::Engine.redirect_back_on_unauthorized? 7 | redirect_back(fallback_location: unauthorized_path) 8 | else 9 | redirect_to unauthorized_path 10 | end 11 | else 12 | store_location 13 | 14 | if Spree::Auth::Engine.redirect_back_on_unauthorized? 15 | redirect_back(fallback_location: login_path) 16 | else 17 | redirect_to login_path 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /templates/app/assets/stylesheets/solidus_starter_frontend.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll automatically include all the stylesheets available in this directory 3 | * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at 4 | * the top of the compiled file, but it's generally better to create a new file per style scope. 5 | * 6 | * spree/frontend/all points to the 7 | * `vendor/assets/stylesheets/spree/frontend/all.css` file generated by 8 | * `solidus:install`. See `setup_assets` at 9 | * https://github.com/solidusio/solidus/blob/main/core/lib/generators/solidus/install/install_generator.rb 10 | *= require spree/frontend/all 11 | * 12 | *= require screen 13 | */ 14 | -------------------------------------------------------------------------------- /templates/app/components/product_card_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ProductCardComponent < ViewComponent::Base 4 | def initialize( 5 | product, 6 | locale: I18n.locale, 7 | price: product.master.default_price, 8 | additional_classes: '', 9 | home_collection: false 10 | ) 11 | @product = product 12 | @locale = locale 13 | @price = price 14 | @additional_classes = additional_classes 15 | @home_collection = home_collection 16 | end 17 | 18 | attr_reader :product, :locale, :price, :additional_classes 19 | 20 | def main_image 21 | product.gallery.images.first 22 | end 23 | 24 | def display_price 25 | @display_price ||= price&.money 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /templates/app/components/image_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ImageComponent < ViewComponent::Base 4 | attr_reader :image, :size, :classes, :options 5 | 6 | def initialize(local_assigns = {}) 7 | @image = local_assigns.delete(:image) 8 | @size = local_assigns.delete(:size) { :mini } 9 | @classes = local_assigns.delete(:classes) 10 | @options = local_assigns 11 | end 12 | 13 | def call 14 | if image 15 | image_tag image.url(size), default_options.merge(options) 16 | else 17 | content_tag :div, nil, class: ['image-placeholder', size].join(' ') 18 | end 19 | end 20 | 21 | private 22 | 23 | def default_options 24 | { alt: image.alt, class: classes } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /templates/app/helpers/product_options_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProductOptionsHelper 4 | def product_variants_with_options(product) 5 | product.variants_and_option_values_for(current_pricing_options) 6 | end 7 | 8 | def sorted_option_values(variant) 9 | variant.option_values.sort_by { |value| value.option_type.position }.map(&:id) 10 | end 11 | 12 | def sorted_option_types(product) 13 | product.option_types.sort_by(&:position) 14 | end 15 | 16 | # move to model 17 | def option_values(product:, option_type:) 18 | product.variants.map do |variant| 19 | variant.option_values.find { |option_value| option_value.option_type_id == option_type.id } 20 | end.compact.uniq 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /templates/app/controllers/solidus_paypal_commerce_platform/paypal_orders_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusPaypalCommercePlatform 4 | class PaypalOrdersController < ::Spree::Api::BaseController 5 | before_action :load_payment_method 6 | skip_before_action :authenticate_user 7 | 8 | def show 9 | authorize! :show, @order, order_token 10 | order_request = @payment_method.gateway.create_order(@order, @payment_method.auto_capture?) 11 | 12 | render json: order_request, status: order_request.status_code 13 | end 14 | 15 | private 16 | 17 | def load_payment_method 18 | @payment_method = ::Spree::PaymentMethod.find(params.require(:payment_method_id)) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /templates/app/views/products/_product_properties.html.erb: -------------------------------------------------------------------------------- 1 | <% unless product_properties.empty? %> 2 |
3 |
4 |

5 | <%= t('spree.properties')%> 6 |

7 | 8 | 9 | 10 | <% product_properties.each do |product_property| %> 11 | 12 | 13 | 14 | 15 | <% end %> 16 | 17 | <% reset_cycle('properties') %> 18 | 19 |
<%= product_property.property.presentation %><%= product_property.value %>
20 |
21 |
22 | <% end %> 23 | -------------------------------------------------------------------------------- /templates/app/views/users/_users_menu.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /templates/app/views/home/_featured_product_banner.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render(ImageComponent.new( 4 | image: product.gallery.images.last, 5 | size: :full, 6 | class: 'w-full object-cover aspect-square md:aspect-auto' 7 | )) %> 8 |
9 | New 10 |
<%= product.name %>
11 | <%= render "shared/call_to_action", { label: "Shop Now", type: 'secondary', url: product } %> 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /templates/spec/requests/cart_line_items_ability_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Cart line item permissions', type: :request do 6 | let(:order) { create(:order, user: nil, store: store) } 7 | let!(:store) { create(:store) } 8 | let(:variant) { create(:variant) } 9 | 10 | context 'when an order exists in the cookies.signed', with_guest_session: true do 11 | before { order.update(guest_token: nil) } 12 | 13 | context '#create' do 14 | it 'checks if user is authorized for :update' do 15 | post cart_line_items_path, params: { variant_id: variant.id } 16 | expect(response).to redirect_to(login_path) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /templates/app/views/products/payment/_paypal_commerce_platform.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render partial: "solidus_paypal_commerce_platform/shared/javascript_sdk_tag", locals: {payment_method: payment_method} %> 3 | 4 |
5 | 6 |
7 | 8 | 14 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /templates/app/views/products/_product_promotions.html.erb: -------------------------------------------------------------------------------- 1 | <% promotions = product.possible_promotions %> 2 | 3 | <% if promotions.any? %> 4 |
5 |

<%= t('spree.promotions') %>

6 | 7 | <% promotions.each do |promotion| %> 8 |
9 |
10 |

<%= promotion.name %>

11 |
12 | 13 |

<%= promotion.description %>

14 | 15 | <% if promotion.products.any? %> 16 | 21 | <% end %> 22 |
23 | <% end %> 24 |
25 | <% end %> 26 | -------------------------------------------------------------------------------- /templates/app/controllers/taxons_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TaxonsController < StoreController 4 | helper 'spree/taxons', 'spree/products', 'taxon_filters' 5 | 6 | before_action :load_taxon, only: [:show] 7 | 8 | respond_to :html 9 | 10 | def show 11 | @searcher = build_searcher(params.merge(taxon: @taxon.id, include_images: true)) 12 | @products = @searcher.retrieve_products 13 | end 14 | 15 | private 16 | 17 | def load_taxon 18 | @taxon = Spree::Taxon.friendly.find(params[:id]) 19 | redirect_to nested_taxons_path(@taxon), status: :moved_permanently if params[:id] != @taxon.permalink 20 | end 21 | 22 | def accurate_title 23 | if @taxon 24 | @taxon.seo_title 25 | else 26 | super 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /templates/app/views/shared/navigation/_taxonomies.html.erb: -------------------------------------------------------------------------------- 1 | <% max_level = Spree::Config[:max_level_in_taxons_menu] || 1 %> 2 | 3 | 20 | -------------------------------------------------------------------------------- /templates/app/components/link_to_cart_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class LinkToCartComponent < ViewComponent::Base 4 | delegate :current_order, :spree, to: :helpers 5 | 6 | def call 7 | link_to text.html_safe, cart_path, class: "cart-info block #{css_class} w-6 h-6", title: I18n.t('spree.cart') 8 | end 9 | 10 | private 11 | 12 | def text 13 | empty_current_order? ? '' : "" 14 | end 15 | 16 | def css_class 17 | empty_current_order? ? 'empty' : 'full' 18 | end 19 | 20 | def empty_current_order? 21 | current_order.nil? || current_order.item_count.zero? 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/steps/delivery_step/_proposed_shipment.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= t('spree.package_from') %> 4 | <%= ship_form.object.stock_location.name %> 5 |

6 | 7 | <%= render "checkouts/steps/delivery_step/shipment_items", shipment_items: ship_form.object.manifest %> 8 | 9 |
10 |

11 | <%= t('spree.shipping_method') %> 12 |

13 | 14 | <%= render( 15 | "checkouts/steps/delivery_step/shipping_methods", 16 | form: ship_form, 17 | shipping_rates: ship_form.object.shipping_rates 18 | ) %> 19 |
20 |
21 | -------------------------------------------------------------------------------- /templates/app/views/taxons/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 |

<%= @taxon.name %>

8 | 9 | <%= render 'products/products', products: @products, taxon: @taxon %> 10 | 11 | <% unless params[:keywords].present? %> 12 | <% cache [I18n.locale, @taxon] do %> 13 | <%= render partial: 'products/products_by_taxon', collection: @taxon.children %> 14 | <% end %> 15 | <% end %> 16 |
17 |
18 | -------------------------------------------------------------------------------- /templates/app/views/products/_product_header.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= product.name %> 4 |

5 | 6 | <% if product.price_for_options(current_pricing_options)&.money and !product.price.nil? %> 7 |

8 | <%= content_tag( 9 | :span, 10 | display_price(product), 11 | itemprop: 'price', 12 | content: product.price_for_options(current_pricing_options)&.money.to_d, 13 | data: { js: 'price' } 14 | ) %> 15 | 16 | <%= content_tag( 17 | :span, 18 | nil, 19 | itemprop: 'priceCurrency', 20 | content: current_pricing_options.currency 21 | ) %> 22 |

23 | <% end %> 24 |
25 | -------------------------------------------------------------------------------- /templates/app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserMailer < Spree::BaseMailer 4 | def reset_password_instructions(user, token, *_args) 5 | @store = Spree::Store.default 6 | @edit_password_reset_url = edit_spree_user_password_url(reset_password_token: token, host: @store.url) 7 | mail to: user.email, from: from_address(@store), subject: "#{@store.name} #{I18n.t(:subject, scope: [:devise, :mailer, :reset_password_instructions])}" 8 | end 9 | 10 | def confirmation_instructions(user, token, _opts = {}) 11 | @store = Spree::Store.default 12 | @confirmation_url = spree_user_confirmation_url(confirmation_token: token, host: @store.url) 13 | mail to: user.email, from: from_address(@store), subject: "#{@store.name} #{I18n.t(:subject, scope: [:devise, :mailer, :confirmation_instructions])}" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /templates/app/javascript/controllers/checkout_payment_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = [ "paymentRadio" ] 5 | 6 | connect() { 7 | this.selectedRadio = this.paymentRadioTargets.find((radio) => radio.checked) 8 | this.render() 9 | } 10 | 11 | paymentSelected(e) { 12 | this.selectedRadio = e.target 13 | this.render() 14 | } 15 | 16 | render() { 17 | this.paymentRadioTargets.forEach( 18 | (radio) => { 19 | const fieldset = this.element.querySelector(`fieldset[name="${radio.dataset.fieldsetName}"]`) 20 | 21 | if (radio === this.selectedRadio) { 22 | fieldset.disabled = false 23 | } else { 24 | radio.checked = false 25 | fieldset.disabled = true 26 | } 27 | } 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /templates/spec/components/product_card_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "solidus_starter_frontend_spec_helper" 4 | 5 | RSpec.describe ProductCardComponent, type: :component do 6 | include FactoryBot::Syntax::Methods 7 | 8 | it "renders the product with its main image and price" do 9 | product = build(:product, name: "The best product", id: 123) 10 | price = build(:price) 11 | rendered_component = render_inline(described_class.new(product, price: price)) 12 | 13 | aggregate_failures do 14 | expect(rendered_component.css("li .product-card_header a").to_html).to include("The best product") 15 | expect(rendered_component.css("li .product-card_header a").first[:href]).to eq('/products/123') 16 | expect(rendered_component.css("li .product-card_image a").first[:href]).to eq('/products/123') 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /templates/spec/controllers/checkout_sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe CheckoutSessionsController, type: :controller do 6 | let(:order) { create(:order_with_line_items, email: nil, user: nil, guest_token: token) } 7 | let(:user) { build(:user, spree_api_key: 'fake') } 8 | let(:token) { 'some_token' } 9 | let(:cookie_token) { token } 10 | 11 | before do 12 | request.cookie_jar.signed[:guest_token] = cookie_token 13 | allow(controller).to receive(:current_order) { order } 14 | end 15 | 16 | context '#new' do 17 | it 'checks if the user is authorized for :edit' do 18 | expect(controller).to receive(:authorize!).with(:edit, order, token) 19 | request.cookie_jar.signed[:guest_token] = token 20 | get :new, params: {} 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /templates/app/views/products/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | <% if params[:keywords] && @products.empty? %> 12 | 13 | <%= t('spree.no_products_found') %> 14 | 15 | <% else %> 16 | <%= render 'products', products: @products, taxon: @taxon %> 17 | <% end %> 18 |
19 |
20 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rake", "rake") 30 | -------------------------------------------------------------------------------- /bin/guard: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'guard' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("guard", "guard") 30 | -------------------------------------------------------------------------------- /templates/app/controllers/store_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class StoreController < Spree::BaseController 4 | include Spree::Core::ControllerHelpers::Pricing 5 | include Spree::Core::ControllerHelpers::Order 6 | include Taxonomies 7 | 8 | etag { config_locale } 9 | 10 | layout 'storefront' 11 | 12 | def unauthorized 13 | render 'shared/auth/unauthorized', layout: Spree::Config[:layout], status: 401 14 | end 15 | 16 | def cart_link 17 | render partial: 'shared/cart/link_to_cart' 18 | fresh_when(etag: current_order, template: 'shared/cart/_link_to_cart') 19 | end 20 | 21 | private 22 | 23 | def config_locale 24 | I18n.locale 25 | end 26 | 27 | def lock_order 28 | Spree::OrderMutex.with_lock!(@order) { yield } 29 | rescue Spree::OrderMutex::LockFailed 30 | flash[:error] = t('spree.order_mutex_error') 31 | redirect_to cart_path 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /bin/_guard-core: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application '_guard-core' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("guard", "_guard-core") 30 | -------------------------------------------------------------------------------- /templates/spec/system/authentication/sign_out_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.feature 'Sign Out', type: :system, js: true do 6 | include_context "featured products" 7 | 8 | given!(:user) do 9 | create(:user, 10 | email: 'email@person.com', 11 | password: 'secret', 12 | password_confirmation: 'secret') 13 | end 14 | 15 | background do 16 | visit login_path 17 | fill_in 'Email', with: user.email 18 | fill_in 'Password:', with: user.password 19 | # Regression test for #1257 20 | check 'Remember me' 21 | click_button 'Login' 22 | end 23 | 24 | scenario 'allow a signed in user to logout' do 25 | click_link '', href: '/account' 26 | click_button 'Logout' 27 | visit root_path 28 | expect(page).to have_link nil, href: '/login' 29 | expect(page).not_to have_button 'Logout' 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /templates/app/controllers/coupon_codes_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CouponCodesController < StoreController 4 | before_action :load_order, only: :create 5 | around_action :lock_order, only: :create 6 | 7 | def create 8 | authorize! :update, @order, cookies.signed[:guest_token] 9 | 10 | if params[:coupon_code].present? 11 | @order.coupon_code = params[:coupon_code] 12 | handler = Spree::Config.promotions.coupon_code_handler_class.new(@order).apply 13 | 14 | respond_to do |format| 15 | format.html do 16 | if handler.successful? 17 | flash[:success] = handler.success 18 | else 19 | flash[:error] = handler.error 20 | end 21 | 22 | redirect_back fallback_location: cart_path 23 | end 24 | end 25 | end 26 | end 27 | 28 | private 29 | 30 | def load_order 31 | @order = current_order 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /templates/spec/system/authentication/confirmation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.feature 'Confirmation', type: :system do 6 | include_context "featured products" 7 | 8 | before do 9 | allow(Spree::UserMailer).to receive(:confirmation_instructions) 10 | .and_return(double(deliver: true)) 11 | end 12 | 13 | let!(:store) { create(:store) } 14 | 15 | background do 16 | ActionMailer::Base.default_url_options[:host] = 'http://example.com' 17 | end 18 | 19 | scenario 'create a new user', js: true, confirmable: false do 20 | visit signup_path 21 | 22 | fill_in 'Email', with: 'email@person.com' 23 | fill_in 'Password:', with: 'password' 24 | fill_in 'Password Confirmation', with: 'password' 25 | click_button 'Create' 26 | 27 | expect(page).to have_text 'You have signed up successfully.' 28 | expect(Spree::User.last.confirmed?).to be(false) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/steps/_delivery_step.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= form.fields_for :shipments do |ship_form| %> 4 | <%= render 'checkouts/steps/delivery_step/proposed_shipment', ship_form: ship_form %> 5 | <% end %> 6 | 7 | <% if differentiator.try(:missing?) %> 8 | <%= render "checkouts/steps/delivery_step/unshippable_items", items: differentiator.missing %> 9 | <% end %> 10 | 11 | <% if Spree::Config[:shipping_instructions] %> 12 |
13 | <%= form.label :special_instructions, "#{I18n.t('spree.shipping_instructions')}:" %> 14 | <%= form.text_area :special_instructions %> 15 |
16 | <% end %> 17 |
18 | 19 |
20 | <%= form.button( 21 | I18n.t("spree.save_and_continue"), 22 | class: 'button-primary', 23 | name: :commit 24 | ) %> 25 |
26 |
27 | -------------------------------------------------------------------------------- /templates/app/views/orders/_coupon_code.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag order_coupon_codes_path(@order), method: :post, class: "coupon-code flex flex-col md:flex-row items-center gap-x-4 lg:flex-col gap-y-3" do %> 2 |
3 | <%= text_field_tag 'coupon_code', 4 | nil, 5 | id: 'order_coupon_code', 6 | placeholder: t("spree.coupon_code"), 7 | class: 'rounded-md border border border-gray-mid rounded px-4 py-3 placeholder:text-gray-500 text-black text-body w-full hover:border-gray-400 focus:outline-none focus:ring focus:ring-offset-1 focus:ring-black/20 focus:border-gray-400 dark:bg-gray-800 dark:border-gray-600 dark:text-sand dark:placeholder:text-gray-400 dark:focus:ring-white/30', 8 | required: true 9 | %> 10 |
11 | 12 | <%= render partial: "shared/call_to_action", :locals => { :label => t('spree.apply_code'), :id => :commit, :name => :commit, :url => nil, :type => 'tertiary', additional_classes: 'w-full' } %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /templates/spec/system/caching/taxons_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'taxons', type: :system, caching: true do 6 | let!(:taxonomy) { create(:taxonomy) } 7 | let!(:taxon) { create(:taxon, taxonomy: taxonomy) } 8 | 9 | before do 10 | # Warm up the cache 11 | visit products_path 12 | 13 | clear_cache_events 14 | end 15 | 16 | it "busts the cache when a taxon changes" do 17 | taxon.touch(:updated_at) 18 | 19 | visit products_path 20 | # Cache rewrites: 21 | # - 2 x categories component 22 | # - 1 x categories in navigation 23 | expect(cache_writes.count).to eq(3) 24 | end 25 | 26 | it "busts the cache when max_level_in_taxons_menu conf changes" do 27 | stub_spree_preferences(max_level_in_taxons_menu: 5) 28 | visit products_path 29 | 30 | # Cache rewrites: 31 | # - 2 x categories component 32 | expect(cache_writes.count).to eq(2) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /templates/app/views/orders/_item_info.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | base_class = "item-info".freeze 3 | 4 | # Optional props 5 | stock_info = local_assigns.fetch(:stock_info, true) 6 | classes = local_assigns.fetch(:classes, []) 7 | 8 | # Classes 9 | class_names = classes.push(base_class).join(" ") 10 | %> 11 | 12 |
13 |

14 | <%= link_to line_item.name, product_path(variant.product) %> 15 |

16 | 17 | <% if defined?(line_item.single_money) %> 18 |

19 | <%= line_item.single_money.to_html %> 20 |

21 | <% end %> 22 | 23 |

24 | <%= variant.options_text %> 25 |

26 | 27 | <% if stock_info && line_item.insufficient_stock? %> 28 |

29 | <%= t('spree.out_of_stock') %> 30 |

31 | <% end %> 32 |
33 | -------------------------------------------------------------------------------- /templates/app/views/products/_products.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | paginated_products = @searcher.retrieve_products if params.key?(:keywords) 3 | paginated_products ||= products 4 | %> 5 | 6 | <% content_for :head do %> 7 | <% if paginated_products.respond_to?(:total_pages) %> 8 | <%= rel_next_prev_link_tags paginated_products %> 9 | <% end %> 10 | <% end %> 11 | 12 | <% if products.empty? %> 13 | 14 | <%= t('spree.no_products_found') %> 15 | 16 | <% else %> 17 | <% if params.key?(:keywords) %> 18 | 19 | <%= t('spree.search_results', keywords: h(params[:keywords])) %> 20 | 21 | <% end %> 22 | 23 | <%= render 'products/products_grid', products: products, taxon: taxon %> 24 | <% end %> 25 | 26 | <% if paginated_products.respond_to?(:total_pages) %> 27 | <%= paginate paginated_products %> 28 | <% end %> 29 | -------------------------------------------------------------------------------- /templates/app/views/orders/_payment_info.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= t('spree.payment_information') %> 5 |

6 | 7 | <%= link_to( 8 | t('spree.actions.edit'), 9 | checkout_state_path(:payment), 10 | { class: 'payment-info__edit underline text-body-md' } 11 | ) unless @order.completed? %> 12 |
13 | 14 | <% order.payments.valid.each do |payment| %> 15 | 28 | <% end %> 29 |
30 | -------------------------------------------------------------------------------- /templates/app/views/orders/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= accurate_title %> 4 |

5 | 6 | <% if order_just_completed?(@order) %> 7 |

8 | <%= t('spree.thank_you_for_your_order') %> 9 |

10 | <% end %> 11 | 12 |
13 | <%= render 'order_details', order: @order %> 14 |
15 | 16 |
17 | <%= render partial: "shared/call_to_action", :locals => { :label => t('spree.back_to_store'), :url => root_path } %> 18 | 19 | <% unless order_just_completed?(@order) %> 20 | <% if spree_current_user && respond_to?(:account_path) %> 21 | <%= render partial: "shared/call_to_action", :locals => { :label => t('spree.my_account'), :type => 'secondary', :url => account_path } %> 22 | <% end %> 23 | <% end %> 24 |
25 |
26 | -------------------------------------------------------------------------------- /templates/app/views/shared/_call_to_action.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | base_class = 'w-fit py-3 px-7 rounded-full text-body-sm font-sans-md tracking-wider leading-none uppercase whitespace-nowrap transition-colors duration-200' 3 | 4 | cta_primary_class = 'bg-red-500 text-white hover:bg-red-700' 5 | cta_secondary_class = 'bg-white text-black hover:bg-gray-200 dark:text-sand dark:bg-gray-800 dark:hover:bg-gray-700' 6 | cta_tertiary_class = 'bg-black text-white dark:text-black dark:bg-white dark:hover:bg-gray-100' 7 | 8 | type = type.presence ? type : 'primary' 9 | cta_class = "#{base_class} #{type == 'primary' && cta_primary_class} #{type == 'secondary' && cta_secondary_class} #{type == 'tertiary' && cta_tertiary_class} #{defined?(additional_classes) ? additional_classes : ''}" 10 | %> 11 | 12 | <% if defined?(url) && url != nil %> 13 | <%= link_to label.html_safe, url, class: cta_class, target: defined?(target) ? target : '_self' %> 14 | <% else %> 15 | <%= button_tag(label.html_safe, class: cta_class, id: defined?(id) && id) %> 16 | <% end %> 17 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/steps/delivery_step/_shipment_items.html.erb: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /templates/app/views/orders/payment/_paypal_commerce_platform.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render partial: "solidus_paypal_commerce_platform/shared/javascript_sdk_tag", locals: {payment_method: payment_method} %> 3 | 4 |
5 | 6 |
7 | 8 | 17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /templates/spec/system/authentication/change_email_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.feature 'Change email', type: :system do 6 | include SolidusStarterFrontend::System::CheckoutHelpers 7 | 8 | before { setup_custom_products } 9 | 10 | background do 11 | stub_spree_preferences(Spree::Auth::Config, signout_after_password_change: false) 12 | 13 | user = create(:user) 14 | visit root_path 15 | click_link '', href: '/login' 16 | 17 | fill_in 'spree_user[email]', with: user.email 18 | fill_in 'spree_user[password]', with: 'secret' 19 | click_button 'Login' 20 | 21 | visit edit_account_path 22 | end 23 | 24 | scenario 'work with correct password' do 25 | fill_in 'user[email]', with: 'tests@example.com' 26 | fill_in 'user[password]', with: 'password' 27 | fill_in 'user[password_confirmation]', with: 'password' 28 | click_button 'Update' 29 | 30 | expect(page).to have_text 'Account updated' 31 | expect(page).to have_text 'tests@example.com' 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /templates/app/controllers/solidus_paypal_commerce_platform/shipping_rates_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusPaypalCommercePlatform 4 | class ShippingRatesController < ::Spree::Api::BaseController 5 | before_action :load_order 6 | skip_before_action :authenticate_user 7 | 8 | def simulate_shipping_rates 9 | authorize! :show, @order, order_token 10 | 11 | @order.transaction do 12 | SolidusPaypalCommercePlatform::PaypalAddress.new(@order).simulate_update(params[:address]) 13 | @errors = @order.ship_address.errors.dup 14 | @paypal_order = SolidusPaypalCommercePlatform::PaypalOrder.new(@order).to_replace_json 15 | raise ActiveRecord::Rollback 16 | end 17 | 18 | if @errors.none? 19 | render json: @paypal_order, status: :ok 20 | else 21 | render json: @errors.full_messages, status: :unprocessable_entity 22 | end 23 | end 24 | 25 | private 26 | 27 | def load_order 28 | @order = ::Spree::Order.find_by(number: params[:order_id]) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /templates/app/controllers/autocomplete_results_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AutocompleteResultsController < StoreController 4 | def index 5 | respond_to do |format| 6 | format.html { redirect_to products_path(keywords: params[:keywords]) } 7 | format.turbo_stream { load_results } 8 | end 9 | end 10 | 11 | private 12 | 13 | def load_results 14 | @results ||= begin 15 | results = {} 16 | results[:products] = autocomplete_products 17 | results[:taxons] = autocomplete_taxons 18 | results 19 | end 20 | end 21 | 22 | def autocomplete_products 23 | if params[:keywords].present? 24 | searcher = build_searcher(params.merge(per_page: 5)) 25 | searcher.retrieve_products 26 | else 27 | Spree::Product.none 28 | end 29 | end 30 | 31 | def autocomplete_taxons 32 | if params[:keywords].present? 33 | Spree::Taxon 34 | .where(Spree::Taxon.arel_table[:name].matches("%#{params[:keywords]}%")) 35 | .limit(5) 36 | else 37 | Spree::Taxon.none 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /templates/app/controllers/user_registrations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserRegistrationsController < Devise::RegistrationsController 4 | before_action :check_permissions, only: [:edit, :update] 5 | skip_before_action :require_no_authentication 6 | 7 | def create 8 | build_resource(spree_user_params) 9 | if resource.save 10 | set_flash_message(:notice, :signed_up) 11 | sign_in(:spree_user, resource) 12 | session[:spree_user_signup] = true 13 | respond_with resource, location: after_sign_up_path_for(resource) 14 | else 15 | clean_up_passwords(resource) 16 | respond_with(resource) do |format| 17 | format.html { render :new } 18 | end 19 | end 20 | end 21 | 22 | protected 23 | 24 | def translation_scope 25 | 'devise.user_registrations' 26 | end 27 | 28 | def check_permissions 29 | authorize!(:create, resource) 30 | end 31 | 32 | private 33 | 34 | def spree_user_params 35 | params.require(:spree_user).permit(Spree::PermittedAttributes.user_attributes | [:email]) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/steps/_confirm_step.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | <%= t('spree.confirm') %> 5 | 6 | 7 | <%= render 'orders/order_details', order: @order %> 8 |
9 | 10 |
11 |
12 | <%= render 'checkouts/terms_and_conditions' %> 13 | <%= label_tag :accept_terms_and_conditions, class: 'checkbox-input mt-3' do %> 14 | <%= check_box_tag :accept_terms_and_conditions, 'accepted', false %> 15 | <%= t('spree.agree_to_terms_of_service') %> 16 | <% end %> 17 |
18 |
19 | 20 |
21 | <%= button_tag( 22 | I18n.t('spree.place_order'), 23 | class: 'button-primary', 24 | name: :commit, 25 | ) %> 26 |
27 |
28 | 29 | 32 | -------------------------------------------------------------------------------- /templates/spec/system/authentication/sign_up_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.feature 'Sign Up', type: :system do 6 | include_context "featured products" 7 | 8 | context 'with valid data' do 9 | scenario 'create a new user' do 10 | visit signup_path 11 | 12 | fill_in 'Email', with: 'email@person.com' 13 | fill_in 'Password:', with: 'password' 14 | fill_in 'Password Confirmation', with: 'password' 15 | click_button 'Create' 16 | 17 | expect(page).to have_text 'You have signed up successfully.' 18 | expect(Spree::User.count).to eq(1) 19 | end 20 | end 21 | 22 | context 'with invalid data' do 23 | scenario 'does not create a new user' do 24 | visit signup_path 25 | 26 | fill_in 'Email', with: 'email@person.com' 27 | fill_in 'Password:', with: 'password' 28 | fill_in 'Password Confirmation', with: '' 29 | click_button 'Create' 30 | 31 | expect(page).to have_css '#errorExplanation' 32 | expect(Spree::User.count).to eq(0) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /templates/app/views/orders/_order_shipments.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= title %> 5 |

6 | 7 | <%= link_to( 8 | t('spree.actions.edit'), 9 | checkout_state_path(:delivery), 10 | { class: 'order-shipments__edit underline text-body-md' } 11 | ) unless @order.completed? %> 12 |
13 | 14 | 31 |
32 | -------------------------------------------------------------------------------- /templates/app/views/home/_collection.erb: -------------------------------------------------------------------------------- 1 | <% if products.length > 0 %> 2 |
3 |
4 | <%= title %> 5 | <%= render "shared/call_to_action", { label: "Shop All", type: 'secondary', url: products_path } %> 6 |
7 | 8 | <% mobile_classes = 'grid grid-cols-2 gap-4' %> 9 | <% desktop_classes = 'md:gap-0 md:flex md:snap-x md:justify-between md:snap-mandatory md:overflow-x-auto md:space-x-6' %> 10 | 11 | 23 |
24 | <% end %> 25 | -------------------------------------------------------------------------------- /templates/spec/requests/carts_ability_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Cart permissions', type: :request do 6 | let(:order) { create(:order, user: nil, store: store) } 7 | let!(:store) { create(:store) } 8 | let(:variant) { create(:variant) } 9 | 10 | context 'when an order exists in the cookies.signed', with_guest_session: true do 11 | before { order.update(guest_token: nil) } 12 | 13 | context '#edit' do 14 | it 'checks if user is authorized for :read' do 15 | get cart_path 16 | expect(response).to redirect_to(login_path) 17 | end 18 | end 19 | 20 | context '#update' do 21 | it 'checks if user is authorized for :update' do 22 | patch cart_path, params: { order: { email: "foo@bar.com" } } 23 | expect(response).to redirect_to(login_path) 24 | end 25 | end 26 | 27 | context '#empty' do 28 | it 'checks if user is authorized for :update' do 29 | put empty_cart_path 30 | expect(response).to redirect_to(login_path) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /templates/app/assets/stylesheets/fonts/_fonts.scss: -------------------------------------------------------------------------------- 1 | $inter-font-path: './inter/'; 2 | $source-serif-font-path: './source-serif/'; 3 | 4 | @font-face { 5 | font-family: 'Inter Regular'; 6 | src: url('#{$inter-font-path}inter-v18-latin-regular.woff2') format('woff2'); 7 | font-weight: normal; 8 | font-style: normal; 9 | font-display: swap; 10 | } 11 | 12 | @font-face { 13 | font-family: 'Inter Medium'; 14 | src: url('#{$inter-font-path}inter-v18-latin-500.woff2') format('woff2'); 15 | font-weight: 500; 16 | font-style: normal; 17 | font-display: swap; 18 | } 19 | 20 | @font-face { 21 | font-family: 'Inter Semibold'; 22 | src: url('#{$inter-font-path}inter-v18-latin-600.woff2') format('woff2'); 23 | font-weight: 600; 24 | font-style: normal; 25 | font-display: swap; 26 | } 27 | 28 | @font-face { 29 | font-family: 'Source Serif'; 30 | src: url('#{$source-serif-font-path}source-serif-4-v8-latin-regular.woff2') format('woff2'); 31 | font-display: swap; 32 | } 33 | 34 | @font-face { 35 | font-family: 'Source Serif Medium'; 36 | src: url('#{$source-serif-font-path}source-serif-4-v8-latin-500.woff2') format('woff2'); 37 | font-display: swap; 38 | } 39 | -------------------------------------------------------------------------------- /templates/app/views/cart_line_items/_form.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | schema_properties = { 3 | itemprop: 'offers', 4 | itemscope: true, 5 | itemtype: 'http://schema.org/Offer' 6 | } 7 | %> 8 | 9 | <%= form_for :order, url: cart_line_items_path, html: schema_properties, :onload => "setupToCartBtnPriceChangeEvt(#{product.price})" do |f| %> 10 | <% if product_variants_with_options(product).any? %> 11 |
12 | <% sorted_option_types(product).each_with_index do |option_type, i| %> 13 |
14 | <%= render 'cart_line_items/product_selection/option_type', product: product, option_type: option_type, index: i %> 15 |
16 | <% end %> 17 |
18 | <% end %> 19 | 20 | <%= render 'cart_line_items/product_variants', product: product %> 21 | 22 | <% if product.price %> 23 | <%= render 'cart_line_items/product_submit', product: product %> 24 | <% else %> 25 |
26 |

27 | <%= t('spree.product_not_available_in_this_currency') %> 28 |

29 |
30 | <% end %> 31 | <% end %> 32 | -------------------------------------------------------------------------------- /templates/app/views/products/show.html.erb: -------------------------------------------------------------------------------- 1 | <% @body_id = 'product-details' %> 2 | 3 | <% cache [I18n.locale, current_pricing_options, @product] do %> 4 |
5 |
6 |
7 |
8 | <%= render 'product_thumbnails', product: @product %> 9 | <%= render 'product_image', product: @product %> 10 |
11 |
12 | 13 |
14 | <%= render 'product_header', product: @product %> 15 | <%= render 'product_promotions', product: @product %> 16 | <%= render 'cart_line_items/form', product: @product %> 17 | <%= render 'product_info', product: @product %> 18 |
19 |
20 | 21 |
22 | <%= render partial: 'home/collection', :locals => { :title => "You May Also Like", :products => @similar_products } %> 23 |
24 |
25 | <% end %> 26 | -------------------------------------------------------------------------------- /templates/spec/components/filter_component_spec.rb: -------------------------------------------------------------------------------- 1 | require "solidus_starter_frontend_spec_helper" 2 | 3 | RSpec.describe FilterComponent, type: :component do 4 | let(:filter) { Spree::Core::ProductFilters.price_filter } 5 | let(:search_params) { {} } 6 | 7 | let(:inputs) do 8 | page.all('input') 9 | end 10 | 11 | context 'when rendered' do 12 | before do 13 | render_inline(described_class.new(filter: filter, search_params: search_params)) 14 | end 15 | 16 | it 'renders a list of checkboxes for the filter labels' do 17 | expect(inputs).to_not be_empty 18 | expect(inputs.first[:id]).to eq('Price_Range_Under__10.00') 19 | end 20 | 21 | context 'when a filter list item was checked' do 22 | let(:search_params) do 23 | { price_range_any: ["Under $10.00"] } 24 | end 25 | 26 | it 'renders as checked' do 27 | expect(inputs.first['checked']).to be_truthy 28 | end 29 | end 30 | 31 | context 'when a filter list item was not checked' do 32 | let(:search_params) { { } } 33 | 34 | it 'renders as unchecked' do 35 | expect(inputs.first['checked']).to be_falsey 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /templates/spec/requests/home_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Home layout', type: :request, with_signed_in_user: true do 6 | let(:searcher_class) { instance_double(Spree::Config.searcher_class) } 7 | let(:user) { create(:user) } 8 | let(:product) { build_stubbed(:product) } 9 | let(:variant) { create(:variant) } 10 | let!(:featured_product) { create(:product, name: 'Solidus hoodie', variants: [variant] )} 11 | 12 | before do 13 | allow(Spree::Config.searcher_class).to receive(:new) { searcher_class } 14 | allow(searcher_class).to receive(:current_user=) 15 | allow(searcher_class).to receive(:pricing_options=) 16 | allow(searcher_class).to receive(:retrieve_products) { Spree::Product.where(id: product.id) } 17 | end 18 | 19 | it "provides current user to the searcher class" do 20 | get root_path 21 | expect(searcher_class).to have_received(:current_user=).with(user) 22 | expect(response.status).to eq(200) 23 | end 24 | 25 | context "layout" do 26 | it "renders default layout" do 27 | get root_path 28 | expect(response).to render_template(layout: 'layouts/storefront') 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /templates/spec/requests/checkout_with_views_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | # This spec is useful for when we just want to make sure a view is rendering correctly 6 | # Walking through the entire checkout process is rather tedious, don't you think? 7 | RSpec.describe 'Checkout view rendering', type: :request, with_signed_in_user: true do 8 | let(:token) { 'some_token' } 9 | let(:user) { create(:user) } 10 | # Regression test for https://github.com/spree/spree/issues/3246 11 | context "when using GBP" do 12 | before do 13 | stub_spree_preferences(currency: "GBP") 14 | end 15 | 16 | context "when order is in delivery" do 17 | before do 18 | # Using a let block won't acknowledge the currency setting 19 | # Therefore we just do it like this... 20 | order = Spree::TestingSupport::OrderWalkthrough.up_to(:address) 21 | order.update(user: user) 22 | end 23 | 24 | it "displays rate cost in correct currency" do 25 | get checkout_path 26 | html = Nokogiri::HTML(response.body) 27 | expect(html.css('.shipping-methods__rate').text.strip).to include("£10") 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /templates/app/assets/javascripts/checkout.js: -------------------------------------------------------------------------------- 1 | //= require_self 2 | //= require checkout/address 3 | 4 | Solidus.disableSaveOnClick = () => { 5 | const form = document.querySelector('form.edit_order'); 6 | form.addEventListener('submit', () => { 7 | const elements = form.querySelectorAll('[type="submit"], [type="image"]'); 8 | elements.forEach(element => { 9 | element.setAttribute('disabled', true); 10 | element.classList.remove('primary'); 11 | element.classList.add('disabled'); 12 | }); 13 | }); 14 | }; 15 | 16 | window.addEventListener('DOMContentLoaded', () => { 17 | const termsCheckbox = document.getElementById('accept_terms_and_conditions'); 18 | 19 | if (termsCheckbox) { 20 | const form = termsCheckbox.closest('form'); 21 | const submitButton = form.querySelector('[type="submit"]'); 22 | form.onsubmit = function () { 23 | if (termsCheckbox.checked) { 24 | submitButton.innerHTML = 'Submitting...'; 25 | return true; 26 | } else { 27 | alert('Please review and accept the Terms of Service'); 28 | submitButton.removeAttribute('disabled'); 29 | submitButton.classList.remove('disabled'); 30 | return false; 31 | }; 32 | }; 33 | }; 34 | }); 35 | -------------------------------------------------------------------------------- /templates/app/views/shared/navigation/_theme_switcher.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | base_toggle_icon_class = 'absolute top-[3px] p-0.5 h-[22px] w-[22px] transition-opacity duration-200' 3 | %> 4 | 21 | -------------------------------------------------------------------------------- /templates/spec/controllers/taxons_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe TaxonsController, type: :controller do 6 | describe 'GET #show' do 7 | let!(:taxon) { create(:taxon, permalink: "test") } 8 | let!(:old_param) { taxon.permalink } 9 | 10 | before do 11 | taxon.update(permalink: 'new-sample-product') 12 | end 13 | 14 | context 'when permalink matches id param' do 15 | it 'does not redirect' do 16 | get :show, params: { id: taxon.permalink } 17 | expect(response).to have_http_status(:ok) 18 | end 19 | end 20 | 21 | context 'when old slug is passed' do 22 | it 'redirects to the correct product path' do 23 | get :show, params: { id: old_param } 24 | 25 | expect(response).to redirect_to(nested_taxons_path(taxon)) 26 | expect(response.status).to eq(301) 27 | end 28 | end 29 | 30 | context 'when slug does not match id param and product does not exist' do 31 | it 'returns 404' do 32 | expect do 33 | get :show, params: { id: 'non-existent-slug' } 34 | end.to raise_error(ActiveRecord::RecordNotFound) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'coverage' 4 | require 'simplecov' 5 | 6 | SimpleCov.root File.expand_path(__dir__) 7 | 8 | if ENV['CODECOV_TOKEN'] 9 | require 'simplecov-cobertura' 10 | 11 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 12 | SimpleCov::Formatter::CoberturaFormatter, 13 | SimpleCov.formatter, 14 | ]) 15 | else 16 | warn "Provide a CODECOV_TOKEN environment variable to enable Codecov uploads" 17 | end 18 | 19 | def (SimpleCov::ResultAdapter).call(result) 20 | result = result.transform_keys do |path| 21 | template_path = path.sub( 22 | "#{SimpleCov.root}/sandbox/", 23 | "#{SimpleCov.root}/templates/" 24 | ) 25 | File.exist?(template_path) ? template_path : path 26 | end 27 | result.each do |path, coverage| 28 | next unless path.end_with?('.erb') 29 | 30 | # Remove the extra trailing lines added by ERB 31 | coverage[:lines] = coverage[:lines][...File.read(path).lines.size] 32 | end 33 | result 34 | end 35 | 36 | warn "Tracking coverage on process #{$$}..." 37 | SimpleCov.start do 38 | root __dir__ 39 | enable_coverage_for_eval 40 | add_filter %r{sandbox/(db|config|spec|tmp)/} 41 | track_files "#{SimpleCov.root}/{sandbox,lib,app}/**/*.{rb,erb}}" 42 | end 43 | -------------------------------------------------------------------------------- /templates/spec/requests/current_order_tracking_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'current order tracking', type: :request, with_signed_in_user: true do 6 | let!(:store) { create(:store) } 7 | let(:user) { create(:user) } 8 | 9 | class TestController < StoreController 10 | def create_order 11 | @order = current_order(create_order_if_necessary: true) 12 | head :ok 13 | end 14 | 15 | def not_create_order 16 | head :ok 17 | end 18 | end 19 | 20 | before do 21 | Rails.application.routes.draw do 22 | get '/test', to: 'test#create_order' 23 | get '/test2', to: 'test#not_create_order' 24 | end 25 | end 26 | after do 27 | Rails.application.reload_routes! 28 | end 29 | 30 | it 'automatically tracks who the order was created by & IP address' do 31 | get '/test' 32 | 33 | expect(assigns[:order].created_by).to eq user 34 | expect(assigns[:order].last_ip_address).to eq "127.0.0.1" 35 | end 36 | 37 | context "current order creation" do 38 | it "doesn't create a new order out of the blue" do 39 | expect do 40 | get '/test2' 41 | end.not_to(change { Spree::Order.count }) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /templates/app/views/spree/admin/user_sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% if flash[:alert] %> 2 |
<%= flash[:alert] %>
3 | <% end %> 4 | 5 | <% @body_id = 'login' %> 6 |
7 |
<%= t('spree.admin_login') %>
8 |
9 | <%= form_for Spree::User.new, as: :spree_user, url: spree.admin_create_new_session_path do |f| %> 10 |
11 |

12 | <%= f.label :email, t('spree.email') %>
13 | <%= f.email_field :email, class: 'title', tabindex: 1, autocomplete: 'username' %> 14 |

15 |

16 | <%= f.label :password, t('spree.password') %>
17 | <%= f.password_field :password, class: 'title', tabindex: 2, autocomplete: 'current-password' %> 18 |

19 |
20 |

21 | <%= f.check_box :remember_me, tabindex: 3 %> 22 | <%= f.label :remember_me, t('spree.remember_me') %> 23 |

24 | 25 |

<%= f.submit t('spree.login'), class: 'btn btn-primary', tabindex: 4 %>

26 | <% end %> 27 | <%= t('spree.or') %> 28 | <%= link_to t('spree.forgot_password'), spree.admin_recover_password_path %> 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /templates/app/views/user_passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<%= I18n.t('spree.forgot_password') %>

4 | 5 | <%= form_for Spree::User.new, as: :spree_user, url: reset_password_path, html: { class: "space-y-6 mb-12" } do |f| %> 6 |
7 | <%= f.label :email, "#{t("spree.email")}:" %> 8 | <%= f.email_field :email, placeholder: 'name@example.com' %> 9 |
10 | 11 |
12 | <%= f.button( 13 | t("spree.reset_password"), 14 | class: 'w-full w-fit py-3 px-7 rounded-full text-body-sm font-bold leading-none uppercase whitespace-nowrap transiton-colors duration-200 bg-red-500 text-white hover:bg-red-700', 15 | name: :commit 16 | ) %> 17 |
18 | 19 |
20 | <%= I18n.t("spree.or") %> <%= link_to I18n.t("spree.create_a_new_account"), signup_path, class:"transition-all duration-300 text-black hover:!text-red-500 dark:text-sand underline" %> 21 |
22 | 23 | <% end %> 24 |
25 |
26 | -------------------------------------------------------------------------------- /templates/app/views/orders/_order_item.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= link_to( 4 | render(ImageComponent.new( 5 | image: item.variant.gallery.images.first || item.variant.product.gallery.images.first, 6 | size: :small 7 | )), 8 | item.variant.product 9 | ) %> 10 |
11 | 12 |
13 |
14 | <%= render( 15 | 'orders/item_info', 16 | line_item: item, 17 | stock_info: false, 18 | variant: item.variant, 19 | classes: ["!gap-y-2 [&>h3]:text-body [&>h3]:font-sans-md"] 20 | ) %> 21 | 22 |
23 | <%= item.single_money.to_html %> ( <%= item.quantity %>) 24 |
25 |
26 |
27 | <%= item.display_amount.to_html %> 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /templates/spec/support/solidus_starter_frontend/capybara.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'selenium/webdriver' 4 | require 'capybara/rspec' 5 | require 'capybara-screenshot/rspec' 6 | require 'spree/testing_support/capybara_ext' 7 | 8 | Capybara.default_max_wait_time = 10 9 | 10 | RSpec.configure do |config| 11 | config.before(:each, type: :system) do 12 | driven_by((ENV['CAPYBARA_DRIVER'] || :rack_test).to_sym) 13 | end 14 | 15 | config.before(:each, type: :system, js: true) do |example| 16 | screen_size = example.metadata[:screen_size] || [1800, 1400] 17 | driven_by(:selenium, using: :headless_chrome, screen_size: screen_size) do |capabilities| 18 | capabilities.add_argument("--disable-search-engine-choice-screen") 19 | end 20 | end 21 | end 22 | 23 | Capybara.register_driver :selenium_chrome_headless_docker_friendly do |app| 24 | browser_options = ::Selenium::WebDriver::Chrome::Options.new 25 | browser_options.args << '--headless' 26 | browser_options.args << '--disable-gpu' 27 | browser_options.args << '--disable-search-engine-choice-screen' 28 | # Sandbox cannot be used inside unprivileged Docker container 29 | browser_options.args << '--no-sandbox' 30 | Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options) 31 | end 32 | -------------------------------------------------------------------------------- /templates/app/views/checkout_guest_sessions/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<%= I18n.t('spree.guest_user_account') %>

4 | 5 | <%= form_for @order, 6 | url: checkout_guest_session_path, 7 | method: :post, 8 | html: { class: "space-y-6", id: "checkout_form_registration" } do |f| 9 | %> 10 | 11 |
12 | <%= f.label :email, "#{t("spree.email")}:" %> 13 | <%= f.email_field :email, placeholder: 'name@example.com' %> 14 |
15 | 16 |
17 | <%= f.button t("spree.continue"), 18 | name: :commit, 19 | class: 'w-fit py-3 px-7 rounded-full text-body-sm font-bold leading-none uppercase whitespace-nowrap transiton-colors duration-200 bg-red-500 text-white hover:bg-red-700' 20 | %> 21 |
22 | 23 |
24 | <%= I18n.t("spree.or") %> <%= link_to I18n.t("spree.create_a_new_account"), signup_path, class:"transition-all duration-300 text-black hover:!text-red-500 dark:text-sand underline" %> 25 |
26 | 27 | <% end %> 28 |
29 |
30 | -------------------------------------------------------------------------------- /templates/spec/system/authentication/sign_in_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.feature 'Sign In', type: :system do 6 | include_context "featured products" 7 | 8 | background do 9 | @user = create(:user, email: 'email@person.com', password: 'secret', password_confirmation: 'secret') 10 | visit login_path 11 | end 12 | 13 | scenario 'let a user sign in successfully' do 14 | fill_in 'Email', with: @user.email 15 | fill_in 'Password:', with: @user.password 16 | click_button 'Login' 17 | 18 | expect(page).to have_text 'Logged in successfully' 19 | expect(page).not_to have_link nil, href: '/login' 20 | expect(page).to have_link nil, href: '/account' 21 | expect(current_path).to eq '/' 22 | end 23 | 24 | scenario 'show validation errors' do 25 | fill_in 'Email', with: @user.email 26 | fill_in 'Password:', with: 'wrong_password' 27 | click_button 'Login' 28 | 29 | expect(page).to have_text 'Invalid email or password' 30 | expect(page).to have_text 'Login' 31 | end 32 | 33 | it "should store the user previous location" do 34 | visit account_path 35 | fill_in "Email", with: @user.email 36 | fill_in "Password", with: @user.password 37 | click_button "Login" 38 | expect(current_path).to eq "/account" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /templates/app/views/orders/_address_overview.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= title %> 5 |

6 | 7 | <%= link_to( 8 | t('spree.actions.edit'), 9 | checkout_state_path(:address), 10 | { class: 'address-overview__edit underline text-body-md' } 11 | ) unless @order.completed? %> 12 |
13 | 14 | 42 |
43 | -------------------------------------------------------------------------------- /templates/spec/system/checkout_unshippable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'checkout with unshippable items', type: :system do 6 | let!(:stock_location) { create(:stock_location) } 7 | let(:order) { Spree::TestingSupport::OrderWalkthrough.up_to(:address) } 8 | 9 | before do 10 | create(:line_item, order: order) 11 | order.reload 12 | line_item = order.line_items.last 13 | stock_item = stock_location.stock_item(line_item.variant) 14 | stock_item.adjust_count_on_hand(0) 15 | stock_item.backorderable = false 16 | stock_item.save! 17 | 18 | user = create(:user) 19 | order.user = user 20 | order.recalculate 21 | 22 | allow_any_instance_of(CheckoutsController).to receive_messages(current_order: order) 23 | allow_any_instance_of(CheckoutsController).to receive_messages(spree_current_user: user) 24 | allow_any_instance_of(CheckoutsController).to receive_messages(skip_state_validation?: true) 25 | allow_any_instance_of(CheckoutsController).to receive_messages(ensure_sufficient_stock_lines: true) 26 | end 27 | 28 | it 'displays and removes' do 29 | visit checkout_state_path(:delivery) 30 | expect(page).to have_content('Unshippable Items') 31 | 32 | click_button "Save and Continue" 33 | 34 | order.reload 35 | expect(order.line_items.count).to eq 1 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /templates/app/views/carts/show.html.erb: -------------------------------------------------------------------------------- 1 | <% @body_id = 'cart' %> 2 | 3 |
4 | <% if @order.line_items.empty? %> 5 | <%= render 'carts/cart_empty' %> 6 | <% else %> 7 | <%= form_for @order, url: cart_path, html: { 8 | id: 'update-cart', 9 | class: 'col-span-full lg:col-span-9 2xl:col-span-8', 10 | 'data-cart-page-target': 'form', 11 | 'data-action': 'submit->cart-page#disableUpdateButton' 12 | } do |order_form| %> 13 | <% order = order_form.object %> 14 |
15 | <%= render 'carts/cart_header', order_form: order_form %> 16 | <%= render 'shared/error_messages', target: order %> 17 | <%= render 'carts/cart_items', order_form: order_form %> 18 | <%= render 'carts/cart_secondary_actions', order_form: order_form %> 19 |
20 | <% end %> 21 | 22 |
23 | <%= render 'orders/coupon_code' %> 24 | <%= form_for @order, url: cart_path, html: { 25 | id: 'checkout-button-form', 26 | } do |order_form| %> 27 | <%= render 'carts/cart_footer', order_form: order_form %> 28 | <% end %> 29 |
30 | 31 | <% end %> 32 |
33 | -------------------------------------------------------------------------------- /templates/spec/system/authentication/password_reset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.feature 'Reset Password', type: :system do 6 | let!(:store) { create(:store) } 7 | 8 | background do 9 | ActionMailer::Base.default_url_options[:host] = 'http://example.com' 10 | end 11 | 12 | context 'when an account with this email address exists' do 13 | let!(:user) { create(:user, email: 'foobar@example.com', password: 'secret', password_confirmation: 'secret') } 14 | 15 | scenario 'allows a user to supply an email for the password reset' do 16 | visit login_path 17 | click_link 'Forgot Password?' 18 | fill_in_email 19 | click_button 'Reset my password' 20 | expect(page).to have_text 'you will receive an email with instructions' 21 | end 22 | end 23 | 24 | # Test that we are extending the functionality from 25 | # https://github.com/solidusio/solidus_auth_devise/pull/155 26 | # to the non-admin login 27 | scenario 'does not reveal email addresses if they are not found' do 28 | visit login_path 29 | click_link 'Forgot Password?' 30 | fill_in_email 31 | click_button 'Reset my password' 32 | expect(page).to_not have_text "Email not found" 33 | expect(page).to have_text 'you will receive an email with instructions' 34 | end 35 | 36 | def fill_in_email 37 | fill_in 'Email', with: 'foobar@example.com' 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /templates/app/controllers/user_passwords_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserPasswordsController < Devise::PasswordsController 4 | # Overridden due to bug in Devise. 5 | # respond_with resource, location: new_session_path(resource_name) 6 | # is generating bad url /session/new.user 7 | # 8 | # overridden to: 9 | # respond_with resource, location: login_path 10 | # 11 | def create 12 | self.resource = resource_class.send_reset_password_instructions(params[resource_name]) 13 | 14 | set_flash_message(:notice, :send_instructions) if is_navigational_format? 15 | 16 | if resource.errors.empty? 17 | respond_with resource, location: login_path 18 | else 19 | respond_with_navigational(resource) { render :new } 20 | end 21 | end 22 | 23 | # Devise::PasswordsController allows for blank passwords. 24 | # Silly Devise::PasswordsController! 25 | # Fixes spree/spree#2190. 26 | def update 27 | if params[:spree_user][:password].blank? 28 | self.resource = resource_class.new 29 | resource.reset_password_token = params[:spree_user][:reset_password_token] 30 | set_flash_message(:error, :cannot_be_blank) 31 | render :edit 32 | else 33 | super 34 | end 35 | end 36 | 37 | protected 38 | 39 | def translation_scope 40 | 'devise.user_passwords' 41 | end 42 | 43 | def new_session_path(resource_name) 44 | send("new_#{resource_name}_session_path") 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /templates/app/views/products/_product_thumbnails.html.erb: -------------------------------------------------------------------------------- 1 | <% thumbnails_class = "flex shrink-0 w-1_col-3 justify-center items-center snap-center bg-gray-50 rounded-lg p-4 aspect-square transition-colors duration-150 hover:bg-gray-100 md:rounded-xl lg:rounded-2xl lg:w-full" %> 2 | 3 |
4 | 35 |
36 | 37 | -------------------------------------------------------------------------------- /templates/app/controllers/solidus_paypal_commerce_platform/payments_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusPaypalCommercePlatform 4 | class PaymentsController < ::Spree::Api::BaseController 5 | before_action :load_order 6 | skip_before_action :authenticate_user 7 | 8 | def create 9 | authorize! :update, @order, order_token 10 | paypal_order_id = paypal_params[:paypal_order_id] 11 | 12 | if !paypal_order_id 13 | return redirect_to checkout_state_path(@order.state), 14 | notice: I18n.t("solidus_paypal_commerce_platform.controllers.payments_controller.invalid_paypal_order_id") 15 | end 16 | 17 | if @order.complete? 18 | return redirect_to spree.order_path(@order), 19 | notice: I18n.t("solidus_paypal_commerce_platform.controllers.payments_controller.order_complete") 20 | end 21 | 22 | source = SolidusPaypalCommercePlatform::PaymentSource.new(paypal_order_id: paypal_order_id) 23 | 24 | source.transaction do 25 | if source.save! 26 | @order.payments.create!( 27 | payment_method_id: paypal_params[:payment_method_id], 28 | source: source 29 | ) 30 | 31 | render json: {}, status: :ok 32 | end 33 | end 34 | end 35 | 36 | private 37 | 38 | def paypal_params 39 | params.permit(:paypal_order_id, :order_id, :order_token, :payment_method_id) 40 | end 41 | 42 | def load_order 43 | @order = ::Spree::Order.find_by!(number: params[:order_id]) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /templates/app/controllers/cart_line_items_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CartLineItemsController < StoreController 4 | helper 'spree/products', 'orders' 5 | 6 | respond_to :html 7 | 8 | before_action :store_guest_token 9 | 10 | # Adds a new item to the order (creating a new order if none already exists) 11 | def create 12 | @order = current_order(create_order_if_necessary: true) 13 | authorize! :update, @order, cookies.signed[:guest_token] 14 | 15 | variant = Spree::Variant.find(params[:variant_id]) 16 | quantity = params[:quantity].present? ? params[:quantity].to_i : 1 17 | 18 | # 2,147,483,647 is crazy. See issue https://github.com/spree/spree/issues/2695. 19 | if !quantity.between?(1, 2_147_483_647) 20 | @order.errors.add(:base, t('spree.please_enter_reasonable_quantity')) 21 | else 22 | begin 23 | @line_item = @order.contents.add(variant, quantity) 24 | rescue ActiveRecord::RecordInvalid => error 25 | @order.errors.add(:base, error.record.errors.full_messages.join(", ")) 26 | end 27 | end 28 | 29 | respond_to do |format| 30 | format.html do 31 | if @order.errors.any? 32 | flash[:error] = @order.errors.full_messages.join(", ") 33 | redirect_back_or_default(root_path) 34 | return 35 | else 36 | redirect_to cart_path 37 | end 38 | end 39 | end 40 | end 41 | 42 | private 43 | 44 | def store_guest_token 45 | cookies.permanent.signed[:guest_token] = params[:token] if params[:token] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /templates/app/views/cart_line_items/_product_variants.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | variants = product.variants_and_option_values_for(current_pricing_options) 3 | %> 4 | 5 | <% if variants.any? %> 6 | 40 | <% else %> 41 | <%= hidden_field_tag "variant_id", product.master.id %> 42 | <% end %> 43 | -------------------------------------------------------------------------------- /templates/app/helpers/layout_helper.rb: -------------------------------------------------------------------------------- 1 | module LayoutHelper 2 | # Generates a simple canonical tag based on the request path, preserving allowed 3 | # parameters. For collection actions, a trailing slash is added to the href. 4 | # For more advanced use cases, consider using the `canonical-rails` gem. 5 | # 6 | # @see https://github.com/jumph4x/canonical-rails 7 | # @see https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls 8 | # 9 | # @param host [String] the host to use in the canonical URL 10 | # @param collection_actions [Array] the actions that will include a trailing slash 11 | # @param allowed_parameters [Array] the parameters to preserve in the canonical URL 12 | # @return [String] the generated link rel="canonical" tag 13 | def simple_canonical_tag( 14 | host: current_store&.url, 15 | collection_actions: %w[index], 16 | allowed_parameters: [:keywords, :page, :search, :taxon] 17 | ) 18 | path_without_extension = request.path 19 | .sub(/\.#{params[:format]}$/, "") 20 | .sub(/\/$/, "") 21 | 22 | href = "#{request.protocol}#{host}#{path_without_extension}" 23 | 24 | trailing_slash = request.params.key?('action') && 25 | collection_actions.include?(request.params['action']) 26 | href += '/' if trailing_slash 27 | 28 | query_params = params.select do |key, value| 29 | value.present? && allowed_parameters.include?(key.to_sym) 30 | end.to_unsafe_h 31 | 32 | href += "?#{query_params.to_query}" if query_params.present? 33 | 34 | tag(:link, rel: :canonical, href: href) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /templates/spec/mailers/user_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe UserMailer, type: :mailer do 4 | let!(:store) { create(:store) } 5 | let(:user) { create(:user) } 6 | 7 | before do 8 | user = create(:user) 9 | described_class.reset_password_instructions(user, 'token goes here').deliver_now 10 | @message = ActionMailer::Base.deliveries.last 11 | end 12 | 13 | describe '#reset_password_instructions' do 14 | describe 'message contents' do 15 | before do 16 | described_class.reset_password_instructions(user, 'token goes here').deliver_now 17 | @message = ActionMailer::Base.deliveries.last 18 | end 19 | 20 | context 'subject includes' do 21 | it 'translated devise instructions' do 22 | expect(@message.subject).to include( 23 | I18n.t(:subject, scope: [:devise, :mailer, :reset_password_instructions]) 24 | ) 25 | end 26 | 27 | it 'Spree site name' do 28 | expect(@message.subject).to include store.name 29 | end 30 | end 31 | 32 | context 'body includes' do 33 | it 'password reset url' do 34 | expect(@message.body.raw_source).to include "http://#{store.url}/user/password/edit" 35 | end 36 | end 37 | end 38 | 39 | describe 'legacy support for User object' do 40 | it 'sends an email' do 41 | expect { 42 | described_class.reset_password_instructions(user, 'token goes here').deliver_now 43 | }.to change(ActionMailer::Base.deliveries, :size).by(1) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /templates/spec/system/promotion_code_invalidation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Promotion Code Invalidation', type: :system, js: true do 6 | let!(:promotion) do 7 | FactoryBot.create( 8 | :promotion_with_item_adjustment, 9 | code: "PROMO", 10 | per_code_usage_limit: 1, 11 | adjustment_rate: 5 12 | ) 13 | end 14 | 15 | before do 16 | create(:store) 17 | FactoryBot.create(:product_in_stock, name: "DL-44") 18 | FactoryBot.create(:product_in_stock, name: "E-11") 19 | 20 | visit products_path 21 | click_link "DL-44" 22 | click_button "Add To Cart" 23 | 24 | visit products_path 25 | click_link "E-11" 26 | click_button "Add To Cart" 27 | end 28 | 29 | it 'adding the promotion to a cart with two applicable items' do 30 | fill_in "Coupon code", with: "PROMO" 31 | click_button "Apply Code" 32 | 33 | expect(page).to have_content("The coupon code was successfully applied to your order") 34 | 35 | within("#cart_adjustments") do 36 | expect(page).to have_content("-$10.00") 37 | end 38 | 39 | # Remove an item 40 | 41 | fill_in "order_line_items_attributes_0_quantity", with: 0 42 | click_button "Update" 43 | within("#cart_adjustments") do 44 | expect(page).to have_content("-$5.00") 45 | end 46 | 47 | # Add it back 48 | visit products_path 49 | click_link "DL-44" 50 | click_button "Add To Cart" 51 | within("#cart_adjustments") do 52 | expect(page).to have_content("-$10.00") 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /templates/app/views/home/_collections_with_call_to_action.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | Become the brand everyone talks about. 4 | <%= render "shared/call_to_action", { label: "Get Started", url: 'https://solidus.io/get-started', target: '_blank' } %> 5 |
6 | 7 | <% products.each_with_index do |product, index| %> 8 | <% 9 | product_image = product.gallery.images.second || product.gallery.images.first 10 | %> 11 | 12 | <%= cache [I18n.locale, current_pricing_options, @taxon, product] do %> 13 | <%= link_to product_path(product) do %> 14 |
15 | <%= render(ImageComponent.new( 16 | image: product_image, 17 | class: 'w-full h-full object-cover', 18 | size: :full, 19 | )) %> 20 |
21 | 22 | <%= product.name %> 23 | 24 | 25 | <%= product.display_price %> 26 | 27 |
28 |
29 | <% end %> 30 | <% end %> 31 | <% end %> 32 |
33 | -------------------------------------------------------------------------------- /templates/app/views/user_passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<%= I18n.t('spree.change_my_password') %>

4 | 5 | <%= render 'shared/error_messages', target: @spree_user %> 6 | 7 | <%= form_for @spree_user, as: :spree_user, url: update_password_path, method: :put, html: { class: "space-y-6 mb-12" } do |f| %> 8 |
9 | <%= f.label :password, "#{t("spree.password")}:" %> 10 | <%= f.password_field :password, placeholder: "p455w0rd" %> 11 |
12 | 13 |
14 | <%= f.label :password_confirmation, "#{t("spree.confirm_password")}:" %> 15 | <%= f.password_field :password_confirmation, placeholder: "p455w0rd" %> 16 |
17 | 18 |
19 | <%= f.button t("spree.update"), 20 | name: :commit, 21 | class: 'w-full w-fit py-3 px-7 rounded-full text-body-sm font-bold leading-none uppercase whitespace-nowrap transiton-colors duration-200 bg-red-500 text-white hover:bg-red-700' 22 | %> 23 |
24 | 25 | <%= f.hidden_field :reset_password_token %> 26 | 27 |
28 | <%= I18n.t("spree.or") %> <%= link_to I18n.t("spree.create_a_new_account"), signup_path, class:"transition-all duration-300 text-black hover:!text-red-500 dark:text-sand underline" %> 29 |
30 | 31 | <% end %> 32 |
33 |
34 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_adjustments.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render( 3 | "carts/cart_amount_row", 4 | type: t('spree.cart_subtotal', count: @order.line_items.sum(:quantity)), 5 | amount: @order.display_item_total 6 | ) %> 7 | 8 | <% if @order.line_item_adjustments.exists? %> 9 | <% @order.line_item_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %> 10 | <% if adjustments.sum(&:amount) != 0 %> 11 | <%= render( 12 | "cart_adjustment", 13 | type: t('spree.promotion'), 14 | label: label, 15 | adjustments: adjustments 16 | ) %> 17 | <% end %> 18 | <% end %> 19 | <% end %> 20 | 21 | <% @order.all_adjustments.tax.eligible.group_by(&:label).each do |label, adjustments| %> 22 | <%= render( 23 | "carts/cart_adjustment", 24 | type: t('spree.tax'), 25 | label: label, 26 | adjustments: adjustments 27 | ) %> 28 | <% end %> 29 | 30 | <% @order.shipments.each do |shipment| %> 31 | <%= render( 32 | "carts/cart_amount_row", 33 | type: t('spree.shipping'), 34 | label: shipment.shipping_method.name, 35 | amount: shipment.display_total_before_tax 36 | ) %> 37 | <% end %> 38 | 39 | <% @order.adjustments.eligible.group_by(&:label).each do |label, adjustments| %> 40 | <%= render( 41 | "carts/cart_adjustment", 42 | type: t('spree.adjustment'), 43 | label: label, 44 | adjustments: adjustments 45 | ) %> 46 | <% end %> 47 |
48 | -------------------------------------------------------------------------------- /templates/app/controllers/solidus_paypal_commerce_platform/wizard_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusPaypalCommercePlatform 4 | class WizardController < ::Spree::Api::BaseController 5 | helper ::Spree::Core::Engine.routes.url_helpers 6 | 7 | def create 8 | authorize! :create, ::Spree::PaymentMethod 9 | 10 | @payment_method = ::Spree::PaymentMethod.new(payment_method_params) 11 | 12 | if @payment_method.save 13 | edit_url = spree.edit_admin_payment_method_url(@payment_method) 14 | 15 | render( 16 | json: { redirectUrl: edit_url }, 17 | status: :created, 18 | location: edit_url, 19 | notice: "The PayPal Commerce Platform payment method has been successfully created" 20 | ) 21 | else 22 | render json: @payment_method.errors, status: :unprocessable_entity 23 | end 24 | end 25 | 26 | private 27 | 28 | def payment_method_params 29 | { 30 | name: "PayPal Commerce Platform", 31 | type: SolidusPaypalCommercePlatform::PaymentMethod, 32 | preferred_client_id: api_credentials.client_id, 33 | preferred_client_secret: api_credentials.client_secret, 34 | preferred_test_mode: SolidusPaypalCommercePlatform.config.env.sandbox?, 35 | available_to_admin: false, 36 | } 37 | end 38 | 39 | def api_credentials 40 | @api_credentials ||= SolidusPaypalCommercePlatform::Client.fetch_api_credentials( 41 | auth_code: params.fetch(:authCode), 42 | client_id: params.fetch(:sharedId), 43 | nonce: params.fetch(:nonce), 44 | ) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /templates/spec/requests/products_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Product', type: :request, with_signed_in_user: true do 6 | let!(:product) { create(:product, available_on: 1.year.from_now) } 7 | let(:user) { create(:user) } 8 | 9 | context 'when not admin user' do 10 | it "cannot view non-active products" do 11 | get product_path(product.to_param) 12 | 13 | expect(response.status).to eq(404) 14 | end 15 | 16 | it "provides the current user to the searcher class" do 17 | get products_path 18 | 19 | expect(assigns[:searcher].current_user).to eq user 20 | expect(response.status).to eq(200) 21 | end 22 | end 23 | 24 | context 'when an admin' do 25 | let(:user) { create(:admin_user) } 26 | 27 | # Regression test for https://github.com/spree/spree/issues/1390 28 | it "allows admins to view non-active products" do 29 | get product_path(id: product.to_param) 30 | expect(assigns[:products]).to include(product) 31 | expect(response.status).to eq(200) 32 | end 33 | 34 | # Regression test for https://github.com/spree/spree/issues/2249 35 | it "doesn't error when given an invalid referer" do 36 | # Previously a URI::InvalidURIError exception was being thrown 37 | get product_path(product.to_param), headers: { 'HTTP_REFERER' => 'not|a$url' } 38 | end 39 | end 40 | 41 | context "when invalid search params are passed" do 42 | it "raises ActionController::BadRequest" do 43 | get products_path, params: { search: "blurb" } 44 | expect(response.status).to eq(400) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /templates/app/views/layouts/_top_bar.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render 'shared/search/search_bar', taxons: taxons, taxon: taxon, :locals => { :additional_classes => 'hidden lg:flex' } %> 4 | 7 |
8 | 9 | 10 | 11 | 16 |
17 | 18 |
19 | <%= link_to spree_current_user ? account_path : login_path, title: spree_current_user ? 'Account' : 'Login' do %> 20 | 21 | 22 | 23 | <% end %> 24 |
25 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /templates/app/controllers/user_sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserSessionsController < Devise::SessionsController 4 | # This is included in ControllerHelpers::Order. We just want to call 5 | # it after someone has successfully logged in. 6 | after_action :set_current_order, only: :create 7 | 8 | def create 9 | authenticate_spree_user! 10 | 11 | if spree_user_signed_in? 12 | respond_to do |format| 13 | format.html do 14 | flash[:success] = I18n.t('spree.logged_in_succesfully') 15 | redirect_back_or_default(after_sign_in_path_for(spree_current_user)) 16 | end 17 | format.js { render success_json } 18 | end 19 | else 20 | respond_to do |format| 21 | format.html do 22 | flash.now[:error] = t('devise.failure.invalid') 23 | render :new 24 | end 25 | format.js do 26 | render json: { error: t('devise.failure.invalid') }, 27 | status: :unprocessable_entity 28 | end 29 | end 30 | end 31 | end 32 | 33 | protected 34 | 35 | def translation_scope 36 | 'devise.user_sessions' 37 | end 38 | 39 | private 40 | 41 | def accurate_title 42 | I18n.t('spree.login') 43 | end 44 | 45 | def redirect_back_or_default(default) 46 | redirect_to(session["spree_user_return_to"] || default) 47 | session["spree_user_return_to"] = nil 48 | end 49 | 50 | def success_json 51 | { 52 | json: { 53 | user: spree_current_user, 54 | ship_address: spree_current_user.ship_address, 55 | bill_address: spree_current_user.bill_address 56 | }.to_json 57 | } 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/payment/_paypal_commerce_platform.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "solidus_paypal_commerce_platform/shared/javascript_sdk_tag", locals: {payment_method: payment_method} %> 2 | 3 |
4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | <% unless payment_method.render_only_venmo_standalone? %> 18 | 23 | <% end %> 24 | 25 | <% if payment_method.venmo_standalone_enabled? %> 26 | 31 | <% end %> 32 | 33 |
34 | -------------------------------------------------------------------------------- /templates/app/components/filter_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FilterComponent < ViewComponent::Base 4 | BASE_CLASS = 'filter'.freeze 5 | CSS_CLASS = "#{BASE_CLASS}__list mt-6".freeze 6 | 7 | attr_reader :filter, :search_params 8 | 9 | def initialize(filter:, search_params:) 10 | @filter = filter 11 | @search_params = search_params || {} 12 | end 13 | 14 | def call 15 | safe_join([filter_list_title, filter_list].compact) if filter_list 16 | end 17 | 18 | private 19 | 20 | def filter_list_title 21 | content_tag(:h6, title, class: "#{BASE_CLASS}__title font-sans-md") if title 22 | end 23 | 24 | def filter_list 25 | return @filter_list if @filter_list 26 | return if labels.empty? 27 | 28 | @filter_list = content_tag :ul, class: CSS_CLASS do 29 | safe_join(labels.map { |name, value| filter_list_item(name: name, value: value) }) 30 | end 31 | end 32 | 33 | def filter_list_item(name:, value:) 34 | id = filter_list_item_id(name) 35 | 36 | content_tag(:li, class: 'checkbox-input mb-3') do 37 | concat check_box_tag( 38 | "search[#{filter[:scope].to_s}][]", 39 | value, 40 | filter_list_item_checked?(value), 41 | id: id) 42 | 43 | concat label_tag(id, name) 44 | end 45 | end 46 | 47 | def filter_list_item_id(name) 48 | sanitize_to_id("#{filter[:name]}_#{name}") 49 | end 50 | 51 | def filter_list_item_checked?(value) 52 | search_params[filter[:scope]]&.include?(value.to_s) 53 | end 54 | 55 | def title 56 | filter[:name] 57 | end 58 | 59 | def labels 60 | @labels ||= filter[:labels] || filter[:conds].map { |m,c| [m,m] } 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /.dockerdev/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUBY_VERSION 2 | FROM ruby:$RUBY_VERSION-slim-buster 3 | 4 | ARG PG_VERSION 5 | ARG MYSQL_VERSION 6 | ARG NODE_VERSION 7 | ARG BUNDLER_VERSION 8 | 9 | RUN apt-get update -qq \ 10 | && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ 11 | build-essential \ 12 | gnupg2 \ 13 | curl \ 14 | git \ 15 | imagemagick \ 16 | libmariadb-dev \ 17 | sqlite3 \ 18 | libsqlite3-dev \ 19 | chromium \ 20 | libvips \ 21 | chromium-driver \ 22 | && rm -rf /var/cache/apt/lists/* 23 | 24 | RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ 25 | && echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' $PG_VERSION > /etc/apt/sources.list.d/pgdg.list 26 | 27 | RUN curl -sSL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - 28 | 29 | RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \ 30 | DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ 31 | libpq-dev \ 32 | postgresql-client-$PG_VERSION \ 33 | default-mysql-client \ 34 | nodejs \ 35 | && rm -rf /var/lib/apt/lists/* 36 | 37 | ENV APP_USER=solidus_starter_frontend_user \ 38 | LANG=C.UTF-8 \ 39 | BUNDLE_JOBS=4 \ 40 | BUNDLE_RETRY=3 41 | ENV GEM_HOME=/home/$APP_USER/gems 42 | ENV APP_HOME=/home/$APP_USER/app 43 | ENV PATH=$PATH:$GEM_HOME/bin 44 | 45 | RUN useradd -ms /bin/bash $APP_USER 46 | 47 | RUN gem update --system \ 48 | && gem install bundler:$BUNDLER_VERSION \ 49 | && chown -R $APP_USER:$(id -g $APP_USER) /home/$APP_USER/gems 50 | 51 | USER $APP_USER 52 | 53 | RUN mkdir -p /home/$APP_USER/history 54 | 55 | WORKDIR /home/$APP_USER/app 56 | -------------------------------------------------------------------------------- /templates/app/views/user_registrations/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<%= I18n.t('spree.new_customer') %>

4 | 5 | <%= render 'shared/error_messages', target: resource %> 6 | 7 | <%= form_for resource, as: :spree_user, url: registration_path(resource), html: { class: "space-y-6 mb-12" } do |f| %> 8 |
9 | <%= f.label :email, "#{t("spree.email")}:" %> 10 | <%= f.email_field :email, placeholder: 'name@example.com' %> 11 |
12 | 13 |
14 | <%= f.label :password, "#{t("spree.password")}:" %> 15 | <%= f.password_field :password, placeholder: "p455w0rd" %> 16 |
17 | 18 |
19 | <%= f.label :password_confirmation, "#{t("spree.confirm_password")}:" %> 20 | <%= f.password_field :password_confirmation, placeholder: "p455w0rd" %> 21 |
22 | 23 |
24 | <%= f.button t("spree.create"), 25 | name: :commit, 26 | class: 'w-full w-fit py-3 px-7 rounded-full text-body-sm font-bold leading-none uppercase whitespace-nowrap transiton-colors duration-200 bg-red-500 text-white hover:bg-red-700' 27 | %> 28 |
29 | 30 |
31 | <%= I18n.t("spree.or") %> <%= link_to I18n.t("spree.login_as_existing"), login_path, class:"transition-all duration-300 text-black hover:!text-red-500 dark:text-sand underline" %> 32 |
33 | 34 | <% end %> 35 |
36 |
37 | -------------------------------------------------------------------------------- /templates/spec/system/caching/products_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'products', type: :system, caching: true do 6 | let!(:taxon) { create(:taxon, taxonomy: create(:taxonomy)) } 7 | let!(:product) { create(:product, taxons: [taxon]) } 8 | let!(:product2) { create(:product, taxons: [taxon]) } 9 | 10 | before do 11 | # warm up the cache 12 | visit products_path 13 | 14 | clear_cache_events 15 | end 16 | 17 | it "reads from cache upon a second viewing" do 18 | visit products_path 19 | expect(cache_writes.count).to eq(0) 20 | end 21 | 22 | it "busts the cache when a product is updated" do 23 | product.update(updated_at: 1.day.from_now) 24 | visit products_path 25 | 26 | # Cache rewrites: 27 | # - 1 x products grid updated item 28 | # - 3 x categories in navigation 29 | expect(cache_writes.count).to eq(4) 30 | end 31 | 32 | it "busts the cache when all products are soft-deleted" do 33 | product.discard 34 | product2.discard 35 | visit products_path 36 | 37 | # Cache rewrites: 38 | # - 3 x categories in navigation 39 | expect(cache_writes.count).to eq(3) 40 | end 41 | 42 | it "busts the cache when the newest product is soft-deleted" do 43 | product.discard 44 | visit products_path 45 | 46 | # Cache rewrites: 47 | # - 3 x categories in navigation 48 | expect(cache_writes.count).to eq(3) 49 | end 50 | 51 | it "busts the cache when an older product is soft-deleted" do 52 | product2.discard 53 | visit products_path 54 | 55 | # Cache rewrites: 56 | # - 3 x categories in navigation 57 | expect(cache_writes.count).to eq(3) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /templates/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UsersController < StoreController 4 | skip_before_action :set_current_order, only: :show, raise: false 5 | 6 | include Taxonomies 7 | 8 | def show 9 | load_object 10 | @orders = @user.orders.complete.order('completed_at desc') 11 | end 12 | 13 | def create 14 | @user = Spree::User.new(user_params) 15 | if @user.save 16 | 17 | if current_order 18 | session[:guest_token] = nil 19 | end 20 | 21 | redirect_back_or_default(root_url) 22 | else 23 | render :new 24 | end 25 | end 26 | 27 | def edit 28 | load_object 29 | end 30 | 31 | def update 32 | load_object 33 | if @user.update(user_params) 34 | spree_current_user.reload 35 | redirect_url = account_url 36 | 37 | if params[:user][:password].present? 38 | # this logic needed b/c devise wants to log us out after password changes 39 | if Spree::Auth::Config[:signout_after_password_change] 40 | redirect_url = login_url 41 | else 42 | bypass_sign_in(@user) 43 | end 44 | end 45 | redirect_to redirect_url, notice: I18n.t('spree.account_updated') 46 | else 47 | render :edit 48 | end 49 | end 50 | 51 | def new 52 | authorize! params[:action].to_sym, Spree::User.new 53 | end 54 | 55 | private 56 | 57 | def user_params 58 | params.require(:user).permit(Spree::PermittedAttributes.user_attributes | [:email]) 59 | end 60 | 61 | def load_object 62 | @user ||= Spree::User.find_by(id: spree_current_user&.id) 63 | authorize! params[:action].to_sym, @user 64 | end 65 | 66 | def accurate_title 67 | I18n.t('spree.my_account') 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /templates/spec/system/automatic_promotion_adjustments_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Automatic promotions', type: :system, js: true do 6 | let!(:store) { create(:store) } 7 | let!(:product) do 8 | create(:product, name: "Solidus mug set", price: 20).tap do |product| 9 | product.master.stock_items.update_all count_on_hand: 10 10 | end 11 | end 12 | 13 | let!(:promotion) do 14 | promotion = Spree::Promotion.create!(name: "$10 off when you spend more than $100", apply_automatically: true) 15 | 16 | calculator = Spree::Calculator::FlatRate.new 17 | calculator.preferred_amount = 10 18 | 19 | rule = Spree::Promotion::Rules::ItemTotal.create 20 | rule.preferred_amount = 100 21 | rule.save 22 | 23 | promotion.rules << rule 24 | 25 | action = Spree::Promotion::Actions::CreateAdjustment.create 26 | action.calculator = calculator 27 | action.save 28 | 29 | promotion.actions << action 30 | end 31 | 32 | context "on the cart page" do 33 | before do 34 | visit products_path 35 | click_link product.name 36 | click_button "add-to-cart-button" 37 | end 38 | 39 | it "automatically applies the promotion once the order crosses the threshold" do 40 | fill_in "order_line_items_attributes_0_quantity", with: 6 41 | click_button "Update" 42 | expect(page).to have_content("Promotion ($10 off when you spend more than $100) -$10.00", normalize_ws: true) 43 | 44 | fill_in "order_line_items_attributes_0_quantity", with: 5 45 | click_button "Update" 46 | expect(page).not_to have_content("Promotion ($10 off when you spend more than $100) -$10.00", normalize_ws: true) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /templates/spec/controllers/user_passwords_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe UserPasswordsController, type: :controller do 6 | let(:token) { 'some_token' } 7 | 8 | before { @request.env['devise.mapping'] = Devise.mappings[:spree_user] } 9 | 10 | describe 'GET edit' do 11 | context 'when the user token has not been specified' do 12 | it 'redirects to the new session path' do 13 | get :edit 14 | expect(response).to redirect_to( 15 | 'http://test.host/user/sign_in' 16 | ) 17 | end 18 | 19 | it 'flashes an error' do 20 | get :edit 21 | expect(flash[:alert]).to include( 22 | "You can't access this page without coming from a password reset " \ 23 | 'email' 24 | ) 25 | end 26 | end 27 | 28 | context 'when the user token has been specified' do 29 | it 'does something' do 30 | get :edit, params: { reset_password_token: token } 31 | expect(response.code).to eq('200') 32 | end 33 | end 34 | end 35 | 36 | context '#update' do 37 | context 'when updating password with blank password' do 38 | it 'shows error flash message, sets spree_user with token and re-displays password edit form' do 39 | put :update, params: { spree_user: { password: '', password_confirmation: '', reset_password_token: token } } 40 | expect(assigns(:spree_user).is_a?(Spree::User)).to eq true 41 | expect(assigns(:spree_user).reset_password_token).to eq token 42 | expect(flash[:error]).to eq I18n.t(:cannot_be_blank, scope: [:devise, :user_passwords, :spree_user]) 43 | expect(response).to render_template :edit 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /templates/app/views/layouts/_footer.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | <%= image_tag "logo_full_with_label.svg", class: "mx-auto w-52 mb-7 dark:hidden" %> 6 | <%= image_tag "logo_full_with_label_white.svg", class: "mx-auto w-52 mb-7 hidden dark:block" %> 7 | <%= render partial: "shared/call_to_action", :locals => { :label => "Visit Solidus.io", :url => 'https://solidus.io', :target => '_blank', :additional_classes => "block mx-auto" } %> 8 |
9 |
10 | <%= render "shared/social_icons" %> 11 |
12 |
13 | <% 14 | available_locales = current_store.available_locales.map do |locale| 15 | [ 16 | I18n.t('spree.i18n.this_file_language', 17 | locale: locale, 18 | default: locale.to_s, 19 | fallback: false), locale 20 | ] 21 | end.sort 22 | %> 23 | <% if available_locales.many? %> 24 |
25 | <%= form_tag select_locale_path do %> 26 |
27 | <%= label_tag :switch_to_locale, "#{t('spree.i18n.language')}:" %> 28 | <%= select_tag(:switch_to_locale, options_for_select(available_locales, I18n.locale), data: { action: "locale-selector#submitForm", "locale-selector-target" => "selector" }) %> 29 |
30 | <% end %> 31 |
32 | <% end %> 33 |
34 |
35 | -------------------------------------------------------------------------------- /templates/spec/components/link_to_cart_component_spec.rb: -------------------------------------------------------------------------------- 1 | require "solidus_starter_frontend_spec_helper" 2 | 3 | RSpec.describe LinkToCartComponent, type: :component do 4 | let(:text) { '' } 5 | 6 | let(:link_to_cart_component) do 7 | described_class.new(text) 8 | end 9 | 10 | let(:current_order) { nil } 11 | 12 | context 'when rendered' do 13 | before do 14 | expect(link_to_cart_component) 15 | .to receive(:current_order).at_least(:once).and_return(current_order) 16 | 17 | render_inline(link_to_cart_component) 18 | end 19 | 20 | describe 'concerning current_order' do 21 | context 'when current_order is nil' do 22 | let(:current_order) { nil } 23 | 24 | it 'renders an empty cart' do 25 | link = page.find('a.cart-info') 26 | 27 | aggregate_failures do 28 | expect(link).to_not be_nil 29 | expect(link.text).to be_empty 30 | end 31 | end 32 | end 33 | 34 | context 'when there is a current order' do 35 | let(:line_items_count) { 0 } 36 | let(:current_order) { create(:order_with_line_items, line_items_count: line_items_count) } 37 | 38 | context 'when the current order has no items' do 39 | let(:line_items_count) { 0 } 40 | 41 | it 'renders an empty cart' do 42 | expect(page.find('a.cart-info').text).to be_empty 43 | end 44 | end 45 | 46 | context 'when the current order has an item' do 47 | let(:line_items_count) { 1 } 48 | 49 | it 'renders a cart with its item count' do 50 | expect(page.find('a.cart-info.full .link-text')) 51 | .to have_content(line_items_count) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /templates/app/views/autocomplete_results/_autocomplete_results.html.erb: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /templates/spec/system/order_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'orders', type: :system do 6 | let(:order) { Spree::TestingSupport::OrderWalkthrough.up_to(:complete) } 7 | let(:user) { create(:user) } 8 | 9 | before do 10 | order.update_attribute(:user_id, user.id) 11 | allow_any_instance_of(OrdersController).to receive_messages(spree_current_user: user) 12 | end 13 | 14 | it "can visit an order" do 15 | # Regression test for current_user call on orders/show 16 | visit order_path(order) 17 | end 18 | 19 | it "should display line item price" do 20 | # Regression test for https://github.com/spree/spree/issues/2772 21 | line_item = order.line_items.first 22 | line_item.target_shipment = create(:shipment) 23 | line_item.price = 19.00 24 | line_item.save! 25 | 26 | visit order_path(order) 27 | 28 | # Tests view spree/shared/_order_details 29 | within '.order-item__price-single' do 30 | expect(page).to have_content "19.00" 31 | end 32 | end 33 | 34 | it "should have credit card info if paid with credit card" do 35 | create(:payment, order: order) 36 | visit order_path(order) 37 | within '.payment-info' do 38 | expect(page).to have_content "Ending in 1111" 39 | end 40 | end 41 | 42 | it "should have payment method name visible if not paid with credit card" do 43 | create(:check_payment, order: order) 44 | visit order_path(order) 45 | within '.payment-info' do 46 | expect(page).to have_content "Check" 47 | end 48 | end 49 | 50 | it "should return the correct title when displaying a completed order" do 51 | visit order_path(order) 52 | 53 | within 'h1' do 54 | expect(page).to have_content("#{I18n.t('spree.order')} #{order.number}") 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | mysql: 5 | image: mysql:8.0 6 | command: --default-authentication-plugin=mysql_native_password 7 | environment: 8 | MYSQL_ROOT_PASSWORD: password 9 | volumes: 10 | - mysql:/var/lib/mysql:cached 11 | 12 | postgres: 13 | image: postgres:13.2 14 | environment: 15 | POSTGRES_USER: root 16 | POSTGRES_PASSWORD: password 17 | POSTGRES_DB: solidus_starter_frontend_solidus_test 18 | volumes: 19 | - postgres:/var/lib/postgresql/data:cached 20 | 21 | app: 22 | platform: linux/amd64 23 | build: 24 | context: .dockerdev 25 | dockerfile: Dockerfile 26 | args: 27 | RUBY_VERSION: "2.7.2" 28 | PG_VERSION: 13 29 | NODE_VERSION: 14 30 | BUNDLER_VERSION: 1.17.3 31 | image: solidus_starter_frontend-0.1.0 32 | command: bash -c "(bundle check || bundle) && tail -f /dev/null" 33 | environment: 34 | CAPYBARA_JS_DRIVER: selenium_chrome_headless_docker_friendly 35 | DB_USERNAME: root 36 | DB_PASSWORD: password 37 | DB_ALL: "1" 38 | DB_MYSQL_HOST: mysql 39 | DB_POSTGRES_HOST: postgres 40 | HISTFILE: "/home/solidus_starter_frontend_user/history/bash_history" 41 | MYSQL_HISTFILE: "/home/solidus_starter_frontend_user/history/mysql_history" 42 | ports: 43 | - "${SANDBOX_PORT:-3000}:${SANDBOX_PORT:-3000}" 44 | volumes: 45 | - .:/home/solidus_starter_frontend_user/app:delegated 46 | - bundle:/home/solidus_starter_frontend_user/gems:cached 47 | - history:/home/solidus_starter_frontend_user/history:cached 48 | - .dockerdev/.psqlrc:/home/solidus_starter_frontend_user/.psqlrc:cached 49 | tty: true 50 | stdin_open: true 51 | tmpfs: 52 | - /tmp 53 | depends_on: 54 | - mysql 55 | - postgres 56 | 57 | volumes: 58 | bundle: 59 | history: 60 | postgres: 61 | mysql: 62 | -------------------------------------------------------------------------------- /templates/app/controllers/products_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ProductsController < StoreController 4 | before_action :load_product, only: :show 5 | before_action :load_taxon, only: :index 6 | 7 | helper 'spree/products', 'spree/taxons', 'taxon_filters' 8 | 9 | respond_to :html 10 | 11 | rescue_from Spree::Config.searcher_class::InvalidOptions do |error| 12 | raise ActionController::BadRequest.new, error.message 13 | end 14 | 15 | def index 16 | @searcher = build_searcher(params.merge(include_images: true)) 17 | @products = @searcher.retrieve_products 18 | end 19 | 20 | def show 21 | @variants = @product. 22 | variants_including_master. 23 | display_includes. 24 | with_prices(current_pricing_options). 25 | includes([:option_values, :images]) 26 | 27 | @product_properties = @product.product_properties.includes(:property) 28 | @taxon = Spree::Taxon.find(params[:taxon_id]) if params[:taxon_id] 29 | @similar_products = @product.similar_products 30 | end 31 | 32 | private 33 | 34 | def accurate_title 35 | if @product 36 | @product.meta_title.blank? ? @product.name : @product.meta_title 37 | else 38 | super 39 | end 40 | end 41 | 42 | def load_product 43 | if spree_current_user.try(:has_spree_role?, "admin") 44 | @products = Spree::Product.with_discarded 45 | else 46 | @products = Spree::Product.available 47 | end 48 | @product = @products.friendly.find(params[:id]) 49 | 50 | # Redirects to the correct product URL if the requested slug does not match the current product's slug. 51 | # This ensures that outdated or ID URLs always resolve to the latest canonical URL. 52 | redirect_to @product, status: :moved_permanently if params[:id] != @product.slug 53 | end 54 | 55 | def load_taxon 56 | @taxon = Spree::Taxon.find(params[:taxon]) if params[:taxon].present? 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /templates/app/views/shared/search/_search_bar.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | base_classes = "flex items-center gap-x-1" 3 | form_classes = defined?(locals) && locals[:additional_classes].present? ? "#{base_classes} #{locals[:additional_classes]}" : base_classes 4 | 5 | form_id = defined?(locals) && locals[:search_input_id].present? ? locals[:search_input_id] : 'search-input-area' 6 | 7 | results_list_id = defined?(locals) && locals[:results_list_id].present? ? locals[:results_list_id] : 'autocomplete-results' 8 | %> 9 | 10 | <%= content_tag :div, 11 | "data-controller": "search", 12 | "data-search-current-class": "text-primary" do %> 13 | <%= form_tag autocomplete_results_path, method: :get, class: form_classes, id: "search-form", 14 | "data-turbo": true, 15 | "data-turbo-stream": true, 16 | "data-search-target": "form" do %> 17 | 18 | <%= search_field_tag :keywords, params[:keywords], placeholder: "Search", id: form_id, class: 'bg-transparent !border-b border-gray-300 px-0 py-1 text-sm w-full lg:min-w-[200px] focus:ring-0 focus:border-b-red-500', autocomplete: "off", 19 | "data-action": "input->search#fetchResults 20 | keydown.down->search#nextResult 21 | keydown.up->search#previousResult 22 | keydown.enter->search#openResult:prevent:stop 23 | keydown.esc->search#reset:prevent:stop 24 | click@window->search#focusOut", 25 | "data-search-target": "keywords" %> 26 | 27 | 28 | <%= button_tag type: 'submit', class: "search-bar__button", title: 'Search', "data-search-id-param": form_id do %> 29 | 30 | 31 | 32 | <% end %> 33 | <% end %> 34 | 35 | <% end %> 36 | 37 | 38 | -------------------------------------------------------------------------------- /templates/app/views/user_sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% @body_id = 'login' %> 2 | 3 |
4 |
5 |

<%= I18n.t('spree.login_as_existing') %>

6 |
7 | <%= I18n.t("spree.or") %> <%= link_to I18n.t("spree.create_a_new_account"), signup_path, class:"transition-all duration-300 text-black hover:!text-red-500 dark:text-sand underline" %> 8 |
9 | 10 | <%= form_for Spree::User.new, as: :spree_user, url: create_new_session_path, html: { class: "space-y-6" } do |f| %> 11 |
12 | <%= f.label :email, "#{t("spree.email")}:" %> 13 | <%= f.email_field :email, placeholder: 'name@example.com' %> 14 |
15 | 16 |
17 | <%= f.label :password, "#{t("spree.password")}:" %> 18 | <%= f.password_field :password, placeholder: "p455w0rd" %> 19 |
20 | 21 |
22 |
23 | <%= f.check_box :remember_me, tabindex: 3 %> 24 | <%= f.label :remember_me, I18n.t("spree.remember_me"), class: "mb-0" %> 25 |
26 |
27 | <%= link_to I18n.t("spree.forgot_password"), recover_password_path, class:"transition-all duration-300 text-black hover:!text-red-500 dark:text-sand underline" %> 28 |
29 | 30 |
31 | <%= f.button( 32 | t("spree.login"), 33 | class: 'w-full py-3 px-7 rounded-full text-body-sm font-bold leading-none uppercase whitespace-nowrap transiton-colors duration-200 bg-red-500 text-white hover:bg-red-700', 34 | name: :commit 35 | ) %> 36 |
37 | 38 | <% end %> 39 |
40 |
41 | -------------------------------------------------------------------------------- /templates/spec/requests/orders_ability_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Order permissions', type: :request do 6 | let(:order) { create(:order, user: nil, store: store) } 7 | let!(:store) { create(:store) } 8 | let(:variant) { create(:variant) } 9 | 10 | it 'understands order routes with token' do 11 | expect(token_order_path('R123456', 'ABCDEF')).to eq('/orders/R123456/token/ABCDEF') 12 | end 13 | 14 | context 'when an order exists in the cookies.signed', with_guest_session: true do 15 | before { order.update(guest_token: nil) } 16 | 17 | context "#show" do 18 | it "checks against the specified order" do 19 | get order_path(id: order.number) 20 | expect(response).to redirect_to(login_path) 21 | end 22 | end 23 | end 24 | 25 | context 'when no authenticated user' do 26 | let(:order) { create(:order, number: 'R123') } 27 | let(:another_order) { create(:order) } 28 | 29 | context '#show' do 30 | context 'when token parameter present' do 31 | it 'always override existing token when passing a new one' do 32 | get order_path(id: another_order.number, token: another_order.guest_token) 33 | jar = ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash) 34 | expect(jar.signed[:guest_token]).to eq another_order.guest_token 35 | end 36 | 37 | it 'stores as guest_token in session' do 38 | get order_path(id: order.number, token: order.guest_token) 39 | jar = ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash) 40 | expect(jar.signed[:guest_token]).to eq order.guest_token 41 | end 42 | end 43 | 44 | context 'when no token present' do 45 | it 'responds with 404' do 46 | get order_path(id: 'R123') 47 | 48 | expect(response.status).to eq(404) 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/payment/_gateway.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% param_prefix = "payment_source[#{payment_method.id}]" %> 3 | 4 |
5 | <% "name_on_card_#{payment_method.id}".tap do |id| %> 6 | <%= label_tag id, "#{t('spree.name_on_card')}:" %> 7 | 8 | <%= text_field_tag "#{param_prefix}[name]", 9 | @order.billing_name, 10 | id: id, 11 | autocomplete: "cc-name" 12 | %> 13 | <% end %> 14 |
15 | 16 |
17 | <% "card_number_#{payment_method.id}".tap do |id| %> 18 | <%= label_tag id, "#{t('spree.card_number')}:" %> 19 | 20 | <%= text_field_tag "#{param_prefix}[number]", 21 | nil, 22 | id: id, 23 | placeholder: "xxxx xxxx xxxx xxxx", 24 | autocomplete: "cc-number" 25 | %> 26 | <% end %> 27 |
28 | 29 |
30 |
31 | <% "card_expiry_#{payment_method.id}".tap do |id| %> 32 | <%= label_tag id, "#{t('spree.expiration')}:" %> 33 | 34 | <%= text_field_tag "#{param_prefix}[expiry]", 35 | nil, 36 | id: id, 37 | placeholder: "MM / YY" 38 | %> 39 | <% end %> 40 |
41 | 42 |
43 | <% "card_code_#{payment_method.id}".tap do |id| %> 44 | <%= label_tag id, "#{t('spree.card_code')}:" %> 45 | 46 | <%= text_field_tag "#{param_prefix}[verification_value]", 47 | nil, 48 | id: id 49 | %> 50 | <% end %> 51 |
52 |
53 | 54 | <% if @order.bill_address %> 55 | <%= fields_for "#{param_prefix}[address_attributes]", @order.bill_address do |f| %> 56 | <%= render 'address/form_hidden', form: f %> 57 | <% end %> 58 | <% end %> 59 | 60 | <%= hidden_field_tag "#{param_prefix}[cc_type]", '', id: "cc_type", class: 'ccType' %> 61 |
62 | -------------------------------------------------------------------------------- /templates/spec/controllers/spree/base_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe Spree::BaseController, type: :controller do 6 | describe '#unauthorized_redirect' do 7 | controller(described_class) do 8 | def index; authorize!(:read, :something); end 9 | end 10 | 11 | before do 12 | allow(Spree::Auth::Engine).to receive(:redirect_back_on_unauthorized?).and_return(true) 13 | end 14 | 15 | context "when user is logged in" do 16 | before { sign_in(create(:user)) } 17 | 18 | context "when http_referrer is not present" do 19 | it "redirects to unauthorized path" do 20 | get :index 21 | expect(response).to redirect_to(unauthorized_path) 22 | end 23 | end 24 | 25 | context "when http_referrer is present" do 26 | let(:request_referer_path) { '/redirect' } 27 | let(:request_referer) { "#{request.protocol}#{request.host}#{request_referer_path}" } 28 | 29 | before { request.env['HTTP_REFERER'] = request_referer } 30 | 31 | it "redirects back" do 32 | get :index 33 | expect(response).to redirect_to(request_referer_path) 34 | end 35 | end 36 | end 37 | 38 | context "when user is not logged in" do 39 | context "when http_referrer is not present" do 40 | it "redirects to login path" do 41 | get :index 42 | expect(response).to redirect_to(login_path) 43 | end 44 | end 45 | 46 | context "when http_referrer is present" do 47 | let(:request_referer_path) { '/redirect' } 48 | let(:request_referer) { "#{request.protocol}#{request.host}#{request_referer_path}" } 49 | 50 | before { request.env['HTTP_REFERER'] = request_referer } 51 | 52 | it "redirects back" do 53 | get :index 54 | expect(response).to redirect_to(request_referer_path) 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /templates/app/views/orders/_order_details.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% if order.has_checkout_step?("address") %> 4 |
5 | <%= render( 6 | 'orders/address_overview', 7 | address: order.bill_address, 8 | title: t('spree.billing_address') 9 | ) %> 10 |
11 | <% end %> 12 | 13 | <% if order.has_checkout_step?("delivery") %> 14 |
15 | <%= render( 16 | 'orders/address_overview', 17 | address: order.ship_address, 18 | title: t('spree.shipping_address') 19 | ) %> 20 |
21 | <% end %> 22 | 23 | <% if order.has_checkout_step?("delivery") %> 24 |
25 | <%= render( 26 | 'orders/order_shipments', 27 | order: order, 28 | title: t('spree.shipments') 29 | ) %> 30 |
31 | <% end %> 32 | 33 | <% if order.has_checkout_step?("payment") %> 34 |
35 | <%= render( 36 | 'orders/payment_info', 37 | order: order 38 | ) %> 39 |
40 | <% end %> 41 |
42 | 43 | <% 44 | wrapper_grid_classes = ["col-span-full bg-sand border-[1px] border-gray-300 p-3 text-black lg:p-6 lg:col-start-2 lg:col-span-10 xl:col-start-3 xl:col-span-8"] 45 | wrapper_grid_dark_classes = ["dark:bg-black dark:border-gray-400 dark:text-sand"] 46 | %> 47 | 48 |
49 | <%= render 'orders/order_items', order: order, classes: ["rounded-t-xl border-b-0", wrapper_grid_classes, wrapper_grid_dark_classes] %> 50 | <%= render 'orders/line_items', order: order, classes: ["rounded-b-xl border-t-0 py-0", wrapper_grid_classes, wrapper_grid_dark_classes] %> 51 |
52 |
53 | -------------------------------------------------------------------------------- /templates/app/views/layouts/storefront.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | <%== meta_data_tags %> 8 | <%= simple_canonical_tag %> 9 | <%= csrf_meta_tags %> 10 | 11 | <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> 12 | <%= javascript_include_tag 'solidus_starter_frontend' %> 13 | <%= stylesheet_link_tag 'solidus_starter_frontend', media: 'screen' %> 14 | 15 | <%= javascript_importmap_tags %> 16 | 17 | <%= favicon_link_tag '/storefront_favicon.svg', type: 'image/svg+xml', rel: 'icon' %> 18 | <%= favicon_link_tag '/storefront_favicon.ico', rel: 'icon', type: 'image/ico' %> 19 | 20 | 24 | 25 | <%= yield :head %> 26 | 27 | 28 | 29 |
30 | <%= render 'layouts/header', taxons: @taxons, taxon: @taxon %> 31 |
32 | <%= render BreadcrumbsComponent.new( 33 | taxon: @product&.primary_taxon || @taxon, 34 | item_classes: 'text-body-xs text-gray-500', 35 | separator_classes: 'text-body-xs text-gray-500 ml-2', 36 | container_classes: 'flex gap-2', 37 | wrapper_classes: 'wrapper mt-4', 38 | order: @order 39 | ) %> 40 | <% if flash_messages.present? %> 41 | <%= flash_messages %> 42 | <% end %> 43 | <%= yield %> 44 |
45 | <%= render 'layouts/footer' %> 46 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /templates/app/javascript/controllers/search_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["form", "keywords", "results", "result"]; 5 | static classes = ["current"]; 6 | 7 | // This is needed to restore the current result index when the 8 | // results are updated after a search. 9 | resultsTargetConnected() { 10 | this.currentResultIndex = 0; 11 | this.render(); 12 | } 13 | 14 | fetchResults() { 15 | // this.keywords.length == 0 means the string has been totally deleted 16 | // or the 'x' cancel cross has been clicked. 17 | // the html input type search fires a search event 18 | // in both cases, either a character is typed, or the cancel cross is clicked. 19 | 20 | if (this.keywords.length < 2) { 21 | this.reset(); 22 | return; 23 | } 24 | 25 | clearTimeout(this.timeout); 26 | this.timeout = setTimeout(() => this.formTarget.requestSubmit(), 500); 27 | } 28 | 29 | nextResult() { 30 | if (this.currentResultIndex < this.resultTargets.length - 1) { 31 | this.currentResultIndex++; 32 | this.render(); 33 | } 34 | } 35 | 36 | previousResult() { 37 | if (this.currentResultIndex > 0) { 38 | this.currentResultIndex--; 39 | this.render(); 40 | } 41 | } 42 | 43 | openResult() { 44 | this.resultTargets[this.currentResultIndex].firstElementChild.click(); 45 | } 46 | 47 | focusOut(event) { 48 | if (!this.formTarget.contains(event.target)) { 49 | this.reset(); 50 | } 51 | } 52 | 53 | reset() { 54 | this.currentResultIndex = 0; 55 | if (this.hasResultsTarget) { 56 | this.resultsTarget.innerHTML = ""; 57 | } 58 | } 59 | 60 | render() { 61 | this.resultTargets.forEach((element, index) => { 62 | element.classList.toggle( 63 | this.currentClass, 64 | index == this.currentResultIndex 65 | ); 66 | }); 67 | } 68 | 69 | get keywords() { 70 | return this.keywordsTarget.value; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /templates/spec/requests/user_update_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'User update', type: :request do 6 | context 'CSRF protection' do 7 | %i[exception reset_session null_session].each do |strategy| 8 | # Completely clean the configuration of forgery protection for the 9 | # controller and reset it after the expectations. However, besides `:with`, 10 | # the options given to `protect_from_forgery` are processed on the fly. 11 | # I.e., there's no way to retain them. The initial setup corresponds to the 12 | # dummy application, which uses the default Rails skeleton in that regard. 13 | # So, if at some point Rails changed the given options, we should update it 14 | # here. 15 | around do |example| 16 | controller = UsersController 17 | old_allow_forgery_protection_value = controller.allow_forgery_protection 18 | old_forgery_protection_strategy = controller.forgery_protection_strategy 19 | controller.skip_forgery_protection 20 | controller.allow_forgery_protection = true 21 | controller.protect_from_forgery with: strategy 22 | 23 | example.run 24 | 25 | controller.allow_forgery_protection = old_allow_forgery_protection_value 26 | controller.forgery_protection_strategy = old_forgery_protection_strategy 27 | end 28 | 29 | it "is not possible to take account over with the #{strategy} forgery protection strategy" do 30 | user = create(:user, email: 'legit@mail.com', password: 'password') 31 | 32 | post '/login', params: "spree_user[email]=legit@mail.com&spree_user[password]=password" 33 | begin 34 | put '/users/123456', params: 'user[email]=hacked@example.com' 35 | rescue 36 | # testing that the account is not compromised regardless of any raised 37 | # exception 38 | end 39 | 40 | expect(user.reload.email).to eq('legit@mail.com') 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /templates/app/views/users/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render 'shared/error_messages', target: @user %> 3 | 4 |
5 |

<%= t('spree.my_account') %>

6 | 7 |
8 | <%= @user.email %> 9 | <%= form_with(url: logout_path, method: Devise.sign_out_via, local: true) do %> 10 | <%= button_tag(t("spree.logout"), class: 'transition-all duration-300 text-black hover:!text-red-500 dark:text-sand underline') %> 11 | <% end %> 12 |
13 |
14 | 15 |
16 | <%= render 'users/users_menu' %> 17 |
18 |

<%= t('spree.editing_user') %>

19 | 20 |
21 | <%= form_for @user, html: { class: "space-y-6 mb-12 col-span-full md:col-span-6" } do |f| %> 22 | 23 |
24 | <%= f.label :email, "#{t("spree.email")}:" %> 25 | <%= f.email_field :email %> 26 |
27 | 28 |
29 | <%= f.label :password, "#{t("spree.password")}:" %> 30 | <%= f.password_field :password %> 31 |
32 | 33 |
34 | <%= f.label :password_confirmation, "#{t("spree.confirm_password")}:" %> 35 | <%= f.password_field :password_confirmation %> 36 |
37 | 38 |
39 | <%= f.button t("spree.update"), 40 | name: :commit, 41 | class: 'w-fit py-3 px-7 rounded-full text-body-sm font-bold leading-none uppercase whitespace-nowrap transiton-colors duration-200 bg-red-500 text-white hover:bg-red-700' 42 | %> 43 |
44 | <% end %> 45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /templates/app/views/cart_line_items/product_selection/_option_type.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | classes = { 3 | "color" => "flex items-center justify-center rounded-full cursor-pointer w-8 h-8 border border-gray-50 transition-colors duration-150 [&>span]:hidden hover:border-gray-300 peer-checked:outline peer-checked:outline-2 peer-checked:outline-offset-2 peer-checked:outline-red-500", 4 | "size" => "flex items-center justify-center rounded border-2 border-gray-50 bg-gray-50 py-3 px-6 cursor-pointer font-sans-md text-body text-black hover:bg-white transition-colors duration-150 peer-checked:border-red-500 peer-checked:bg-white" 5 | } 6 | %> 7 | 8 |
9 |
10 |
<%= option_type.presentation %>: 11 | 12 | <%= option_values(product: product, option_type: option_type).first&.presentation %> 13 | 14 |
15 | 16 |
17 | <% option_values(product: product, option_type: option_type).each_with_index do |option_value, i| %> 18 | 34 | <% end %> 35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /templates/app/assets/javascripts/product.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', () => { 2 | const radios = document.querySelectorAll("[data-js='variant-radio']"); 3 | const thumbnailsLinks = document 4 | .querySelectorAll("[data-js='product-thumbnail'] a, [data-js='variant-thumbnail'] a"); 5 | const productImage = document.querySelector("[data-js='product-main-image']"); 6 | const variantsThumbnails = document.querySelectorAll("[data-js='variant-thumbnail']"); 7 | 8 | if (radios.length > 0) { 9 | const selectedRadio = document.querySelector("[data-js='variant-radio'][checked='checked']"); 10 | updateVariantPrice(selectedRadio); 11 | updateVariantImages(selectedRadio.value); 12 | } 13 | 14 | radios.forEach(radio => { 15 | radio.addEventListener('click', () => { 16 | updateVariantPrice(radio); 17 | updateVariantImages(radio.value); 18 | }); 19 | }); 20 | 21 | thumbnailsLinks.forEach(thumbnailLink => { 22 | thumbnailLink.addEventListener('click', (event) => { 23 | event.preventDefault(); 24 | updateProductImage(thumbnailLink.href); 25 | }); 26 | }); 27 | 28 | function updateVariantPrice(variant) { 29 | const variantPrice = variant.dataset.jsPrice; 30 | if (variantPrice) { 31 | document.querySelector("[data-js='price']").innerHTML = variantPrice; 32 | } 33 | }; 34 | 35 | function updateVariantImages(variantId) { 36 | selector = "[data-js='variant-thumbnail'][data-js-id='" + variantId + "']"; 37 | variantsThumbnailsToDisplay = document.querySelectorAll(selector); 38 | 39 | variantsThumbnails.forEach(thumbnail => { 40 | thumbnail.style.display = 'none'; 41 | }); 42 | 43 | variantsThumbnailsToDisplay.forEach(thumbnail => { 44 | thumbnail.style.display = 'list-item'; 45 | }); 46 | 47 | if(variantsThumbnailsToDisplay.length) { 48 | variantFirstImage = variantsThumbnailsToDisplay[0].querySelector('a').href 49 | updateProductImage(variantFirstImage); 50 | } 51 | }; 52 | 53 | function updateProductImage(imageSrc) { 54 | productImage.src = imageSrc; 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /templates/app/views/carts/_cart_item.html.erb: -------------------------------------------------------------------------------- 1 | <% variant = line_item.variant %> 2 | 3 | <%= order_form.fields_for :line_items, line_item do |item_form| %> 4 |
5 |
6 | <%= link_to( 7 | render(ImageComponent.new( 8 | image: variant.gallery.images.first || variant.product.gallery.images.first, 9 | size: :small 10 | )), 11 | variant.product 12 | ) %> 13 |
14 | 15 |
16 | <%= render( 17 | 'orders/item_info', 18 | line_item: line_item, 19 | variant: variant, 20 | classes: ['cart-item__info'] 21 | ) %> 22 |
23 |

<%= line_item.display_amount.to_html unless line_item.quantity.nil? %>

24 |
25 |
26 | 27 |
28 |
29 |
30 | <%= item_form.number_field :quantity %> 31 |
32 | 33 | 36 | 37 |
38 | <%= render 'carts/cart_item_remove', order_form: order_form, item_form: item_form, line_item: line_item %> 39 |
40 |
41 | 42 | 45 |
46 |
47 | <% end %> 48 | -------------------------------------------------------------------------------- /templates/spec/requests/locale_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'Locale', type: :request do 6 | include_context "fr locale" 7 | 8 | context 'switch_to_locale specified' do 9 | context "available locale" do 10 | it 'sets locale and redirects' do 11 | get locale_set_path, params: { switch_to_locale: 'fr' } 12 | expect(I18n.locale).to eq :fr 13 | expect(response).to redirect_to('/') 14 | expect(session[:locale]).to eq('fr') 15 | expect(flash[:notice]).to eq(I18n.t("spree.locale_changed")) 16 | end 17 | end 18 | 19 | context "unavailable locale" do 20 | it 'does not change locale and redirects' do 21 | get locale_set_path, params: { switch_to_locale: 'klingon' } 22 | expect(I18n.locale).to eq :en 23 | expect(response).to redirect_to('/') 24 | expect(flash[:error]).to eq(I18n.t("spree.locale_not_changed")) 25 | end 26 | end 27 | end 28 | 29 | context 'locale specified' do 30 | context "available locale" do 31 | it 'sets locale and redirects' do 32 | get locale_set_path, params: { locale: 'fr' } 33 | expect(I18n.locale).to eq :fr 34 | expect(response).to redirect_to('/') 35 | expect(flash[:notice]).to eq(I18n.t("spree.locale_changed")) 36 | end 37 | end 38 | 39 | context "unavailable locale" do 40 | it 'does not change locale and redirects' do 41 | get locale_set_path, params: { locale: 'klingon' } 42 | expect(I18n.locale).to eq :en 43 | expect(response).to redirect_to('/') 44 | expect(flash[:error]).to eq(I18n.t("spree.locale_not_changed")) 45 | end 46 | end 47 | end 48 | 49 | context 'both locale and switch_to_locale specified' do 50 | it 'uses switch_to_locale value' do 51 | get locale_set_path, params: { locale: 'en', switch_to_locale: 'fr' } 52 | expect(I18n.locale).to eq :fr 53 | expect(response).to redirect_to('/') 54 | expect(flash[:notice]).to eq(I18n.t("spree.locale_changed")) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /templates/spec/components/image_component_spec.rb: -------------------------------------------------------------------------------- 1 | require "solidus_starter_frontend_spec_helper" 2 | 3 | RSpec.describe ImageComponent, type: :component do 4 | let(:page_image) { page.find('img') } 5 | 6 | context 'when rendered' do 7 | def assets_prefix 8 | @assets_prefix ||= Rails.application.config.assets.prefix 9 | end 10 | 11 | before do 12 | render_inline(described_class.new(arguments)) 13 | end 14 | 15 | context 'when no arguments are provided' do 16 | let(:arguments) { { } } 17 | 18 | it 'renders a placeholder' do 19 | expect(page).to have_selector('div.image-placeholder.mini') 20 | end 21 | end 22 | 23 | context 'when an image is provided' do 24 | let(:alt) { 'some-alt' } 25 | let(:image) { build(:image, alt: alt) } 26 | let(:arguments) { { image: image } } 27 | 28 | context 'when the image has an alt' do 29 | let(:alt) { 'some-alt' } 30 | 31 | it 'renders the image' do 32 | expect(page_image['alt']).to eq(alt) 33 | expect(page_image['src']).to match(%r{#{assets_prefix}/noimage/mini-.*.png}) 34 | end 35 | end 36 | 37 | context 'when the image does not have an alt' do 38 | let(:alt) { nil } 39 | 40 | it 'renders the image' do 41 | expect(page_image['alt']).to be_nil 42 | expect(page_image['src']).to match(%r{#{assets_prefix}/noimage/mini-.*.png}) 43 | end 44 | end 45 | end 46 | 47 | context 'when all the required arguments are provided' do 48 | let(:arguments) do 49 | { 50 | image: build(:image), 51 | size: :small, 52 | itemprop: 'some-itemprop', 53 | classes: ['some-class'], 54 | data: { key: 'value' }, 55 | } 56 | end 57 | 58 | it 'renders the image' do 59 | expect(page_image['class']).to eq('some-class') 60 | expect(page_image['itemprop']).to eq('some-itemprop') 61 | expect(page_image['data-key']).to eq('value') 62 | expect(page_image['src']).to match(%r{#{assets_prefix}/noimage/small-.*.png}) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /templates/spec/system/first_order_promotion_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_starter_frontend_spec_helper' 4 | 5 | RSpec.describe 'First Order promotion', type: :system do 6 | include SolidusStarterFrontend::System::CheckoutHelpers 7 | 8 | let!(:promotion) do 9 | FactoryBot.create( 10 | :promotion_with_first_order_rule, 11 | :with_order_adjustment, 12 | code: "FIRSTONEFREE", 13 | per_code_usage_limit: 10 14 | ) 15 | end 16 | 17 | before do 18 | create(:store) 19 | product = FactoryBot.create(:product) 20 | visit products_path 21 | click_link product.name 22 | click_button "Add To Cart" 23 | end 24 | 25 | it 'Adding first order promotion to cart and checking out as guest' do 26 | fill_in "Coupon code", with: "FIRSTONEFREE" 27 | click_button "Apply Code" 28 | expect(page).to have_content("The coupon code was successfully applied to your order") 29 | 30 | within("#cart_adjustments") do 31 | expect(page).to have_content("-$10.00") 32 | end 33 | end 34 | 35 | it 'Trying to reuse first order promotion', js: true do 36 | previous_user = FactoryBot.create( 37 | :user, 38 | email: "sam@tom.com" 39 | ) 40 | _previous_order = create(:completed_order_with_totals, user: previous_user) 41 | fill_in "Coupon code", with: "FIRSTONEFREE" 42 | click_button "Apply Code" 43 | expect(page).to have_content("The coupon code was successfully applied to your order") 44 | checkout_as_guest 45 | fill_in "Customer email", with: "sam@tom.com" 46 | fill_in_address 47 | click_on "Save and Continue" 48 | expect(page).to_not have_content("#summary-order-charges") 49 | end 50 | 51 | def fill_in_address 52 | address = "order_bill_address_attributes" 53 | fill_in "#{address}_name", with: "Ryan Bigg" 54 | fill_in "#{address}_address1", with: "143 Swan Street" 55 | fill_in "#{address}_city", with: "Richmond" 56 | select "United States of America", from: "#{address}_country_id" 57 | fill_in "#{address}_zipcode", with: "12345" 58 | fill_in "#{address}_phone", with: "(555) 555-5555" 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /templates/app/views/checkouts/steps/_address_step.html.erb: -------------------------------------------------------------------------------- 1 | <% order = form.object %> 2 | 3 |
4 |
5 | 6 | <%= t("spree.billing_address") %> 7 | 8 | 9 | <%= form.fields_for :bill_address do |bill_form| %> 10 | <%= render( 11 | "checkouts/steps/address_step/address_inputs", 12 | form: bill_form, 13 | address_type: "billing", 14 | address: order.bill_address, 15 | ) %> 16 | <% end %> 17 |
18 | 19 |
20 | 21 | <%= t('spree.shipping_address') %> 22 | 23 | 24 | <%= form.fields_for :ship_address do |ship_form| %> 25 | <%= label_tag 'order_use_billing', class: 'checkbox-input mb-5' do %> 26 | <%= check_box_tag('order[use_billing]', 1, order.shipping_eq_billing_address?) %> 27 | <%= t('spree.use_billing_address') %> 28 | <% end %> 29 | 30 | <%= render( 31 | "checkouts/steps/address_step/address_inputs", 32 | form: ship_form, 33 | address_type: "shipping", 34 | address: order.ship_address, 35 | additional_classes: '!border-b-0' 36 | ) %> 37 | <% end %> 38 |
39 | 40 |
41 | <% if spree_current_user %> 42 | <%= label_tag 'save-user-address', class: 'checkbox-input' do %> 43 | <%= check_box_tag( 44 | :save_user_address, 45 | 1, 46 | spree_current_user.respond_to?(:persist_order_address), 47 | id: 'save-user-address' 48 | ) %> 49 | <%= I18n.t("spree.save_my_address") %> 50 | <% end %> 51 | <% end %> 52 | 53 | <%= form.button( 54 | I18n.t("spree.save_and_continue"), 55 | class: 'button-primary', 56 | name: :commit 57 | ) %> 58 |
59 |
60 | --------------------------------------------------------------------------------