mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-01 21:47:16 +00:00
Merge pull request #7840 from jibees/new-checkout-first-step
New checkout first + second step
This commit is contained in:
@@ -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;
|
||||
|
||||
117
app/assets/stylesheets/darkswarm/split-checkout.scss
Normal file
117
app/assets/stylesheets/darkswarm/split-checkout.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ ul.check-list {
|
||||
}
|
||||
|
||||
.light-grey {
|
||||
color: #666666;
|
||||
color: $min-accessible-grey;
|
||||
}
|
||||
|
||||
.pad {
|
||||
|
||||
9
app/constraints/split_checkout_constraint.rb
Normal file
9
app/constraints/split_checkout_constraint.rb
Normal file
@@ -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
|
||||
295
app/controllers/split_checkout_controller.rb
Normal file
295
app/controllers/split_checkout_controller.rb
Normal file
@@ -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
|
||||
@@ -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('<br />').html_safe
|
||||
content_tag(:span, errors, class: 'formError')
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def feature?(feature, user = nil)
|
||||
OpenFoodNetwork::FeatureToggle.enabled?(feature, user)
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,5 +33,4 @@
|
||||
.small-12.medium-4.columns
|
||||
= render partial: "checkout/summary"
|
||||
|
||||
|
||||
= render partial: "shared/footer"
|
||||
|
||||
110
app/views/split_checkout/_details.html.haml
Normal file
110
app/views/split_checkout/_details.html.haml
Normal file
@@ -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")
|
||||
7
app/views/split_checkout/_form.html.haml
Normal file
7
app/views/split_checkout/_form.html.haml
Normal file
@@ -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
|
||||
22
app/views/split_checkout/_payment.html.haml
Normal file
22
app/views/split_checkout/_payment.html.haml
Normal file
@@ -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")
|
||||
10
app/views/split_checkout/_tabs.html.haml
Normal file
10
app/views/split_checkout/_tabs.html.haml
Normal file
@@ -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")
|
||||
32
app/views/split_checkout/edit.html.haml
Normal file
32
app/views/split_checkout/edit.html.haml
Normal file
@@ -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"
|
||||
27
app/webpacker/controllers/dependant_select_controller.js
Normal file
27
app/webpacker/controllers/dependant_select_controller.js
Normal file
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
9
app/webpacker/controllers/paymentmethod_controller.js
Normal file
9
app/webpacker/controllers/paymentmethod_controller.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Controller } from "stimulus";
|
||||
export default class extends Controller {
|
||||
static targets = ["panel"];
|
||||
|
||||
selectPaymentMethod(event) {
|
||||
this.panelTarget.innerHTML = `<span>${event.target.dataset.paymentmethodDescription}</span>`;
|
||||
this.panelTarget.style.display = "block";
|
||||
}
|
||||
}
|
||||
24
app/webpacker/controllers/shippingmethod_controller.js
Normal file
24
app/webpacker/controllers/shippingmethod_controller.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
app/webpacker/controllers/toggle_controller.js
Normal file
12
app/webpacker/controllers/toggle_controller.js
Normal file
@@ -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";
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user