feat(auth): display confirmation banner and require email verification

Adds a confirmation banner on the login page after a user successfully registers a new account. This provides clear user feedback that their registration was successful and that an email has been sent for account verification.

Also prevents users from logging in without a confirmed email address by updating the registration and new account creation flows to set the 'confirmed' status to false by default. A confirmation token is generated and a confirmation link is logged to the console (for now) to enable email verification.
This commit is contained in:
2026-01-18 16:28:33 -05:00
parent 5b1909f943
commit 159354c8f4
4 changed files with 44 additions and 24 deletions

View File

@@ -25,7 +25,7 @@ const Order = () => {
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState(0);
const [loading, setLoading] = useState(false);
const [pricing, setPricing] = useState({ price_per_gallon: 0, date: null });
const [pricing, setPricing] = useState({ price_per_gallon: 0, price_same_day: 0, price_prime: 0, price_emergency: 0, date: null });
const [totalPrice, setTotalPrice] = useState(0);
const [selectedGallons, setSelectedGallons] = useState(100);
const [selectedDate, setSelectedDate] = useState(dayjs().add(1, 'day'));
@@ -36,7 +36,7 @@ const Order = () => {
gallons_ordered: 100,
expected_delivery_date: dayjs().add(1, 'day'),
dispatcher_notes: '',
payment_type: 1, // credit
payment_type: 11, // credit (Authorize.net)
});
const [notesLength, setNotesLength] = useState(0);
@@ -126,12 +126,12 @@ const Order = () => {
};
const handleGallonsChange = (value) => {
setFormData({ ...formData, gallons_ordered: value });
setFormData(prev => ({ ...prev, gallons_ordered: value }));
setSelectedGallons(value);
};
const handleDateChange = (date) => {
setFormData({ ...formData, expected_delivery_date: date });
setFormData(prev => ({ ...prev, expected_delivery_date: date }));
setSelectedDate(date);
};
@@ -157,9 +157,9 @@ const Order = () => {
// Get surcharge based on service type and prime
const getSurcharge = () => {
let surcharge = 0;
if (serviceType === 'same_day') surcharge += 45;
if (serviceType === 'emergency') surcharge += 250;
if (primeSelected) surcharge += serviceType === 'emergency' ? 50 : 25;
if (serviceType === 'same_day') surcharge += pricing.price_same_day;
if (serviceType === 'emergency') surcharge += pricing.price_emergency;
if (primeSelected) surcharge += pricing.price_prime;
return surcharge;
};
@@ -247,7 +247,7 @@ const Order = () => {
const onFinish = async () => {
setLoading(true);
try {
if (formData.payment_type === 1) {
if (formData.payment_type === 11) {
// Validate card selection
if (!selectedCardId) {
message.error('Please select a payment card or add a new one');
@@ -288,7 +288,7 @@ const Order = () => {
gallons_ordered: 100,
expected_delivery_date: dayjs().add(1, 'day'),
dispatcher_notes: '',
payment_type: 1,
payment_type: 11,
});
setServiceType(null);
setPrimeSelected(false);
@@ -318,7 +318,7 @@ const Order = () => {
gallons_ordered: 100,
expected_delivery_date: dayjs().add(1, 'day'),
dispatcher_notes: '',
payment_type: 1,
payment_type: 11,
});
setServiceType(null);
setPrimeSelected(false);
@@ -411,7 +411,7 @@ const Order = () => {
onClick={handleSameDaySelect}
style={{ marginRight: 10 }}
>
Same Day (+$45)
Same Day (+${pricing.price_same_day})
</Button>
)}
{!isSameDayAvailable() && (
@@ -423,7 +423,7 @@ const Order = () => {
type={serviceType === 'emergency' ? "primary" : "default"}
onClick={handleEmergencySelect}
>
Emergency (+$250)
Emergency (+${pricing.price_emergency})
</Button>
</div>
)}
@@ -446,7 +446,7 @@ const Order = () => {
type={primeSelected ? "primary" : "default"}
onClick={() => setPrimeSelected(!primeSelected)}
>
Prime (+${serviceType === 'emergency' ? 50 : 25})
Prime (+${pricing.price_prime})
</Button>
</div>
</div>
@@ -475,10 +475,10 @@ const Order = () => {
<Form.Item label="Payment Method">
<Radio.Group onChange={handlePaymentChange} value={formData.payment_type}>
<Radio value={0}>Cash</Radio>
<Radio value={1}>Credit</Radio>
<Radio value={11}>Credit</Radio>
</Radio.Group>
</Form.Item>
{formData.payment_type === 1 && (
{formData.payment_type === 11 && (
<div>
{loadingCards ? (
<Card style={{ textAlign: 'center', padding: spacing.lg }}>
@@ -561,17 +561,17 @@ const Order = () => {
<Descriptions.Item label="Gallons">{formData.gallons_ordered}</Descriptions.Item>
<Descriptions.Item label="Price per Gallon">${pricing.price_per_gallon.toFixed(2)}</Descriptions.Item>
{serviceType && (
<Descriptions.Item label="Service Surcharge">${serviceType === 'same_day' ? 45 : 250} ({serviceType === 'same_day' ? 'Same Day' : 'Emergency'})</Descriptions.Item>
<Descriptions.Item label="Service Surcharge">${serviceType === 'same_day' ? pricing.price_same_day : pricing.price_emergency} ({serviceType === 'same_day' ? 'Same Day' : 'Emergency'})</Descriptions.Item>
)}
{primeSelected && (
<Descriptions.Item label="Prime Surcharge">${serviceType === 'emergency' ? 50 : 25}</Descriptions.Item>
<Descriptions.Item label="Prime Surcharge">${pricing.price_prime}</Descriptions.Item>
)}
<Descriptions.Item label="Total Amount">${totalPrice.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="Delivery Date">{formData.expected_delivery_date.format('YYYY-MM-DD')}</Descriptions.Item>
<Descriptions.Item label="Notes">{formData.dispatcher_notes || 'None'}</Descriptions.Item>
<Descriptions.Item label="Payment Method">{formData.payment_type === 0 ? 'Cash' : 'Credit Card'}</Descriptions.Item>
</Descriptions>
{formData.payment_type === 1 && getSelectedCard() && (
{formData.payment_type === 11 && getSelectedCard() && (
<Card
style={{
marginTop: 16,

View File

@@ -1,12 +1,21 @@
import React from 'react';
import { Form, Input, Button, Card, message } from 'antd';
import React, { useEffect, useState } from 'react';
import { Form, Input, Button, Card, message, Alert } from 'antd';
import { MailOutlined, LockOutlined } from '@ant-design/icons';
import { useNavigate, Link } from 'react-router-dom';
import { useNavigate, Link, useLocation } from 'react-router-dom';
import api from '../../utils/api';
import { commonStyles } from '../../theme';
function Login() {
const navigate = useNavigate();
const location = useLocation();
const [showConfirmationBanner, setShowConfirmationBanner] = useState(false);
useEffect(() => {
const params = new URLSearchParams(location.search);
if (params.get('registered') === 'true') {
setShowConfirmationBanner(true);
}
}, [location]);
const onFinish = async (values) => {
try {
@@ -21,6 +30,17 @@ function Login() {
return (
<Card title="Login" style={{ maxWidth: 400, margin: '0 auto', ...commonStyles.cardGray }}>
{showConfirmationBanner && (
<Alert
message="Registration Successful"
description="An email has been sent to your email address with instructions to confirm your account."
type="success"
showIcon
closable
onClose={() => setShowConfirmationBanner(false)}
style={{ marginBottom: '16px' }}
/>
)}
<Form
name="login"
onFinish={onFinish}

View File

@@ -11,8 +11,8 @@ function Register() {
const onFinish = async (values) => {
try {
await api.post('/auth/register', values);
message.success('Registration successful, please login');
navigate('/login');
message.success('Registration successful, please check your email to confirm your account');
navigate('/login?registered=true');
} catch (error) {
message.error('Registration failed: ' + (error.response?.data?.detail || 'Unknown error'));
}

View File

@@ -147,7 +147,7 @@ function New() {
}
message.success('Customer registration completed successfully!');
navigate('/');
navigate('/login?registered=true');
} catch (error) {
if (error.response?.status === 400 && error.response.data?.detail) {
message.error(error.response.data.detail);