diff --git a/app/assets/stylesheets/darkswarm/branding.scss b/app/assets/stylesheets/darkswarm/branding.scss index 1ca4224cee..467ff9979a 100644 --- a/app/assets/stylesheets/darkswarm/branding.scss +++ b/app/assets/stylesheets/darkswarm/branding.scss @@ -1,6 +1,7 @@ $ofn-brand: #f27052; -$distributor-header-shadow: 0 1px 0 rgba(0, 0, 0, 0.05), 0 8px 6px -6px rgba(0, 0, 0, 0.2); +$distributor-header-shadow: 0 1px 0 rgba(0, 0, 0, 0.05), + 0 8px 6px -6px rgba(0, 0, 0, 0.2); // e.g. australia, uk, norway specific color @@ -36,8 +37,8 @@ $med-grey: #666; $med-drk-grey: #444; $dark-grey: #333; $light-grey: #ddd; -$light-grey-transparency: rgba(0, 0, 0, .1); -$very-light-grey-transparency: rgba(0, 0, 0, .05); +$light-grey-transparency: rgba(0, 0, 0, 0.1); +$very-light-grey-transparency: rgba(0, 0, 0, 0.05); $black: #000; $white: #fff; @@ -75,3 +76,7 @@ $social-facebook: #3b5998; $social-instagram: #e1306c; $social-linkedin: #0e76a8; $social-twitter: #00acee; + +// Typography +$min-accessible-grey: #666; +$darker-grey: #222; diff --git a/app/assets/stylesheets/darkswarm/split-checkout.scss b/app/assets/stylesheets/darkswarm/split-checkout.scss new file mode 100644 index 0000000000..776cb1ff29 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/split-checkout.scss @@ -0,0 +1,117 @@ +.checkout-tab { + height: 4rem; + display: flex; + flex-direction: column; + justify-content: center; + + span { + font-size: 1.3rem; + @include headingFont; + } + + &:not(.selected) { + background-color: $white; + border-bottom: 5px solid $min-accessible-grey; + + span { + color: $min-accessible-grey; + } + } + + &.selected { + background-color: $ofn-brand; + + span { + color: $white; + text-decoration: underline; + } + } +} + +.checkout-step { + margin-right: auto; + margin-left: auto; + margin-top: 3rem; + + .checkout-substep { + margin-bottom: 1rem; + margin-top: 5rem; + + &:first-child { + margin-top: 3rem; + } + } + + .checkout-title { + font-size: 1.1rem; + @include headingFont; + font-weight: $font-weight-bold; + text-decoration: underline; + color: $darker-grey; + margin-bottom: 1.5rem; + } + + .checkout-input { + margin-bottom: 1.5rem; + + .field_with_errors { + label { + font-weight: $font-weight-bold; + color: $red-700; + } + input { + border-color: $red-700; + } + } + + label { + margin-bottom: 0.3rem; + } + + input, + select { + margin: 0; + } + + span.formError { + background-color: rgba(193, 18, 43, 0.1); + color: $red-700; + font-style: normal; + margin: 0; + font-size: 0.8rem; + display: block; + padding-left: 5px; + padding-right: 5px; + padding-top: 2px; + padding-bottom: 2px; + } + + #distributor_address.panel { + font-size: 0.875rem; + padding: 1rem; + + span { + white-space: pre-wrap; + } + } + } + + .checkout-submit { + margin-top: 5rem; + margin-bottom: 5rem; + + .button { + width: 100%; + margin-bottom: 2rem; + + &.primary { + background-color: $orange-500; + } + + &.cancel { + background-color: $grey-100; + color: $black; + } + } + } +} diff --git a/app/assets/stylesheets/darkswarm/typography.scss b/app/assets/stylesheets/darkswarm/typography.scss index f95de26a89..37d7c955c0 100644 --- a/app/assets/stylesheets/darkswarm/typography.scss +++ b/app/assets/stylesheets/darkswarm/typography.scss @@ -132,7 +132,7 @@ ul.check-list { } .light-grey { - color: #666666; + color: $min-accessible-grey; } .pad { diff --git a/app/constraints/split_checkout_constraint.rb b/app/constraints/split_checkout_constraint.rb new file mode 100644 index 0000000000..e0b0a18eda --- /dev/null +++ b/app/constraints/split_checkout_constraint.rb @@ -0,0 +1,9 @@ +class SplitCheckoutConstraint + def matches?(request) + Flipper.enabled? :split_checkout, current_user(request) + end + + def current_user(request) + @spree_current_user ||= request.env['warden'].user + end +end diff --git a/app/controllers/split_checkout_controller.rb b/app/controllers/split_checkout_controller.rb new file mode 100644 index 0000000000..62e4a4d722 --- /dev/null +++ b/app/controllers/split_checkout_controller.rb @@ -0,0 +1,295 @@ +# frozen_string_literal: true + +require 'open_food_network/address_finder' + +class SplitCheckoutController < ::BaseController + layout 'darkswarm' + + include OrderStockCheck + include Spree::BaseHelper + + helper 'terms_and_conditions' + helper 'checkout' + + # We need pessimistic locking to avoid race conditions. + # Otherwise we fail on duplicate indexes or end up with negative stock. + prepend_around_action CurrentOrderLocker, only: [:edit, :update] + + prepend_before_action :set_checkout_step + prepend_before_action :check_hub_ready_for_checkout + prepend_before_action :check_order_cycle_expiry + prepend_before_action :require_order_cycle + prepend_before_action :require_distributor_chosen + + before_action :load_order, :load_shipping_methods, :load_countries + + before_action :ensure_order_not_completed + before_action :ensure_checkout_allowed + before_action :handle_insufficient_stock + + before_action :associate_user + before_action :check_authorization + before_action :enable_embedded_shopfront + + helper 'spree/orders' + + def edit + return handle_redirect_from_stripe if valid_payment_intent_provided? + + redirect_to_step unless @checkout_step + + # This is only required because of spree_paypal_express. If we implement + # a version of paypal that uses this controller, and more specifically + # the #action_failed method, then we can remove this call + # OrderCheckoutRestart.new(@order).call + rescue Spree::Core::GatewayError => e + rescue_from_spree_gateway_error(e) + end + + def update + if @order.update(order_params) + OrderWorkflow.new(@order).advance_to_payment + + if @order.state == "payment" + redirect_to checkout_step_path(:payment) + else + render :edit + end + else + flash[:error] = "Saving failed, please update the highlighted fields" + + render :edit + end + end + + # Clears the cached order. Required for #current_order to return a new order + # to serve as cart. See https://github.com/spree/spree/blob/1-3-stable/core/lib/spree/core/controller_helpers/order.rb#L14 + # for details. + def expire_current_order + session[:order_id] = nil + @current_order = nil + end + + private + + def set_checkout_step + @checkout_step = params[:step] + end + + def order_params + params.require(:order).permit( + :email, :shipping_method_id, :special_instructions, + bill_address_attributes: PermittedAttributes::Address.attributes, + ship_address_attributes: PermittedAttributes::Address.attributes + ) + end + + def redirect_to_step + if @order.state == "payment" + if true# order.has_no_payment_method_chosen? + redirect_to checkout_step_path(:payment) + else + redirect_to checkout_step_path(:summary) + end + else + redirect_to checkout_step_path(:details) + end + end + + def check_authorization + authorize!(:edit, current_order, session[:access_token]) + end + + def ensure_checkout_allowed + redirect_to main_app.cart_path unless @order.checkout_allowed? + end + + def ensure_order_not_completed + redirect_to main_app.cart_path if @order.completed? + end + + def load_shipping_methods + @shipping_methods = Spree::ShippingMethod.for_distributor(@order.distributor).order(:name) + end + + def load_countries + @countries = available_countries.map { |c| [c.name, c.id] } + @countries_with_states = available_countries.map { |c| [c.id, c.states.map { |s| [s.name, s.id] }] } + end + + def load_order + @order = current_order + + redirect_to(main_app.shop_path) && return if redirect_to_shop? + redirect_to_cart_path && return unless valid_order_line_items? + + before_address + setup_for_current_state + end + + def redirect_to_shop? + !@order || + !@order.checkout_allowed? || + @order.completed? + end + + def valid_order_line_items? + @order.insufficient_stock_lines.empty? && + OrderCycleDistributedVariants.new(@order.order_cycle, @order.distributor). + distributes_order_variants?(@order) + end + + def redirect_to_cart_path + respond_to do |format| + format.html do + redirect_to main_app.cart_path + end + + format.json do + render json: { path: main_app.cart_path }, status: :bad_request + end + end + end + + def setup_for_current_state + method_name = :"before_#{@order.state}" + __send__(method_name) if respond_to?(method_name, true) + end + + def before_address + associate_user + + finder = OpenFoodNetwork::AddressFinder.new(@order.email, @order.customer, spree_current_user) + + @order.bill_address = finder.bill_address + @order.ship_address = finder.ship_address + end + + def before_payment + current_order.payments.destroy_all if request.put? + end + + def valid_payment_intent_provided? + return false unless params["payment_intent"]&.starts_with?("pi_") + + last_payment = OrderPaymentFinder.new(@order).last_payment + @order.state == "payment" && + last_payment&.state == "requires_authorization" && + last_payment&.response_code == params["payment_intent"] + end + + def handle_redirect_from_stripe + return checkout_failed unless @order.process_payments! + + if OrderWorkflow.new(@order).next && order_complete? + checkout_succeeded + redirect_to(order_path(@order)) && return + else + checkout_failed + end + end + + def checkout_workflow(shipping_method_id) + while @order.state != "complete" + if @order.state == "payment" + return if redirect_to_payment_gateway + + return action_failed unless @order.process_payments! + end + + next if OrderWorkflow.new(@order).next({ shipping_method_id: shipping_method_id }) + + return action_failed + end + + update_response + end + + def redirect_to_payment_gateway + return unless params&.dig(:order)&.dig(:payments_attributes)&.first&.dig(:payments_attributes) + + redirect_path = Checkout::PaypalRedirect.new(params).path + redirect_path = Checkout::StripeRedirect.new(params, @order).path if redirect_path.blank? + return if redirect_path.blank? + + render json: { path: redirect_path }, status: :ok + true + end + + def order_error + if @order.errors.present? + @order.errors.full_messages.to_sentence + else + t(:payment_processing_failed) + end + end + + def update_response + if order_complete? + checkout_succeeded + update_succeeded_response + else + action_failed(RuntimeError.new("Order not complete after the checkout workflow")) + end + end + + def order_complete? + @order.state == "complete" || @order.completed? + end + + def checkout_succeeded + Checkout::PostCheckoutActions.new(@order).success(self, params, spree_current_user) + + session[:access_token] = current_order.token + flash[:notice] = t(:order_processed_successfully) + end + + def update_succeeded_response + respond_to do |format| + format.html do + respond_with(@order, location: order_path(@order)) + end + format.json do + render json: { path: order_path(@order) }, status: :ok + end + end + end + + def action_failed(error = RuntimeError.new(order_error)) + checkout_failed(error) + action_failed_response + end + + def checkout_failed(error = RuntimeError.new(order_error)) + Bugsnag.notify(error) + Checkout::PostCheckoutActions.new(@order).failure + end + + def action_failed_response + respond_to do |format| + format.html do + render :edit + end + format.json do + discard_flash_errors + render json: { errors: @order.errors, flash: flash.to_hash }.to_json, status: :bad_request + end + end + end + + def rescue_from_spree_gateway_error(error) + flash[:error] = t(:spree_gateway_error_flash_for_checkout, error: error.message) + action_failed(error) + end + + def permitted_params + PermittedAttributes::Checkout.new(params).call + end + + def discard_flash_errors + # Marks flash errors for deletion after the current action has completed. + # This ensures flash errors generated during XHR requests are not persisted in the + # session for longer than expected. + flash.discard(:error) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 83c19dc94a..c00825f74d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -4,6 +4,17 @@ module ApplicationHelper include RawParams include Pagy::Frontend + def error_message_on(object, method, _options = {}) + object = convert_to_model(object) + obj = object.respond_to?(:errors) ? object : instance_variable_get("@#{object}") + if obj && obj.errors[method].present? + errors = obj.errors[method].map { |err| h(err) }.join('
').html_safe + content_tag(:span, errors, class: 'formError') + else + '' + end + end + def feature?(feature, user = nil) OpenFoodNetwork::FeatureToggle.enabled?(feature, user) end diff --git a/app/models/spree/payment.rb b/app/models/spree/payment.rb index d807d6fac1..915ada2f2e 100644 --- a/app/models/spree/payment.rb +++ b/app/models/spree/payment.rb @@ -145,7 +145,7 @@ module Spree adjustment.originator = payment_method adjustment.label = adjustment_label adjustment.save - else + elsif payment_method.present? payment_method.create_adjustment(adjustment_label, self, true) adjustment.reload end diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index 9d85b3e8df..085542294a 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -33,5 +33,4 @@ .small-12.medium-4.columns = render partial: "checkout/summary" - = render partial: "shared/footer" diff --git a/app/views/split_checkout/_details.html.haml b/app/views/split_checkout/_details.html.haml new file mode 100644 index 0000000000..eee3c61ea7 --- /dev/null +++ b/app/views/split_checkout/_details.html.haml @@ -0,0 +1,110 @@ += f.fields :bill_address, model: @order.bill_address do |bill_address| + %div.checkout-substep + -# YOUR DETAILS + %div.checkout-title + = t("split_checkout.step1.your_details.title") + + %div.checkout-input + = bill_address.label :firstname, t("split_checkout.step1.your_details.first_name.label") + = bill_address.text_field :firstname, { placeholder: t("split_checkout.step1.your_details.first_name.placeholder") } + = f.error_message_on "bill_address.firstname" + + %div.checkout-input + = bill_address.label :lastname, t("split_checkout.step1.your_details.last_name.label") + = bill_address.text_field :lastname, { placeholder: t("split_checkout.step1.your_details.last_name.placeholder") } + = f.error_message_on "bill_address.lastname" + + %div.checkout-input + = f.label :email, t("split_checkout.step1.your_details.email.label") + = f.text_field :email, { placeholder: t("split_checkout.step1.your_details.email.placeholder") } + = f.error_message_on :email + + %div.checkout-input + = bill_address.label :phone, t("split_checkout.step1.your_details.phone.label") + = bill_address.text_field :phone, { placeholder: t("split_checkout.step1.your_details.phone.placeholder") } + = f.error_message_on "bill_address.phone" + + %div.checkout-substep{ "data-controller": "dependant-select", "data-dependant-select-options-value": @countries_with_states } + -# BILLING ADDRESS + %div.checkout-title + = t("split_checkout.step1.billing_address.title") + + %div.checkout-input + = bill_address.label :address1, t("split_checkout.step1.billing_address.address1.label") + = bill_address.text_field :address1, { placeholder: t("split_checkout.step1.billing_address.address1.placeholder") } + = f.error_message_on "bill_address.address1" + + %div.checkout-input + = bill_address.label :address2, t("split_checkout.step1.billing_address.address2.label") + = bill_address.text_field :address2, { placeholder: t("split_checkout.step1.billing_address.address2.placeholder") } + = f.error_message_on "bill_address.address2" + + %div.checkout-input + = bill_address.label :city, t("split_checkout.step1.billing_address.city.label") + = bill_address.text_field :city, { placeholder: t("split_checkout.step1.billing_address.city.placeholder") } + = f.error_message_on "bill_address.city" + + %div.checkout-input + = bill_address.label :state_id, t("split_checkout.step1.billing_address.state_id.label") + = bill_address.select :state_id, @countries_with_states, { }, { "data-dependant-select-target": "select" } + + %div.checkout-input + = bill_address.label :zipcode, t("split_checkout.step1.billing_address.zipcode.label") + = bill_address.text_field :zipcode, { placeholder: t("split_checkout.step1.billing_address.zipcode.placeholder") } + = f.error_message_on "bill_address.zipcode" + + %div.checkout-input + = bill_address.label :country_id, t("split_checkout.step1.billing_address.country_id.label") + = bill_address.select :country_id, @countries, { selected: @order.bill_address.country_id || DefaultCountry.id }, {"data-dependant-select-target": "source", "data-action": "dependant-select#handleSelectChange"} + + - if spree_current_user||true + %div.checkout-input + = f.check_box :checkout_default_bill_address + = f.label :checkout_default_bill_address, t(:checkout_default_bill_address) + +%div.checkout-substep{ "data-controller": "toggle shippingmethod" } + -# DELIVERY ADDRESS + %div.checkout-title + = t("split_checkout.step1.delivery_address.title") + + - selected_shipping_method = @order.shipping_method&.id + - @shipping_methods.each do |shipping_method| + %div.checkout-input + = fields_for shipping_method do |shipping_method_form| + = shipping_method_form.radio_button :name, shipping_method.id, + id: "shipping_method_#{shipping_method.id}", + checked: (shipping_method.id == selected_shipping_method), + "data-description": shipping_method.description, + "data-action": "toggle#toggle shippingmethod#selectShippingMethod", + "data-toggle-show": shipping_method.require_ship_address + = shipping_method_form.label shipping_method.id, shipping_method.name, {for: "shipping_method_" + shipping_method.id.to_s } + %em.light + = payment_method_price(shipping_method, @order) + + + %div.checkout-input{ "data-toggle-target": "content", style: "display: none" } + = f.check_box "Checkout.ship_address_same_as_billing", { id: "Checkout.ship_address_same_as_billing" } + = f.label "Checkout.ship_address_same_as_billing", t(:checkout_address_same), { for: "Checkout.ship_address_same_as_billing" } + + - if spree_current_user + %div.checkout-input{ "data-toggle-target": "content", style: "display: none" } + = f.check_box "Checkout.default_ship_address", { id: "Checkout.default_ship_address" } + = f.label "Checkout.default_ship_address", t(:checkout_default_ship_address), { for: "Checkout.default_ship_address" } + + %div.checkout-input{"data-shippingmethod-target": "shippingMethodDescription"} + #distributor_address.panel + %span{"data-shippingmethod-target": "shippingMethodDescriptionContent"} + %br/ + %br/ + - if @order.order_cycle.pickup_time_for(@order.distributor) + = t :checkout_ready_for + = @order.order_cycle.pickup_time_for(@order.distributor) + + .div.checkout-input + = f.label :special_instructions, t(:checkout_instructions) + = f.text_area :special_instructions, size: "60x4" + +%div.checkout-submit + = f.submit t("split_checkout.step1.submit"), class: "button primary", disabled: @terms_and_conditions_accepted == false || @platform_tos_accepted == false + %a.button.cancel{href: main_app.cart_path} + = t("split_checkout.step1.cancel") diff --git a/app/views/split_checkout/_form.html.haml b/app/views/split_checkout/_form.html.haml new file mode 100644 index 0000000000..84a1d12edd --- /dev/null +++ b/app/views/split_checkout/_form.html.haml @@ -0,0 +1,7 @@ +- content_for :injection_data do + = inject_available_payment_methods + = inject_saved_credit_cards + +%div.checkout-step.medium-6 + = form_with url: checkout_update_path(@checkout_step), model: @order, method: :put do |f| + = render "split_checkout/#{@checkout_step}", f: f diff --git a/app/views/split_checkout/_payment.html.haml b/app/views/split_checkout/_payment.html.haml new file mode 100644 index 0000000000..73e515a961 --- /dev/null +++ b/app/views/split_checkout/_payment.html.haml @@ -0,0 +1,22 @@ +%div.checkout-substep{"data": {"controller": "paymentmethod"}} + %div.checkout-title + = t("split_checkout.step2.payment_method.title") + - available_payment_methods.each do |payment_method| + %div.checkout-input + = f.radio_button :payment_method_id, payment_method.id, + id: "payment_method_#{payment_method.id}", + "data-action": "paymentmethod#selectPaymentMethod", + "data-paymentmethod-description": "#{payment_method.description}" + = f.label payment_method.id, "#{payment_method.name} (#{payment_method_price(payment_method, @order)})", {for: "payment_method_" + payment_method.id.to_s } + + %div + .panel{"data-paymentmethod-target": "panel", style: "display: none"} + + +%div.checkout-substep + = t("split_checkout.step2.explaination") + +%div.checkout-submit + = f.submit t("split_checkout.step2.submit"), class: "button primary", disabled: @terms_and_conditions_accepted == false || @platform_tos_accepted == false + %a.button.cancel{href: main_app.checkout_step_path(:details)} + = t("split_checkout.step2.cancel") diff --git a/app/views/split_checkout/_tabs.html.haml b/app/views/split_checkout/_tabs.html.haml new file mode 100644 index 0000000000..3b29750a94 --- /dev/null +++ b/app/views/split_checkout/_tabs.html.haml @@ -0,0 +1,10 @@ +%div.flex + %div.columns.three.text-center.checkout-tab{"class": ("selected" if @checkout_step == "details")} + %span + = t("split_checkout.your_details") + %div.columns.three.text-center.checkout-tab{"class": ("selected" if @checkout_step == "payment")} + %span + = t("split_checkout.payment_method") + %div.columns.three.text-center.checkout-tab{"class": ("selected" if @checkout_step == "summary")} + %span + = t("split_checkout.order_summary") diff --git a/app/views/split_checkout/edit.html.haml b/app/views/split_checkout/edit.html.haml new file mode 100644 index 0000000000..d56295b53e --- /dev/null +++ b/app/views/split_checkout/edit.html.haml @@ -0,0 +1,32 @@ +- content_for(:title) do + = t :checkout_title + +- content_for :injection_data do + = inject_enterprise_and_relatives + = inject_available_countries + +.darkswarm.footer-pad + - content_for :order_cycle_form do + %closing + = t :checkout_now + %p + = t :checkout_order_ready + %strong + = pickup_time current_order_cycle + + - content_for :ordercycle_sidebar do + .show-for-large-up.large-4.columns + = render partial: "shopping_shared/order_cycles" + + = render partial: "shopping_shared/header" + + .sub-header.show-for-medium-down + = render partial: "shopping_shared/order_cycles" + + %checkout.row + .small-12.medium-12.columns + = render partial: "split_checkout/tabs" + = render partial: "split_checkout/form" + + += render partial: "shared/footer" diff --git a/app/webpacker/controllers/dependant_select_controller.js b/app/webpacker/controllers/dependant_select_controller.js new file mode 100644 index 0000000000..b58e7c7417 --- /dev/null +++ b/app/webpacker/controllers/dependant_select_controller.js @@ -0,0 +1,27 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + static targets = ["source", "select"]; + static values = { options: Array }; + + connect() { + this.populateSelect(parseInt(this.sourceTarget.value)); + } + + handleSelectChange() { + this.populateSelect(parseInt(this.sourceTarget.value)); + } + + populateSelect(sourceId) { + const allOptions = this.optionsValue; + const options = allOptions.find((option) => option[0] === sourceId)[1]; + const selectBox = this.selectTarget; + selectBox.innerHTML = ""; + options.forEach((item) => { + const opt = document.createElement("option"); + opt.value = item[1]; + opt.innerHTML = item[0]; + selectBox.appendChild(opt); + }); + } +} diff --git a/app/webpacker/controllers/paymentmethod_controller.js b/app/webpacker/controllers/paymentmethod_controller.js new file mode 100644 index 0000000000..ac1c9dff60 --- /dev/null +++ b/app/webpacker/controllers/paymentmethod_controller.js @@ -0,0 +1,9 @@ +import { Controller } from "stimulus"; +export default class extends Controller { + static targets = ["panel"]; + + selectPaymentMethod(event) { + this.panelTarget.innerHTML = `${event.target.dataset.paymentmethodDescription}`; + this.panelTarget.style.display = "block"; + } +} diff --git a/app/webpacker/controllers/shippingmethod_controller.js b/app/webpacker/controllers/shippingmethod_controller.js new file mode 100644 index 0000000000..4a008581b3 --- /dev/null +++ b/app/webpacker/controllers/shippingmethod_controller.js @@ -0,0 +1,24 @@ +import { Controller } from "stimulus"; +export default class extends Controller { + static targets = [ + "shippingMethodDescription", + "shippingMethodDescriptionContent", + ]; + connect() { + // Hide shippingMethodDescription by default + this.shippingMethodDescriptionTarget.style.display = "none"; + } + selectShippingMethod(event) { + const input = event.target; + if (input.tagName === "INPUT") { + if (input.dataset.description.length > 0) { + this.shippingMethodDescriptionTarget.style.display = "block"; + this.shippingMethodDescriptionContentTarget.innerText = + input.dataset.description; + } else { + this.shippingMethodDescriptionTarget.style.display = "none"; + this.shippingMethodDescriptionContentTarget.innerText = null; + } + } + } +} diff --git a/app/webpacker/controllers/toggle_controller.js b/app/webpacker/controllers/toggle_controller.js new file mode 100644 index 0000000000..de87ad095d --- /dev/null +++ b/app/webpacker/controllers/toggle_controller.js @@ -0,0 +1,12 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + static targets = ["content"]; + + toggle(event) { + const input = event.currentTarget; + this.contentTargets.forEach((t) => { + t.style.display = input.dataset.toggleShow === "true" ? "block" : "none"; + }); + } +} diff --git a/config/locales/en.yml b/config/locales/en.yml index 0222570709..fa393e57b6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1620,6 +1620,57 @@ en: checkout_back_to_cart: "Back to Cart" cost_currency: "Cost Currency" + split_checkout: + your_details: 1 - Your details + payment_method: 2 - Payment method + order_summary: 3 - Order summary + step1: + your_details: + title: Your details + first_name: + label: First Name + placeholder: e.g. Jane + last_name: + label: Last Name + placeholder: e.g. Doe + email: + label: Email + placeholder: e.g. Janedoe@email.com + phone: + label: Phone number + placeholder: e.g. 07987654321 + billing_address: + title: Billing address + address1: + label: Address (Street + House Number) + placeholder: e.g. Flat 1 Elm apartments + address2: + label: Additional address info (optional) + placeholder: e.g. Cavalier avenur + city: + label: City + placeholder: e.g. London + state_id: + label: State + zipcode: + label: Postcode + placeholder: e.g. SW11 3QN + country_id: + label: Country + delivery_address: + title: Delivery address + submit: Next - Payment method + cancel: Back to Edit basket + step2: + payment_method: + title: Payment method + explaination: You can review and confirm your order in the next step which includes the final costs. + submit: Next - Order summary + cancel: Back to Your details + errors: + required: Field cannot be blank + invalid_number: "Please enter a valid phone number" + invalid_email: "Please enter a valid email address" order_paid: PAID order_not_paid: NOT PAID order_total: Total order diff --git a/config/routes.rb b/config/routes.rb index 3385d97a02..69d1561185 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -69,8 +69,17 @@ Openfoodnetwork::Application.routes.draw do resources :webhooks, only: [:create] end - get '/checkout', to: 'checkout#edit' , as: :checkout - put '/checkout', to: 'checkout#update' , as: :update_checkout + constraints SplitCheckoutConstraint.new do + get '/checkout', to: 'split_checkout#edit' + + constraints step: /(details|payment|summary)/ do + get '/checkout/:step', to: 'split_checkout#edit', as: :checkout_step + put '/checkout/:step', to: 'split_checkout#update', as: :checkout_update + end + end + + get '/checkout', to: 'checkout#edit' + put '/checkout', to: 'checkout#update', as: :update_checkout get '/checkout/:state', to: 'checkout#edit', as: :checkout_state get '/checkout/paypal_payment/:order_id', to: 'checkout#paypal_payment', as: :paypal_payment