This is a refactored version of my original post here that uses the spree_frontend. To be able to follow along, please take 2-3 minutes to have a read. v1, as I would call it, uses an outdated version of Spree Commerce. v2 (this post) now uses Spree v4.5.x which uses the spree_backend
gem.
I've updated StockItemsControllerDecorator
# https://github.com/spree/spree_backend/blob/main/app/controllers/spree/admin/stock_items_controller.rb
module Spree
module Admin
module StockItemsControllerDecorator
# renamed from NotifyCustomersHelper
include NotifyCustomers
def self.prepended(base)
base.before_action :process_notifiees_on_stock_item, only: :update
# We have not taken into account should stock_movement.save fails.
# see https://github.com/spree/spree_backend/blob/main/app/controllers/spree/admin/stock_items_controller.rb#L13
base.before_action :process_notifiees_on_stock_movement, only: :create
# We don't need to track when deleted as the "track Inventory" can be checked when deleted.
# For this, look at Spree::Admin::VariantsIncludingMasterController#update
# base.before_action :notify_notifiees, only: :destroy
end
end
end
end
::Spree::Admin::StockItemsController.prepend Spree::Admin::StockItemsControllerDecorator if ::Spree::Admin::StockItemsController.included_modules.exclude?(Spree::Admin::StockItemsControllerDecorator)
VariantsIncludingMasterControllerDecorator was also refactored as we will only send an email when no longer tracking inventory:
module Spree
module Admin
module VariantsIncludingMasterControllerDecorator
include NotifyCustomers
def self.prepended(base)
# Defined in NotifyCustomers. Continue reading...
base.before_action :notify_notifiees, only: :update
end
end
end
end
::Spree::Admin::VariantsIncludingMasterController.prepend Spree::Admin::VariantsIncludingMasterControllerDecorator if ::Spree::Admin::VariantsIncludingMasterController.included_modules.exclude?(Spree::Admin::VariantsIncludingMasterControllerDecorator)
I've also refactored NotifyCustomers
and instead look for the variant instead of the product:
module NotifyCustomers
private
# We've made the executive decision by not keeping stocks.
# Alert all customers that the product is available to purchase.
def notify_notifiees
variant_id = params[:id]
not_tracking = !ActiveRecord::Type::Boolean.new.cast(
params[:variant][:track_inventory]
)
not_tracking && email_all_notifiees(variant_id)
end
def process_notifiees_on_stock_movement
quantity = params[:stock_movement][:quantity].to_i
variant_id = params[:variant_id]
unless quantity.zero?
email_all_notifiees(variant_id)
end
end
def email_all_notifiees(variant_id)
notifiees = lookup_notifiees_by(variant_id)
send_notification_email(notifiees)
# We said we'd delete their email address
notifiees.destroy_all
end
def process_notifiees_on_stock_item
# Backorderable: boolean
# stock_item.backorderable
# Number of items in stock: integer
# stock_item.count_on_hand
unless stock_item.count_on_hand.zero?
variant_id = stock_item.variant.id
email_all_notifiees(variant_id)
end
end
def lookup_notifiees_by(variant_id)
Spree::VariantNotification.where(variant_id: variant_id)
end
def send_notification_email(notifiees)
if notifiees.present?
emails_to_send = notifiees.pluck(:email)
puts "---- SENDING EMAIL TO #{emails_to_send.length} email addresses (plural for now)"
# Now send the email
# You'll need to implement a mailer for this
end
end
end
Migration file was also updated (I'm doing this in a new project):
class CreateSpreeVariantNotifications < ActiveRecord::Migration[7.0]
def change
create_table :spree_variant_notifications do |t|
t.references :user
t.references :variant, null: false
t.string :email, null: false
t.timestamps
end
end
end
Updated models:
module Spree
class VariantNotification < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
# Optional as a user doesnt have to be signed in
belongs_to :user, class_name: 'Spree::User', optional: true
belongs_to :variant, class_name: 'Spree::Variant'
end
end
module Spree
module VariantDecorator
def self.prepended(base)
base.has_many :notifications, class_name: 'Spree::VariantNotification', foreign_key: 'variant_id', dependent: :destroy
end
end
end
::Spree::Variant.prepend Spree::VariantDecorator if ::Spree::Variant.included_modules.exclude?(Spree::VariantDecorator)
Also the place where the email gets processed into our database, the controller:
module Spree
# Though belongs to a variant, we'll keep this for the product
module Products
class NotifyController < Spree::StoreController
before_action :set_variant
include ActionView::Helpers::TextHelper
# Does not uses a layout
layout false
def notify_me
email = strip_tags(notify_params[:email])
@notif = @variant.notifications.find_or_create_by(email: email) do |perm|
# user_id can be null
perm.user_id = spree_current_user&.id || notify_params[:user_id]
end
respond_to do |format|
if @notif.save
format.turbo_stream
else
format.html { redirect_to spree.product_path(@variant.product), alert: "Something went wrong", status: 422 }
end
end
end
private
def notify_params
params.fetch(:product_notification, {}).permit(:email)
end
def set_variant
@variant = Spree::Variant.find_by(id: params[:variant_id])
unless @variant.present?
redirect_to root_path
end
end
end
end
end
# Route updated to:
post '/products/:variant_id/notify', to: 'products/notify#notify_me', as: 'variant_notify'
_notify_me_when_available.html
, inside _cart_form
but placed outside the order form was also updated:
<%= form_with(
scope: :product_notification,
url: spree.variant_notify_path(variant),
data: {
controller: "reset-form",
action: "turbo:submit-end->reset-form#reset"
},
id: dom_id(variant, :notify)) do |form| %>
<%= form.label :email, "Notify me when in stock" %>
<%= form.email_field :email, class: 'spree-flat-input', placeholder: "Enter your email address", required: true %>
<%= form.submit "Notify me", class: "btn btn-primary w-100 text-uppercase font-weight-bold mt-2" %>
<p class="mt-2 text-sm">After this notification, your email address will be deleted and not used again.</p>
<% end %>
I'm using turbo in this new project of mine. When form gets submitted, I clear the input field and when successful, I update the DOM with a partial (spree/shared/_variant_notify_success
):
<div class="p-3 text-center bg-green-600 text-white sm:text-base text-sm">
<p>Great! We'll send you a one-time email when item becomes available.</p>
</div>
Using..
<%= turbo_stream.replace dom_id(@variant, :notify) do %>
<%= render "spree/shared/variant_notify_success" %>
<% end %>
...outside the order form:
<%# Outside order form %>
<% unless is_product_available_in_currency && @product.can_supply? %>
<%= render 'spree/shared/notify_me_when_available', variant: default_variant %>
<% end %>
Additional work is needed to show/hide the notification form should a variant is out of stock. My example will only display the notification form when product cannot supply.
Spree frontend has an event lister for .product-variants-variant-values-radio
as seen here. You could put your logic there to show/hide the notification form. Here's the logic:
Inside your click event for OPTION_VALUE_SELECTOR
(in your own js
file or use Stimulus):
With the selected variant_id
, parse the data for data-variants
on the cart from. Inside the array of data-variants
, check that the variant is purchasable
, using variants.find(o.id == variant_id).purchasable
, for example. Based on true or false, show/hide the notification form. I got purchasable
from here.
That should be all for now. At the time of this post, I have not implement show/hide when an option is selected. Live demo here.
Top comments (0)