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:
@@ -25,7 +25,7 @@ const Order = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
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 [totalPrice, setTotalPrice] = useState(0);
|
||||||
const [selectedGallons, setSelectedGallons] = useState(100);
|
const [selectedGallons, setSelectedGallons] = useState(100);
|
||||||
const [selectedDate, setSelectedDate] = useState(dayjs().add(1, 'day'));
|
const [selectedDate, setSelectedDate] = useState(dayjs().add(1, 'day'));
|
||||||
@@ -36,7 +36,7 @@ const Order = () => {
|
|||||||
gallons_ordered: 100,
|
gallons_ordered: 100,
|
||||||
expected_delivery_date: dayjs().add(1, 'day'),
|
expected_delivery_date: dayjs().add(1, 'day'),
|
||||||
dispatcher_notes: '',
|
dispatcher_notes: '',
|
||||||
payment_type: 1, // credit
|
payment_type: 11, // credit (Authorize.net)
|
||||||
});
|
});
|
||||||
const [notesLength, setNotesLength] = useState(0);
|
const [notesLength, setNotesLength] = useState(0);
|
||||||
|
|
||||||
@@ -126,12 +126,12 @@ const Order = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGallonsChange = (value) => {
|
const handleGallonsChange = (value) => {
|
||||||
setFormData({ ...formData, gallons_ordered: value });
|
setFormData(prev => ({ ...prev, gallons_ordered: value }));
|
||||||
setSelectedGallons(value);
|
setSelectedGallons(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateChange = (date) => {
|
const handleDateChange = (date) => {
|
||||||
setFormData({ ...formData, expected_delivery_date: date });
|
setFormData(prev => ({ ...prev, expected_delivery_date: date }));
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -157,9 +157,9 @@ const Order = () => {
|
|||||||
// Get surcharge based on service type and prime
|
// Get surcharge based on service type and prime
|
||||||
const getSurcharge = () => {
|
const getSurcharge = () => {
|
||||||
let surcharge = 0;
|
let surcharge = 0;
|
||||||
if (serviceType === 'same_day') surcharge += 45;
|
if (serviceType === 'same_day') surcharge += pricing.price_same_day;
|
||||||
if (serviceType === 'emergency') surcharge += 250;
|
if (serviceType === 'emergency') surcharge += pricing.price_emergency;
|
||||||
if (primeSelected) surcharge += serviceType === 'emergency' ? 50 : 25;
|
if (primeSelected) surcharge += pricing.price_prime;
|
||||||
return surcharge;
|
return surcharge;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ const Order = () => {
|
|||||||
const onFinish = async () => {
|
const onFinish = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (formData.payment_type === 1) {
|
if (formData.payment_type === 11) {
|
||||||
// Validate card selection
|
// Validate card selection
|
||||||
if (!selectedCardId) {
|
if (!selectedCardId) {
|
||||||
message.error('Please select a payment card or add a new one');
|
message.error('Please select a payment card or add a new one');
|
||||||
@@ -288,7 +288,7 @@ const Order = () => {
|
|||||||
gallons_ordered: 100,
|
gallons_ordered: 100,
|
||||||
expected_delivery_date: dayjs().add(1, 'day'),
|
expected_delivery_date: dayjs().add(1, 'day'),
|
||||||
dispatcher_notes: '',
|
dispatcher_notes: '',
|
||||||
payment_type: 1,
|
payment_type: 11,
|
||||||
});
|
});
|
||||||
setServiceType(null);
|
setServiceType(null);
|
||||||
setPrimeSelected(false);
|
setPrimeSelected(false);
|
||||||
@@ -318,7 +318,7 @@ const Order = () => {
|
|||||||
gallons_ordered: 100,
|
gallons_ordered: 100,
|
||||||
expected_delivery_date: dayjs().add(1, 'day'),
|
expected_delivery_date: dayjs().add(1, 'day'),
|
||||||
dispatcher_notes: '',
|
dispatcher_notes: '',
|
||||||
payment_type: 1,
|
payment_type: 11,
|
||||||
});
|
});
|
||||||
setServiceType(null);
|
setServiceType(null);
|
||||||
setPrimeSelected(false);
|
setPrimeSelected(false);
|
||||||
@@ -411,7 +411,7 @@ const Order = () => {
|
|||||||
onClick={handleSameDaySelect}
|
onClick={handleSameDaySelect}
|
||||||
style={{ marginRight: 10 }}
|
style={{ marginRight: 10 }}
|
||||||
>
|
>
|
||||||
Same Day (+$45)
|
Same Day (+${pricing.price_same_day})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!isSameDayAvailable() && (
|
{!isSameDayAvailable() && (
|
||||||
@@ -423,7 +423,7 @@ const Order = () => {
|
|||||||
type={serviceType === 'emergency' ? "primary" : "default"}
|
type={serviceType === 'emergency' ? "primary" : "default"}
|
||||||
onClick={handleEmergencySelect}
|
onClick={handleEmergencySelect}
|
||||||
>
|
>
|
||||||
Emergency (+$250)
|
Emergency (+${pricing.price_emergency})
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -446,7 +446,7 @@ const Order = () => {
|
|||||||
type={primeSelected ? "primary" : "default"}
|
type={primeSelected ? "primary" : "default"}
|
||||||
onClick={() => setPrimeSelected(!primeSelected)}
|
onClick={() => setPrimeSelected(!primeSelected)}
|
||||||
>
|
>
|
||||||
Prime (+${serviceType === 'emergency' ? 50 : 25})
|
Prime (+${pricing.price_prime})
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -475,10 +475,10 @@ const Order = () => {
|
|||||||
<Form.Item label="Payment Method">
|
<Form.Item label="Payment Method">
|
||||||
<Radio.Group onChange={handlePaymentChange} value={formData.payment_type}>
|
<Radio.Group onChange={handlePaymentChange} value={formData.payment_type}>
|
||||||
<Radio value={0}>Cash</Radio>
|
<Radio value={0}>Cash</Radio>
|
||||||
<Radio value={1}>Credit</Radio>
|
<Radio value={11}>Credit</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{formData.payment_type === 1 && (
|
{formData.payment_type === 11 && (
|
||||||
<div>
|
<div>
|
||||||
{loadingCards ? (
|
{loadingCards ? (
|
||||||
<Card style={{ textAlign: 'center', padding: spacing.lg }}>
|
<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="Gallons">{formData.gallons_ordered}</Descriptions.Item>
|
||||||
<Descriptions.Item label="Price per Gallon">${pricing.price_per_gallon.toFixed(2)}</Descriptions.Item>
|
<Descriptions.Item label="Price per Gallon">${pricing.price_per_gallon.toFixed(2)}</Descriptions.Item>
|
||||||
{serviceType && (
|
{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 && (
|
{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="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="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="Notes">{formData.dispatcher_notes || 'None'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="Payment Method">{formData.payment_type === 0 ? 'Cash' : 'Credit Card'}</Descriptions.Item>
|
<Descriptions.Item label="Payment Method">{formData.payment_type === 0 ? 'Cash' : 'Credit Card'}</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
{formData.payment_type === 1 && getSelectedCard() && (
|
{formData.payment_type === 11 && getSelectedCard() && (
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Form, Input, Button, Card, message } from 'antd';
|
import { Form, Input, Button, Card, message, Alert } from 'antd';
|
||||||
import { MailOutlined, LockOutlined } from '@ant-design/icons';
|
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 api from '../../utils/api';
|
||||||
import { commonStyles } from '../../theme';
|
import { commonStyles } from '../../theme';
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const navigate = useNavigate();
|
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) => {
|
const onFinish = async (values) => {
|
||||||
try {
|
try {
|
||||||
@@ -21,6 +30,17 @@ function Login() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Login" style={{ maxWidth: 400, margin: '0 auto', ...commonStyles.cardGray }}>
|
<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
|
<Form
|
||||||
name="login"
|
name="login"
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ function Register() {
|
|||||||
const onFinish = async (values) => {
|
const onFinish = async (values) => {
|
||||||
try {
|
try {
|
||||||
await api.post('/auth/register', values);
|
await api.post('/auth/register', values);
|
||||||
message.success('Registration successful, please login');
|
message.success('Registration successful, please check your email to confirm your account');
|
||||||
navigate('/login');
|
navigate('/login?registered=true');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('Registration failed: ' + (error.response?.data?.detail || 'Unknown error'));
|
message.error('Registration failed: ' + (error.response?.data?.detail || 'Unknown error'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function New() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message.success('Customer registration completed successfully!');
|
message.success('Customer registration completed successfully!');
|
||||||
navigate('/');
|
navigate('/login?registered=true');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response?.status === 400 && error.response.data?.detail) {
|
if (error.response?.status === 400 && error.response.data?.detail) {
|
||||||
message.error(error.response.data.detail);
|
message.error(error.response.data.detail);
|
||||||
|
|||||||
Reference in New Issue
Block a user