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