add persistent email session for order lookup and reviews
All checks were successful
deploy / deploy (push) Successful in 1m13s

Replaces the short-lived (1 hour) session-based order lookup with a
persistent cookie-based email session lasting 30 days. This foundation
enables customers to leave reviews and view orders without re-verifying
their email each time.

- Add EmailSession module for signed cookie management
- Add EmailSession plug to load verified email into session
- Set email session on order lookup verification
- Set email session on checkout completion (via /checkout/complete)
- Update orders and order detail pages to use email session
- Add reviews system plan document

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-04-01 09:44:53 +01:00
parent 3b23a413ed
commit 34822254e3
13 changed files with 811 additions and 5 deletions

View File

@@ -0,0 +1,67 @@
defmodule Berrypod.EmailSession do
@moduledoc """
Manages persistent email sessions for verified customers.
Used for order lookup, review submission, and review editing without
requiring re-verification each time. The session is stored as a signed
cookie that lasts 30 days.
Unlike the short-lived order lookup tokens (1 hour), this provides a
"remember me" style experience for returning customers.
"""
import Plug.Conn
@cookie_name "email_session"
@max_age 30 * 24 * 60 * 60
@salt "email_session_v1"
@doc """
Sets the email session cookie for a verified email address.
Call this after successful email verification (order lookup, review
submission, checkout completion) to enable the customer to access
their orders and reviews without re-verifying.
"""
def put_session(conn, email) when is_binary(email) do
email = String.downcase(String.trim(email))
token = Phoenix.Token.sign(BerrypodWeb.Endpoint, @salt, email)
put_resp_cookie(conn, @cookie_name, token,
max_age: @max_age,
http_only: true,
secure: Application.get_env(:berrypod, :env) == :prod,
same_site: "Lax"
)
end
@doc """
Retrieves the verified email address from the session cookie.
Returns `{:ok, email}` if the cookie is valid and not expired,
or `:error` otherwise.
"""
def get_email(conn) do
with token when is_binary(token) <- conn.cookies[@cookie_name],
{:ok, email} <-
Phoenix.Token.verify(BerrypodWeb.Endpoint, @salt, token, max_age: @max_age) do
{:ok, email}
else
_ -> :error
end
end
@doc """
Clears the email session cookie.
Use when the customer explicitly logs out or requests to be forgotten.
"""
def clear_session(conn) do
delete_resp_cookie(conn, @cookie_name)
end
@doc """
Returns the cookie name for testing purposes.
"""
def cookie_name, do: @cookie_name
end

View File

@@ -71,7 +71,7 @@ defmodule BerrypodWeb.CheckoutController do
%{
mode: "payment",
line_items: line_items,
success_url: R.url(R.checkout_success()) <> "?session_id={CHECKOUT_SESSION_ID}",
success_url: R.url("/checkout/complete") <> "?session_id={CHECKOUT_SESSION_ID}",
cancel_url: R.url(R.cart()),
metadata: %{"order_id" => order.id},
shipping_address_collection: %{

View File

@@ -0,0 +1,32 @@
defmodule BerrypodWeb.CheckoutSuccessController do
@moduledoc """
Handles the redirect back from Stripe checkout.
This controller intercepts the Stripe redirect to set the email session
cookie before forwarding to the checkout success LiveView. This allows
customers to later view their orders and leave reviews without needing
to re-verify their email.
"""
use BerrypodWeb, :controller
alias Berrypod.{EmailSession, Orders}
def show(conn, %{"session_id" => session_id}) do
# Look up the order to get the customer email
order = Orders.get_order_by_stripe_session(session_id)
conn =
if order && order.customer_email do
EmailSession.put_session(conn, order.customer_email)
else
conn
end
redirect(conn, to: R.checkout_success() <> "?session_id=#{session_id}")
end
def show(conn, _params) do
redirect(conn, to: R.home())
end
end

View File

@@ -1,6 +1,7 @@
defmodule BerrypodWeb.OrderLookupController do
use BerrypodWeb, :controller
alias Berrypod.EmailSession
alias Berrypod.Orders
alias Berrypod.Orders.OrderNotifier
@@ -44,7 +45,7 @@ defmodule BerrypodWeb.OrderLookupController do
case Phoenix.Token.verify(BerrypodWeb.Endpoint, @salt, token, max_age: @max_age) do
{:ok, email} ->
conn
|> put_session(:order_lookup_email, email)
|> EmailSession.put_session(email)
|> redirect(to: R.orders())
{:error, :expired} ->

View File

@@ -16,7 +16,7 @@ defmodule BerrypodWeb.Shop.Pages.OrderDetail do
socket =
socket
|> assign(:lookup_email, session["order_lookup_email"])
|> assign(:lookup_email, session["email_session"])
|> assign(:page, page)
{:noreply, socket}

View File

@@ -1,6 +1,9 @@
defmodule BerrypodWeb.Shop.Pages.Orders do
@moduledoc """
Orders list page handler for the unified Shop.Page LiveView.
Uses the email session cookie (30 days) set during order lookup
verification or checkout completion.
"""
import Phoenix.Component, only: [assign: 3]
@@ -8,7 +11,7 @@ defmodule BerrypodWeb.Shop.Pages.Orders do
alias Berrypod.{Orders, Pages}
def init(socket, _params, _uri, session) do
email = session["order_lookup_email"]
email = session["email_session"]
page = Pages.get_page("orders")
socket =

View File

@@ -0,0 +1,26 @@
defmodule BerrypodWeb.Plugs.EmailSession do
@moduledoc """
Plug that loads the verified email from the email session cookie into assigns.
This makes `@email_session` available in controllers and LiveViews,
containing the verified email address if the customer has one.
"""
import Plug.Conn
alias Berrypod.EmailSession
def init(opts), do: opts
def call(conn, _opts) do
case EmailSession.get_email(conn) do
{:ok, email} ->
conn
|> assign(:email_session, email)
|> put_session("email_session", email)
:error ->
assign(conn, :email_session, nil)
end
end
end

View File

@@ -14,6 +14,7 @@ defmodule BerrypodWeb.Router do
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_scope_for_user
plug BerrypodWeb.Plugs.EmailSession
plug BerrypodWeb.Plugs.CountryDetect
plug BerrypodWeb.Plugs.LoadTheme
end
@@ -219,10 +220,12 @@ defmodule BerrypodWeb.Router do
end
# Order lookup verification — sets session email then redirects to /orders
# Checkout complete — sets email session cookie then redirects to success page
scope "/", BerrypodWeb do
pipe_through [:browser]
get "/orders/verify/:token", OrderLookupController, :verify
get "/checkout/complete", CheckoutSuccessController, :show
get "/unsubscribe/:token", UnsubscribeController, :unsubscribe
get "/newsletter/confirm/:token", NewsletterController, :confirm
end