├── .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 |
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 |
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 |
2 | <% variants.each do |variant, quantity| %>
3 |
4 | <%= render(ImageComponent.new(
5 | image: variant.gallery.images.first || variant.product.gallery.images.first,
6 | size: :mini
7 | )) %>
8 |
9 | <%= variant.name %>
10 | <%= quantity %>
11 | <%= display_price(variant) %>
12 | <% end %>
13 |
14 |
--------------------------------------------------------------------------------
/templates/app/views/products/_products_grid.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% products.each do |product| %>
3 | <% cache [I18n.locale, current_pricing_options, taxon, product] do %>
4 | <%= render ProductCardComponent.new(
5 | product,
6 | price: product.price_for_options(current_pricing_options),
7 | additional_classes: "col-span-full md:col-span-6 lg:col-span-4"
8 | ) %>
9 | <% end %>
10 | <% end %>
11 |
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 |
8 | <% product.taxons.each do |taxon| %>
9 |
10 | <%= link_to taxon.name, taxon_seo_url(taxon) %>
11 |
12 | <% end %>
13 |
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 |
2 | <% shipping_rates.each do |rate| %>
3 |
4 | <%= form.label :selected_shipping_rate_id, value: rate.id, class: "radio-input flex gap-x-3 items-center last:mb-0" do %>
5 | <%= form.radio_button(:selected_shipping_rate_id, rate.id) %>
6 | <%= "#{rate.name} #{rate.display_cost}" %>
7 | <% end %>
8 |
9 | <% end %>
10 |
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 |
7 | <% target.errors.full_messages.each do |msg| %>
8 | <%= msg %>
9 | <% end %>
10 |
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 |
3 | <%= render 'autocomplete_results', results: @results %>
4 |
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 | <%= product_property.property.presentation %>
13 | <%= product_property.value %>
14 |
15 | <% end %>
16 |
17 | <% reset_cycle('properties') %>
18 |
19 |
20 |
21 |
22 | <% end %>
23 |
--------------------------------------------------------------------------------
/templates/app/views/users/_users_menu.erb:
--------------------------------------------------------------------------------
1 |
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 |
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 |
4 | <% cache [I18n.locale, taxonomies, taxon, max_level] do %>
5 | <% taxonomies.each do |taxonomy| %>
6 | <% cache [I18n.locale, taxonomy, taxon, max_level] do %>
7 | <%= render(TaxonsTreeComponent.new(
8 | title: t('spree.categories'),
9 | root_taxon: taxonomy.root,
10 | current_taxon: taxon,
11 | max_level: max_level,
12 | item_classes: 'mb-2.5 hover:underline underline-offset-8 w-fit flex',
13 | current_item_classes: 'underline',
14 | title_classes: 'font-sans-md mb-3'
15 | )) %>
16 | <% end %>
17 | <% end %>
18 | <% end %>
19 |
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? ? '' : "#{current_order.item_count}
"
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 |
3 | <%= render 'shared/navigation/taxonomies', taxon: @taxon %>
4 | <%= render 'shared/search/filters', taxon: @taxon %>
5 |
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 |
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 |
3 | <% if "products" == params[:controller] && @taxon %>
4 | <%= render 'shared/search/filters', taxon: @taxon %>
5 | <% else %>
6 | <%= render 'shared/navigation/taxonomies', taxon: @taxon %>
7 | <%= render 'shared/search/filters', taxon: @taxon %>
8 | <% end %>
9 |
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 |
16 |
17 | <%= render partial_name_with_fallback(
18 | 'orders/payment_info',
19 | payment.payment_method.partial_name,
20 | 'default'
21 | ), payment: payment %>
22 |
23 |
24 | <%= payment.display_amount %>
25 | (<%= t(payment.state, scope: 'spree.payment_states') %>)
26 |
27 |
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 |
15 | <% order.shipments.each do |shipment| %>
16 |
17 | <%= t(
18 | 'spree.shipment_details',
19 | stock_location: shipment.stock_location.name,
20 | shipping_method: shipment.selected_shipping_rate.name
21 | ) %>
22 | <% if order.shipped? && link_to_tracking(shipment) %>
23 |
24 |
<%= t('spree.tracking') %>
25 |
<%= link_to_tracking(shipment) %>
26 |
27 | <% end %>
28 |
29 | <% end %>
30 |
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 |
5 | <%= check_box_tag nil, nil, nil, class: 'sr-only peer', data: {
6 | theme_switcher_target: "themeSwitch", action: "theme-switcher#toggleTheme"
7 | } %>
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
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 |
15 | <%= address.name %>
16 | <% unless address.company.blank? %>
17 | <%= address.company %>
18 | <% end %>
19 | <%= address.address1 %>
20 | <% unless address.address2.blank? %>
21 | <%= address.address2 %>
22 | <% end %>
23 |
24 | <%= address.city %>
25 | <%= address.state_text %>
26 | <%= address.zipcode %>
27 |
28 | <%= address.country.try(:name) %>
29 | <% unless address.phone.blank? %>
30 |
31 | <%= t('spree.phone') %>
32 | <%= address.phone %>
33 |
34 | <% end %>
35 | <% unless address.alternative_phone.blank? %>
36 |
37 | <%= t('spree.alternative_phone') %>
38 | <%= address.alternative_phone %>
39 |
40 | <% end %>
41 |
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 |
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 |
7 |
8 | <%= t('spree.variants') %>
9 |
10 |
11 |
12 | <% variants.each_with_index do |variant, index| %>
13 |
14 | <%= radio_button_tag(
15 | 'variant_id',
16 | variant.id,
17 | index == 0,
18 | "data-js" => 'variant-radio',
19 | "data-js_price" => variant.price_for_options(current_pricing_options)&.money.to_s,
20 | "data-option-value-ids" => sorted_option_values(variant).to_json,
21 | "data-target" => "variantOptions",
22 | "data-price" => variant.price_for_options(current_pricing_options)&.money&.to_html
23 | ) %>
24 |
25 | <%= label_tag "variant_id_#{ variant.id }" do %>
26 | <%= variant_options variant %>
27 |
28 | <% if variant_price variant %>
29 | <%= variant_price variant %>
30 | <% end %>
31 |
32 | <% unless variant.can_supply? %>
33 | <%= t('spree.out_of_stock') %>
34 | <% end %>
35 | <% end %>
36 |
37 | <% end %>
38 |
39 |
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 |
5 | <%= render "shared/navigation/theme_switcher" %>
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | <%= link_to t('spree.cart'), cart_path %>
14 |
15 |
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 |
26 | <%= link_to "https://solidus.io", class:"flex flex-row items-center gap-1" do %>
27 | solidus.io
28 |
29 |
30 |
31 | <% end %>
32 |
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 |
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 |
2 | <% if results[:products].any? %>
3 | <% results[:products].each do |product| %>
4 |
6 | <%= link_to product.name, product_path(product), class: 'w-fit hover:text-primary' %>
7 |
8 | <% end %>
9 |
10 | <%= link_to products_path(keywords: params[:keywords]), class: "flex items-center gap-x-1 transition-all duration-150 hover:gap-x-2" do %>
11 | See all results
12 |
13 |
14 |
15 | <% end %>
16 |
17 | <% else %>
18 | No results
19 |
20 | <%= link_to products_path, class: "flex items-center gap-x-1 transition-all duration-150 hover:gap-x-2" do %>
21 | See all results
22 |
23 |
24 |
25 | <% end %>
26 |
27 | <% end %>
28 |
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 |
19 | <%= tag(
20 | :input,
21 | class: "peer absolute opacity-0 h-0 w-0",
22 | type: "radio",
23 | value: option_value.id,
24 | checked: i.zero?,
25 | name: "#{option_type.name}",
26 | "data-action": "clickOnSelection",
27 | "data-presentation": option_value.presentation,
28 | "data-option-index": index
29 | ) %>
30 | '>
31 | <%= option_value.presentation %>
32 |
33 |
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 |
34 |
<%= line_item.display_amount.to_html unless line_item.quantity.nil? %>
35 |
36 |
37 |
38 | <%= render 'carts/cart_item_remove', order_form: order_form, item_form: item_form, line_item: line_item %>
39 |
40 |
41 |
42 |
43 | <%= render 'carts/cart_item_remove', order_form: order_form, item_form: item_form, line_item: line_item %>
44 |
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 |
--------------------------------------------------------------------------------