Fixed the Customer Context. Because of how setState worked, customers were not properly registered.
Customer Profile Page, Customer Addresses Page, Checkout Page
This commit is contained in:
1 parent
81dc9acd81
commit
12d7d21d28
25 files changed
+3410
-8596
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from 'next';
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: 'standalone',
|
// output: 'standalone',
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
-7159
File diff suppressed because it is too large.
Load diff
+15
-15
@@ -6,13 +6,13 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/types": "^2.0.1",
|
"@medusajs/types": "^2.1.2",
|
||||||
"@mikro-orm/core": "^5.9.7",
|
"@mikro-orm/core": "^6.4.1",
|
||||||
"@mikro-orm/postgresql": "^6.3.13",
|
"@mikro-orm/postgresql": "^6.3.13",
|
||||||
"@next/third-parties": "^15.0.1",
|
"@next/third-parties": "15.1.1",
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
@@ -25,10 +25,10 @@
|
|||||||
"@tanstack/react-query": "^5.59.16",
|
"@tanstack/react-query": "^5.59.16",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.468.0",
|
||||||
"next": "^15.0.1",
|
"next": "15.1.1",
|
||||||
"react": "19.0.0-rc-69d4b800-20241021",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0-rc-69d4b800-20241021",
|
"react-dom": "19.0.0",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"swiper": "^11.1.14",
|
"swiper": "^11.1.14",
|
||||||
@@ -37,14 +37,14 @@
|
|||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/eslint-plugin-next": "^15.0.0-rc.1",
|
"@next/eslint-plugin-next": "15.1.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "19.0.2",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "19.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
||||||
"@typescript-eslint/parser": "^8.8.1",
|
"@typescript-eslint/parser": "^8.8.1",
|
||||||
"eslint": "^9.13.0",
|
"eslint": "^9.13.0",
|
||||||
"eslint-config-next": "^15.0.0-rc.1",
|
"eslint-config-next": "15.1.1",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-next": "^0.0.0",
|
"eslint-plugin-next": "^0.0.0",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"typescript-eslint": "^8.11.0"
|
"typescript-eslint": "^8.11.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "19.0.2",
|
||||||
"@types/react-dom": "18.3.1"
|
"@types/react-dom": "19.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,9 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Navbar />
|
|
||||||
<RegionProvider>
|
<RegionProvider>
|
||||||
<CustomerProvider>
|
<CustomerProvider>
|
||||||
|
<Navbar />
|
||||||
<CartProvider>{children}</CartProvider>
|
<CartProvider>{children}</CartProvider>
|
||||||
</CustomerProvider>
|
</CustomerProvider>
|
||||||
</RegionProvider>
|
</RegionProvider>
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { HttpTypes } from '@medusajs/types';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useCustomer } from '@/providers/customer';
|
||||||
|
import { useRegion } from '@/providers/region';
|
||||||
|
|
||||||
|
export default function AddressesManager() {
|
||||||
|
const { region } = useRegion();
|
||||||
|
const { customer, token, setCustomer } = useCustomer();
|
||||||
|
|
||||||
|
const [addresses, setAddresses] = useState<HttpTypes.StoreCustomerAddress[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [hasMorePages, setHasMorePages] = useState(false);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
id: '', // Empty for new addresses, set when editing
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
address1: '',
|
||||||
|
company: '',
|
||||||
|
postalCode: '',
|
||||||
|
city: '',
|
||||||
|
countryCode: '',
|
||||||
|
province: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const limit = 20;
|
||||||
|
|
||||||
|
// Fetch addresses when component loads or page changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!customer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const offset = (currentPage - 1) * limit;
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
limit: `${limit}`,
|
||||||
|
offset: `${offset}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/customers/me/addresses?${searchParams.toString()}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ addresses: addressesData, count }) => {
|
||||||
|
setAddresses((prev) => (prev.length > offset ? prev : [...prev, ...addressesData]));
|
||||||
|
setHasMorePages(count > limit * currentPage);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [currentPage, customer]);
|
||||||
|
|
||||||
|
// Handles input field changes
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles adding or updating an address
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const apiUrl = formData.id
|
||||||
|
? `${process.env.NEXT_PUBLIC_STORE_URL}/store/customers/me/addresses/${formData.id}` // Edit
|
||||||
|
: `${process.env.NEXT_PUBLIC_STORE_URL}/store/customers/me/addresses`; // Add
|
||||||
|
|
||||||
|
// In your code, "POST" is used both for adding and editing.
|
||||||
|
// If your backend uses PUT/PATCH for editing, adjust accordingly.
|
||||||
|
const method = 'POST';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(apiUrl, {
|
||||||
|
credentials: 'include',
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
first_name: formData.firstName,
|
||||||
|
last_name: formData.lastName,
|
||||||
|
address_1: formData.address1,
|
||||||
|
company: formData.company,
|
||||||
|
postal_code: formData.postalCode,
|
||||||
|
city: formData.city,
|
||||||
|
country_code: formData.countryCode,
|
||||||
|
province: formData.province,
|
||||||
|
phone: formData.phoneNumber,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
setCustomer(data.customer);
|
||||||
|
resetForm();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding/updating address', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles deleting an address
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/customers/me/addresses/${id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setAddresses((prev) => prev.filter((addr) => addr.id !== id));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting address', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sets form data when editing an address
|
||||||
|
const handleEdit = (address: HttpTypes.StoreCustomerAddress) => {
|
||||||
|
setIsEditing(true);
|
||||||
|
setFormData({
|
||||||
|
id: address.id,
|
||||||
|
firstName: address.first_name,
|
||||||
|
lastName: address.last_name,
|
||||||
|
address1: address.address_1,
|
||||||
|
company: address.company,
|
||||||
|
postalCode: address.postal_code,
|
||||||
|
city: address.city,
|
||||||
|
countryCode: address.country_code,
|
||||||
|
province: address.province,
|
||||||
|
phoneNumber: address.phone,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resets the form
|
||||||
|
const resetForm = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setFormData({
|
||||||
|
id: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
address1: '',
|
||||||
|
company: '',
|
||||||
|
postalCode: '',
|
||||||
|
city: '',
|
||||||
|
countryCode: '',
|
||||||
|
province: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='addresses-manager'>
|
||||||
|
<form onSubmit={handleSubmit} className='address-form'>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{isEditing ? 'Edit Address' : 'Add Address'}</legend>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='firstName'>First Name</label>
|
||||||
|
<input
|
||||||
|
id='firstName'
|
||||||
|
type='text'
|
||||||
|
name='firstName'
|
||||||
|
placeholder='First Name'
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='lastName'>Last Name</label>
|
||||||
|
<input
|
||||||
|
id='lastName'
|
||||||
|
type='text'
|
||||||
|
name='lastName'
|
||||||
|
placeholder='Last Name'
|
||||||
|
value={formData.lastName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='address1'>Address</label>
|
||||||
|
<input
|
||||||
|
id='address1'
|
||||||
|
type='text'
|
||||||
|
name='address1'
|
||||||
|
placeholder='Address'
|
||||||
|
value={formData.address1}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='company'>Company</label>
|
||||||
|
<input
|
||||||
|
id='company'
|
||||||
|
type='text'
|
||||||
|
name='company'
|
||||||
|
placeholder='Company'
|
||||||
|
value={formData.company}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='postalCode'>Postal Code</label>
|
||||||
|
<input
|
||||||
|
id='postalCode'
|
||||||
|
type='text'
|
||||||
|
name='postalCode'
|
||||||
|
placeholder='Postal Code'
|
||||||
|
value={formData.postalCode}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='city'>City</label>
|
||||||
|
<input
|
||||||
|
id='city'
|
||||||
|
type='text'
|
||||||
|
name='city'
|
||||||
|
placeholder='City'
|
||||||
|
value={formData.city}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='countryCode'>Country</label>
|
||||||
|
<select
|
||||||
|
id='countryCode'
|
||||||
|
name='countryCode'
|
||||||
|
value={formData.countryCode}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value='' disabled>
|
||||||
|
Select a country...
|
||||||
|
</option>
|
||||||
|
{region?.countries?.map((country) => (
|
||||||
|
<option key={country.iso_2} value={country.iso_2}>
|
||||||
|
{country.display_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='province'>Province</label>
|
||||||
|
<input
|
||||||
|
id='province'
|
||||||
|
type='text'
|
||||||
|
name='province'
|
||||||
|
placeholder='Province'
|
||||||
|
value={formData.province}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='phoneNumber'>Phone Number</label>
|
||||||
|
<input
|
||||||
|
id='phoneNumber'
|
||||||
|
type='tel'
|
||||||
|
name='phoneNumber'
|
||||||
|
placeholder='Phone Number'
|
||||||
|
value={formData.phoneNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-actions'>
|
||||||
|
<button type='submit' disabled={loading}>
|
||||||
|
{isEditing ? 'Update' : 'Add'}
|
||||||
|
</button>
|
||||||
|
{isEditing && (
|
||||||
|
<button type='button' onClick={resetForm}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<section className='address-list'>
|
||||||
|
<h2>Your Addresses</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>City</th>
|
||||||
|
<th>Country Code</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{addresses.map((address) => (
|
||||||
|
<tr key={address.id}>
|
||||||
|
<td>{address.city}</td>
|
||||||
|
<td>{address.country_code}</td>
|
||||||
|
<td>
|
||||||
|
<button onClick={() => handleEdit(address)}>Edit</button>
|
||||||
|
<button onClick={() => handleDelete(address.id)}>Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// app/addresses/page.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import AddressesManager from './AddressesManager';
|
||||||
|
|
||||||
|
export default function AddressesPage() {
|
||||||
|
return (
|
||||||
|
<div className='px-4 py-8 pt-28'>
|
||||||
|
<h1>Manage Your Addresses</h1>
|
||||||
|
<AddressesManager />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// app/account/profile/ProfileForm.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface Customer {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
phone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileFormProps {
|
||||||
|
customer: Customer;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfileForm({ customer, onCancel }: ProfileFormProps) {
|
||||||
|
const [firstName, setFirstName] = useState(customer.first_name || '');
|
||||||
|
const [lastName, setLastName] = useState(customer.last_name || '');
|
||||||
|
const [phone, setPhone] = useState(customer.phone || '');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/customers/me`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
phone: phone,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
// handle error
|
||||||
|
const errorData = await res.json();
|
||||||
|
setError(errorData?.message || 'Failed to update profile');
|
||||||
|
} else {
|
||||||
|
// success - you might want to refresh the page or re-fetch data
|
||||||
|
// For simplicity, let's just call onCancel to go back
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
setError('An error occurred while updating.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className='flex flex-col max-w-sm'>
|
||||||
|
{error && <p className='text-red-500 mb-2'>{error}</p>}
|
||||||
|
|
||||||
|
<label className='mb-2'>
|
||||||
|
First Name:
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
className='block w-full border border-gray-300 p-2 mt-1'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className='mb-2'>
|
||||||
|
Last Name:
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
className='block w-full border border-gray-300 p-2 mt-1'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className='mb-2'>
|
||||||
|
Phone:
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={phone}
|
||||||
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
|
className='block w-full border border-gray-300 p-2 mt-1'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className='flex gap-2 mt-4'>
|
||||||
|
<button type='submit' disabled={loading} className='px-4 py-2 bg-green-500 text-white rounded'>
|
||||||
|
{loading ? 'Saving...' : 'Save Changes'}
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={onCancel} disabled={loading} className='px-4 py-2 bg-gray-300 rounded'>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// app/account/profile/ProfileView.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import ProfileForm from './ProfileForm';
|
||||||
|
|
||||||
|
interface Customer {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
phone?: string;
|
||||||
|
// ... other fields from the Medusa customer object
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileViewProps {
|
||||||
|
customer: Customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfileView({ customer }: ProfileViewProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
setIsEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!isEditing ? (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<strong>Email:</strong> {customer.email}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>First name:</strong> {customer.first_name || 'N/A'}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Last name:</strong> {customer.last_name || 'N/A'}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Phone:</strong> {customer.phone || 'N/A'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button onClick={handleEditClick} className='mt-4 px-4 py-2 bg-blue-500 text-white rounded'>
|
||||||
|
Edit Profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ProfileForm customer={customer} onCancel={handleCancel} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// app/account/profile/page.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { useCustomer } from '@/providers/customer';
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
// Access customer data (and possibly other methods: register, login, logout)
|
||||||
|
const { customer, token } = useCustomer();
|
||||||
|
|
||||||
|
// For demonstration, we keep track of form states to update first/last name
|
||||||
|
const [firstName, setFirstName] = useState(customer?.first_name ?? '');
|
||||||
|
const [lastName, setLastName] = useState(customer?.last_name ?? '');
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
|
||||||
|
// If the user isn't logged in, `customer` will be undefined
|
||||||
|
if (!customer) {
|
||||||
|
return <div className='p-4'>Please log in to view your profile.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setMessage('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Make a PUT request to update the currently logged-in Medusa customer
|
||||||
|
const res = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/customers/me`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`, // pass the JWT
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY as string,
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorData = await res.json();
|
||||||
|
throw new Error(errorData?.message || 'Failed to update profile.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage('Profile updated successfully!');
|
||||||
|
|
||||||
|
// If you'd like to refresh the data in your context,
|
||||||
|
// you could call a function from your provider here (e.g., getCustomer()).
|
||||||
|
// Or simply do another fetch of /customers/me and update context:
|
||||||
|
// await someProviderMethodToRefreshCustomer();
|
||||||
|
} catch (error: any) {
|
||||||
|
setMessage(`Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='p-4'>
|
||||||
|
<h1 className='text-xl font-bold mb-4'>Your Profile</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Email:</strong> {customer.email}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>First Name:</strong> {customer.first_name}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Last Name:</strong> {customer.last_name}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr className='my-4' />
|
||||||
|
|
||||||
|
<h2 className='font-semibold mb-2'>Update Profile</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className='flex flex-col max-w-sm gap-2'>
|
||||||
|
{message && <p className='text-red-500'>{message}</p>}
|
||||||
|
|
||||||
|
<label>
|
||||||
|
New First Name:
|
||||||
|
<input
|
||||||
|
className='block w-full border border-gray-300 p-2 mt-1'
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
New Last Name:
|
||||||
|
<input
|
||||||
|
className='block w-full border border-gray-300 p-2 mt-1'
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type='submit' className='mt-2 bg-blue-600 text-white px-4 py-2 rounded'>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
<Link className='w-full mt-4 bg-white' href={'./addresses'}>
|
||||||
|
Manage addresses
|
||||||
|
</Link>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Minus, Plus, Trash2 } from 'lucide-react';
|
import { Minus, Plus, Trash2 } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
@@ -56,9 +57,9 @@ export default function MyCartComponent() {
|
|||||||
<span className='font-semibold'>Total</span>
|
<span className='font-semibold'>Total</span>
|
||||||
<span className='font-semibold'>₹{cart?.total?.toFixed(2)}</span>
|
<span className='font-semibold'>₹{cart?.total?.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button className='w-full mt-4 bg-white' size='lg'>
|
<Link className='w-full mt-4 bg-white' href={'/checkout/email'} size='lg'>
|
||||||
Proceed to Checkout
|
Proceed to Checkout
|
||||||
</Button>
|
</Link>
|
||||||
Need Help? Call us at 9915020200
|
Need Help? Call us at 9915020200
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useCart } from '@/providers/cart';
|
||||||
|
import { useCustomer } from '@/providers/customer';
|
||||||
|
|
||||||
|
export default function CheckoutAddressStep() {
|
||||||
|
const { cart, setCart } = useCart();
|
||||||
|
const { customer } = useCustomer();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// State for new address form fields
|
||||||
|
const [firstName, setFirstName] = useState('');
|
||||||
|
const [lastName, setLastName] = useState('');
|
||||||
|
const [address1, setAddress1] = useState('');
|
||||||
|
const [company, setCompany] = useState('');
|
||||||
|
const [postalCode, setPostalCode] = useState('');
|
||||||
|
const [city, setCity] = useState('');
|
||||||
|
const [countryCode, setCountryCode] = useState('');
|
||||||
|
const [province, setProvince] = useState('');
|
||||||
|
const [phoneNumber, setPhoneNumber] = useState('');
|
||||||
|
|
||||||
|
// State for saved address selection
|
||||||
|
const [selectedAddress, setSelectedAddress] = useState(customer?.addresses?.[0]?.id || '');
|
||||||
|
|
||||||
|
// Toggle between using a saved address and entering a new one
|
||||||
|
const [useSaved, setUseSaved] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customer && customer.addresses && customer.addresses.length > 0) {
|
||||||
|
setSelectedAddress(customer.addresses[0].id);
|
||||||
|
}
|
||||||
|
}, [customer]);
|
||||||
|
|
||||||
|
// Function to update the cart using a saved address
|
||||||
|
const updateAddressFromSaved = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!cart || !selectedAddress) return;
|
||||||
|
|
||||||
|
const customerAddress = customer?.addresses.find((address) => address.id === selectedAddress);
|
||||||
|
if (!customerAddress) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Build the address from the saved address data
|
||||||
|
const address = {
|
||||||
|
first_name: customerAddress.first_name || '',
|
||||||
|
last_name: customerAddress.last_name || '',
|
||||||
|
address_1: customerAddress.address_1 || '',
|
||||||
|
company: customerAddress.company || '',
|
||||||
|
postal_code: customerAddress.postal_code || '',
|
||||||
|
city: customerAddress.city || '',
|
||||||
|
country_code: customerAddress.country_code || cart.region?.countries?.[0]?.iso_2 || '',
|
||||||
|
province: customerAddress.province || '',
|
||||||
|
phone: customerAddress.phone || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/carts/${cart.id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
shipping_address: address,
|
||||||
|
billing_address: address,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const { cart: updatedCart } = await res.json();
|
||||||
|
setCart(updatedCart);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to update the cart using the new address form inputs
|
||||||
|
const updateAddressFromNew = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!cart) return;
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const address = {
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
address_1: address1,
|
||||||
|
company: company,
|
||||||
|
postal_code: postalCode,
|
||||||
|
city: city,
|
||||||
|
country_code: countryCode || cart.region?.countries?.[0]?.iso_2 || '',
|
||||||
|
province: province,
|
||||||
|
phone: phoneNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/carts/${cart.id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
shipping_address: address,
|
||||||
|
billing_address: address,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const { cart: updatedCart } = await res.json();
|
||||||
|
setCart(updatedCart);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='px-4 py-8 pt-28'>
|
||||||
|
<form>
|
||||||
|
{!cart && <span>Loading...</span>}
|
||||||
|
|
||||||
|
{/* Toggle for saved vs. new address */}
|
||||||
|
<div className='mb-4'>
|
||||||
|
<label className='mr-4'>
|
||||||
|
<input type='radio' checked={useSaved} onChange={() => setUseSaved(true)} /> Use Saved Address
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type='radio' checked={!useSaved} onChange={() => setUseSaved(false)} /> Enter New Address
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{useSaved ? (
|
||||||
|
<div className='mb-4'>
|
||||||
|
{(customer?.addresses?.length ?? 0) > 0 ? (
|
||||||
|
<>
|
||||||
|
<select
|
||||||
|
value={selectedAddress}
|
||||||
|
onChange={(e) => setSelectedAddress(e.target.value)}
|
||||||
|
className='mb-4 p-2 border border-gray-300 rounded'
|
||||||
|
>
|
||||||
|
{customer?.addresses?.map((address) => (
|
||||||
|
<option value={address.id} key={address.id}>
|
||||||
|
{address.country_code} – {address.address_1}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
disabled={!cart || loading || !selectedAddress}
|
||||||
|
onClick={updateAddressFromSaved}
|
||||||
|
className='bg-blue-500 text-white px-4 py-2 rounded'
|
||||||
|
>
|
||||||
|
{loading ? 'Saving...' : 'Save Address'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>No saved addresses available.</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='First Name'
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
className='p-2 border border-gray-300 rounded'
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Last Name'
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
className='p-2 border border-gray-300 rounded'
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Address Line'
|
||||||
|
value={address1}
|
||||||
|
onChange={(e) => setAddress1(e.target.value)}
|
||||||
|
className='p-2 border border-gray-300 rounded'
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Company'
|
||||||
|
value={company}
|
||||||
|
onChange={(e) => setCompany(e.target.value)}
|
||||||
|
className='p-2 border border-gray-300 rounded'
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Postal Code'
|
||||||
|
value={postalCode}
|
||||||
|
onChange={(e) => setPostalCode(e.target.value)}
|
||||||
|
className='p-2 border border-gray-300 rounded'
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='City'
|
||||||
|
value={city}
|
||||||
|
onChange={(e) => setCity(e.target.value)}
|
||||||
|
className='p-2 border border-gray-300 rounded'
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={countryCode}
|
||||||
|
onChange={(e) => setCountryCode(e.target.value)}
|
||||||
|
className='p-2 border border-gray-300 rounded'
|
||||||
|
>
|
||||||
|
{cart?.region?.countries?.map((country) => (
|
||||||
|
<option key={country.iso_2} value={country.iso_2}>
|
||||||
|
{country.display_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Province'
|
||||||
|
value={province}
|
||||||
|
onChange={(e) => setProvince(e.target.value)}
|
||||||
|
className='p-2 border border-gray-300 rounded'
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='tel'
|
||||||
|
placeholder='Phone Number'
|
||||||
|
value={phoneNumber}
|
||||||
|
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||||
|
className='p-2 border border-gray-300 rounded'
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
disabled={!cart || loading}
|
||||||
|
onClick={updateAddressFromNew}
|
||||||
|
className='bg-blue-500 text-white px-4 py-2 rounded'
|
||||||
|
>
|
||||||
|
{loading ? 'Saving...' : 'Save Address'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
<Link className='w-full mt-4 bg-white' href={'/checkout/shipping'} size='lg'>
|
||||||
|
Proceed to Shipping
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.formInput {
|
||||||
|
@apply border border-[hsl(0,0%,50%)] rounded-md py-3 px-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formButton {
|
||||||
|
@apply py-2 px-4 bg-[#703133] text-white font-semibold shadow focus:outline-none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
'use client'; // include with Next.js 13+
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useCart } from '@/providers/cart';
|
||||||
|
import { useCustomer } from '@/providers/customer';
|
||||||
|
|
||||||
|
export default function CheckoutEmailStep() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { cart, setCart } = useCart();
|
||||||
|
// Check if there's a logged-in customer:
|
||||||
|
const { customer } = useCustomer();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// If a customer is already logged in, skip this step
|
||||||
|
useEffect(() => {
|
||||||
|
if (customer) {
|
||||||
|
router.replace('/checkout/address');
|
||||||
|
}
|
||||||
|
}, [customer, router]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cart && !cart.items?.length) {
|
||||||
|
console.log('Cart is empty');
|
||||||
|
// TODO redirect to another path
|
||||||
|
} else {
|
||||||
|
console.log(cart);
|
||||||
|
}
|
||||||
|
}, [cart]);
|
||||||
|
|
||||||
|
const updateCartEmail = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
if (!cart || !email.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/carts/${cart.id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ cart: updatedCart }) => {
|
||||||
|
setCart(updatedCart);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='px-4 py-8 pt-28'>
|
||||||
|
{!cart && <span>Loading...</span>}
|
||||||
|
<input
|
||||||
|
type='email'
|
||||||
|
placeholder='Email'
|
||||||
|
value={email}
|
||||||
|
disabled={!cart}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button disabled={!cart || !email || loading} onClick={updateCartEmail}>
|
||||||
|
Set Email
|
||||||
|
</button>
|
||||||
|
<Link className='w-full mt-4 bg-white' href={'/checkout/address'} size='lg'>
|
||||||
|
Proceed to Address
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import styles from './checkout.module.css';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { useCart } from '@/providers/cart';
|
||||||
|
|
||||||
|
export default function CheckoutPage() {
|
||||||
|
const orderItems = [
|
||||||
|
{ name: 'Product 1', price: 19.99, quantity: 2 },
|
||||||
|
{ name: 'Product 2', price: 29.99, quantity: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const subtotal = orderItems.reduce((acc, item) => acc + item.price * item.quantity, 0);
|
||||||
|
const shipping = 5.99;
|
||||||
|
const total = subtotal + shipping;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen pt-28 grid md:grid-cols-2 gap-8 overflow-visible'>
|
||||||
|
<div className='sticky self-start flex flex-row gap-4 flex-wrap p-4'>
|
||||||
|
<h3 className='basis-full'>Delivery</h3>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='country'
|
||||||
|
className={`grow shrink basis-full ${styles.formInput}`}
|
||||||
|
placeholder='Country / Region'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='firstName'
|
||||||
|
className={`grow shrink basis-5/12 ${styles.formInput}`}
|
||||||
|
placeholder='First Name'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='lastName'
|
||||||
|
className={`grow shrink basis-5/12 box-border ${styles.formInput}`}
|
||||||
|
placeholder='Last Name'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='address'
|
||||||
|
className={`grow shrink basis-full box-border ${styles.formInput}`}
|
||||||
|
placeholder='Address'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='addressTwo'
|
||||||
|
className={`grow shrink basis-full box-border ${styles.formInput}`}
|
||||||
|
placeholder='Additional Address'
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='city'
|
||||||
|
className={`grow shrink basis-3/12 box-border ${styles.formInput}`}
|
||||||
|
placeholder='City'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='state'
|
||||||
|
className={`grow shrink basis-3/12 box-border ${styles.formInput}`}
|
||||||
|
placeholder='State'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='pincode'
|
||||||
|
className={`grow shrink basis-3/12 box-border ${styles.formInput}`}
|
||||||
|
placeholder='PIN Code'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='phone'
|
||||||
|
className={`grow shrink basis-full box-border ${styles.formInput}`}
|
||||||
|
placeholder='Phone'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Payment Details</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='space-y-4'>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='cardNumber'>Card Number</Label>
|
||||||
|
<Input id='cardNumber' placeholder='1234 5678 9012 3456' />
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='expirationDate'>Expiration Date</Label>
|
||||||
|
<Input id='expirationDate' placeholder='MM/YY' />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='cvv'>CVV</Label>
|
||||||
|
<Input id='cvv' placeholder='123' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button className='w-full'>Place Order</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className='sticky self-start'>
|
||||||
|
<div className='space-y-8'>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Order Summary</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='space-y-4'>
|
||||||
|
{orderItems.map((item, index) => (
|
||||||
|
<div key={index} className='flex justify-between'>
|
||||||
|
<span>
|
||||||
|
{item.name} x {item.quantity}
|
||||||
|
</span>
|
||||||
|
<span>${(item.price * item.quantity).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Separator />
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span>Subtotal</span>
|
||||||
|
<span>${subtotal.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span>Shipping</span>
|
||||||
|
<span>${shipping.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className='flex justify-between font-bold'>
|
||||||
|
<span>Total</span>
|
||||||
|
<span>${total.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
'use client'; // include with Next.js 13+
|
||||||
|
|
||||||
|
import { HttpTypes } from '@medusajs/types';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import RazorpayPayment from './razorpay';
|
||||||
|
|
||||||
|
import { useCart } from '@/providers/cart';
|
||||||
|
|
||||||
|
export default function CheckoutPaymentStep() {
|
||||||
|
const { cart, setCart } = useCart();
|
||||||
|
const [paymentProviders, setPaymentProviders] = useState<HttpTypes.StorePaymentProvider[]>([]);
|
||||||
|
const [selectedPaymentProvider, setSelectedPaymentProvider] = useState<string | undefined>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('checkout payment step, load razorpay script');
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
|
||||||
|
script.async = true;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(script);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/payment-providers?region_id=${cart.region_id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ payment_providers }) => {
|
||||||
|
setPaymentProviders(payment_providers);
|
||||||
|
setSelectedPaymentProvider(cart.payment_collection?.payment_sessions?.[0]?.id || payment_providers[0].id);
|
||||||
|
});
|
||||||
|
}, [cart]);
|
||||||
|
|
||||||
|
const handleSelectProvider = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('checkout payment step, handleSelectProvider', cart, paymentProviders, selectedPaymentProvider);
|
||||||
|
if (!cart || !selectedPaymentProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
let paymentCollectionId = cart.payment_collection?.id;
|
||||||
|
if (!paymentCollectionId) {
|
||||||
|
// create payment collection
|
||||||
|
const { payment_collection } = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/payment-collections`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
cart_id: cart.id,
|
||||||
|
}),
|
||||||
|
}).then((res) => res.json());
|
||||||
|
|
||||||
|
paymentCollectionId = payment_collection.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize payment session
|
||||||
|
const sess = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_STORE_URL}/store/payment-collections/${paymentCollectionId}/payment-sessions`,
|
||||||
|
{
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider_id: selectedPaymentProvider,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
).then((res) => res.json());
|
||||||
|
|
||||||
|
console.log('sess', sess);
|
||||||
|
// re-fetch cart
|
||||||
|
const { cart: updatedCart } = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/carts/${cart.id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
}).then((res) => res.json());
|
||||||
|
|
||||||
|
setCart(updatedCart);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaymentUi = useCallback(() => {
|
||||||
|
const activePaymentSession = cart?.payment_collection?.payment_sessions?.[0];
|
||||||
|
if (!activePaymentSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case activePaymentSession.provider_id.startsWith('pp_razorpay_'):
|
||||||
|
return <RazorpayPayment />;
|
||||||
|
case activePaymentSession.provider_id.startsWith('pp_system_default'):
|
||||||
|
return <span>You chose manual payment! No additional actions required.</span>;
|
||||||
|
default:
|
||||||
|
return <span>You chose {activePaymentSession.provider_id} which is in development.</span>;
|
||||||
|
}
|
||||||
|
}, [cart]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='px-4 py-8 pt-28'>
|
||||||
|
<form>
|
||||||
|
<select value={selectedPaymentProvider} onChange={(e) => setSelectedPaymentProvider(e.target.value)}>
|
||||||
|
{paymentProviders.map((provider) => (
|
||||||
|
<option key={provider.id} value={provider.id}>
|
||||||
|
{provider.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={async (e) => {
|
||||||
|
await handleSelectProvider(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{getPaymentUi()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
'use client'; // include with Next.js 13+
|
||||||
|
|
||||||
|
import { HttpTypes } from '@medusajs/types';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import RazorpayPayment from './razorpay';
|
||||||
|
|
||||||
|
import { useCart } from '@/providers/cart';
|
||||||
|
|
||||||
|
export default function CheckoutPaymentStep() {
|
||||||
|
const { cart, setCart } = useCart();
|
||||||
|
const [paymentProvider, setPaymentProvider] = useState<HttpTypes.StorePaymentProvider | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
|
||||||
|
script.async = true;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(script);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('useEffect cart', cart);
|
||||||
|
|
||||||
|
fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/payment-providers?region_id=${cart.region_id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ payment_providers }) => {
|
||||||
|
if (JSON.stringify(payment_providers[0]) !== JSON.stringify(paymentProvider)) {
|
||||||
|
setPaymentProvider(payment_providers[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [cart]);
|
||||||
|
|
||||||
|
const fetchOrCreatePaymentCollection = async (cart: any) => {
|
||||||
|
const paymentCollectionId = cart.payment_collection?.id;
|
||||||
|
if (paymentCollectionId) {
|
||||||
|
return paymentCollectionId;
|
||||||
|
}
|
||||||
|
// create payment collection
|
||||||
|
const { payment_collection } = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/payment-collections`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
cart_id: cart.id,
|
||||||
|
}),
|
||||||
|
}).then((res) => res.json());
|
||||||
|
|
||||||
|
return payment_collection.id;
|
||||||
|
};
|
||||||
|
const initializePaymentSession = async (paymentCollectionId: string, providerId: string) => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_STORE_URL}/store/payment-collections/${paymentCollectionId}/payment-sessions`,
|
||||||
|
{
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider_id: providerId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const sessionData = await response.json();
|
||||||
|
return sessionData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUpdatedCart = async (cartId: string) => {
|
||||||
|
const response = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/carts/${cartId}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { cart: updatedCart } = await response.json();
|
||||||
|
return updatedCart;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('useEffect paymentProvider', cart, paymentProvider);
|
||||||
|
if (!cart || !paymentProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const initializePayment = async () => {
|
||||||
|
try {
|
||||||
|
// Step 1: Fetch or create the Payment Collection
|
||||||
|
const paymentCollectionId = await fetchOrCreatePaymentCollection(cart);
|
||||||
|
|
||||||
|
// Step 2: Initialize Payment Session for the provider
|
||||||
|
const session = await initializePaymentSession(paymentCollectionId, paymentProvider.id);
|
||||||
|
|
||||||
|
// Step 3: Fetch and update the cart to reflect the changes
|
||||||
|
const updatedCart = await fetchUpdatedCart(cart.id);
|
||||||
|
if (JSON.stringify(cart) !== JSON.stringify(updatedCart)) {
|
||||||
|
setCart(updatedCart);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing payment:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializePayment();
|
||||||
|
}, [paymentProvider]);
|
||||||
|
|
||||||
|
const getPaymentUi = useCallback(() => {
|
||||||
|
const activePaymentSession = cart?.payment_collection?.payment_sessions?.[0];
|
||||||
|
if (activePaymentSession) {
|
||||||
|
return <RazorpayPayment />;
|
||||||
|
}
|
||||||
|
}, [cart]);
|
||||||
|
|
||||||
|
return <div className='px-4 py-8 pt-28'>{getPaymentUi()}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
'use client'; // required for Next.js 13+ Client Components
|
||||||
|
|
||||||
|
import Script from 'next/script';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { useCart } from '@/providers/cart';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component injects the Razorpay checkout script
|
||||||
|
* and displays the `RazorpayForm` after the script has loaded.
|
||||||
|
*/
|
||||||
|
export default function RazorpayPayment() {
|
||||||
|
const [scriptLoaded, setScriptLoaded] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/*
|
||||||
|
Load the Razorpay script directly from their CDN.
|
||||||
|
Once loaded, you can safely initialize the checkout
|
||||||
|
*/}
|
||||||
|
<Script src='https://checkout.razorpay.com/v1/checkout.js' onLoad={() => setScriptLoaded(true)} />
|
||||||
|
|
||||||
|
{/* Only render the payment form if the script has loaded */}
|
||||||
|
{scriptLoaded && <RazorpayForm />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple form that triggers the Razorpay checkout
|
||||||
|
* using data from the cart.
|
||||||
|
*/
|
||||||
|
function RazorpayForm() {
|
||||||
|
const { cart, refreshCart } = useCart();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Typically the order_id is generated on your server (Medusa backend)
|
||||||
|
// and passed back via the payment session's data.
|
||||||
|
const order_id = cart?.payment_collection?.payment_sessions?.[0]?.data?.id;
|
||||||
|
// fallback currency, if needed
|
||||||
|
const currency = cart?.payment_collection?.payment_sessions?.[0]?.data?.currency || 'INR';
|
||||||
|
// fallback amount in smallest currency unit, if needed
|
||||||
|
const amount = cart?.payment_collection?.payment_sessions?.[0]?.data?.amount || cart?.total;
|
||||||
|
|
||||||
|
// The "Place Order" button triggers Razorpay's checkout
|
||||||
|
const handlePayment = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!cart || !order_id) {
|
||||||
|
console.error('Cart or order_id not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Configure the Razorpay checkout options
|
||||||
|
const options = {
|
||||||
|
key: process.env.NEXT_PUBLIC_RAZORPAY_KEY ?? 'test_key', // replace with your actual key
|
||||||
|
amount: amount, // in the smallest currency unit (e.g. paise for INR)
|
||||||
|
currency: currency,
|
||||||
|
name: 'Mozimo Chocolates',
|
||||||
|
description: 'Mozimo + Razorpay Payment',
|
||||||
|
order_id: order_id,
|
||||||
|
handler: async function (response: any) {
|
||||||
|
// This function is called when payment is successful
|
||||||
|
// `response.razorpay_payment_id` will have the Payment ID
|
||||||
|
// `response.razorpay_order_id` will have the Order ID
|
||||||
|
// `response.razorpay_signature` will have the signature
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Complete the cart in Medusa
|
||||||
|
const result = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/carts/${cart.id}/complete`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
const data = await result.json();
|
||||||
|
// Example response shape:
|
||||||
|
// { type: "order" | "cart", order?: any, cart?: any, error?: any }
|
||||||
|
|
||||||
|
if (data.type === 'cart' && data.cart) {
|
||||||
|
// Some error or failure state
|
||||||
|
console.error('Error completing cart:', data.error);
|
||||||
|
} else if (data.type === 'order' && data.order) {
|
||||||
|
// Payment and order creation successful
|
||||||
|
alert('Order placed.');
|
||||||
|
// Clears/reloads cart in your client state
|
||||||
|
refreshCart();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error completing cart:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prefill: {
|
||||||
|
// Prefill customer data in the Razorpay form
|
||||||
|
name: cart.billing_address?.first_name ?? '',
|
||||||
|
email: cart.email ?? '',
|
||||||
|
contact: cart.billing_address?.phone ?? '',
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
address: `${cart.billing_address?.address_1 ?? ''} ${cart.billing_address?.address_2 ?? ''}`,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
color: '#3399cc',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new Razorpay checkout instance and open it
|
||||||
|
const rzp = new (window as any).Razorpay(options);
|
||||||
|
|
||||||
|
// Optional: you can bind an event for payment failure as well
|
||||||
|
rzp.on('payment.failed', function (response: any) {
|
||||||
|
setLoading(false);
|
||||||
|
console.error('Payment failed:', response.error);
|
||||||
|
// handle failure in your UI
|
||||||
|
});
|
||||||
|
|
||||||
|
rzp.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/*
|
||||||
|
Because Razorpay handles the card input in its own modal,
|
||||||
|
we only need a simple button. We do not need something like
|
||||||
|
Stripe's <CardElement /> for card fields.
|
||||||
|
*/}
|
||||||
|
<button onClick={handlePayment} disabled={loading || !order_id}>
|
||||||
|
{loading ? 'Processing...' : 'Place Order'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
'use client'; // include with Next.js 13+
|
||||||
|
|
||||||
|
import { CardElement, Elements, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||||
|
import { loadStripe } from '@stripe/stripe-js';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useCart } from '../../providers/cart';
|
||||||
|
|
||||||
|
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PK || 'temp');
|
||||||
|
|
||||||
|
export default function StripePayment() {
|
||||||
|
const { cart } = useCart();
|
||||||
|
const clientSecret = cart?.payment_collection?.payment_sessions?.[0].data.client_secret as string;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Elements
|
||||||
|
stripe={stripePromise}
|
||||||
|
options={{
|
||||||
|
clientSecret,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StripeForm clientSecret={clientSecret} />
|
||||||
|
</Elements>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StripeForm = ({ clientSecret }: { clientSecret: string | undefined }) => {
|
||||||
|
const { cart, refreshCart } = useCart();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const stripe = useStripe();
|
||||||
|
const elements = useElements();
|
||||||
|
|
||||||
|
async function handlePayment(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||||
|
e.preventDefault();
|
||||||
|
const card = elements?.getElement(CardElement);
|
||||||
|
|
||||||
|
if (!stripe || !elements || !card || !cart || !clientSecret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
stripe
|
||||||
|
?.confirmCardPayment(clientSecret, {
|
||||||
|
payment_method: {
|
||||||
|
card,
|
||||||
|
billing_details: {
|
||||||
|
name: cart.billing_address?.first_name,
|
||||||
|
email: cart.email,
|
||||||
|
phone: cart.billing_address?.phone,
|
||||||
|
address: {
|
||||||
|
city: cart.billing_address?.city,
|
||||||
|
country: cart.billing_address?.country_code,
|
||||||
|
line1: cart.billing_address?.address_1,
|
||||||
|
line2: cart.billing_address?.address_2,
|
||||||
|
postal_code: cart.billing_address?.postal_code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ error }) => {
|
||||||
|
if (error) {
|
||||||
|
// TODO handle errors
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`http://localhost:9000/store/carts/${cart.id}/complete`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ type, cart, order, error }) => {
|
||||||
|
if (type === 'cart' && cart) {
|
||||||
|
// an error occured
|
||||||
|
console.error(error);
|
||||||
|
} else if (type === 'order' && order) {
|
||||||
|
// TODO redirect to order success page
|
||||||
|
alert('Order placed.');
|
||||||
|
console.log(order);
|
||||||
|
refreshCart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<CardElement />
|
||||||
|
<button onClick={handlePayment} disabled={loading}>
|
||||||
|
Place Order
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
'use client'; // include with Next.js 13+
|
||||||
|
|
||||||
|
import { HttpTypes } from '@medusajs/types';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useCart } from '@/providers/cart';
|
||||||
|
|
||||||
|
export default function CheckoutShippingStep() {
|
||||||
|
const { cart, setCart } = useCart();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [shippingOptions, setShippingOptions] = useState<HttpTypes.StoreCartShippingOption[]>([]);
|
||||||
|
const [calculatedPrices, setCalculatedPrices] = useState<Record<string, number>>({});
|
||||||
|
const [selectedShippingOption, setSelectedShippingOption] = useState<string | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/shipping-options?cart_id=${cart.id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ shipping_options }) => {
|
||||||
|
setShippingOptions(shipping_options);
|
||||||
|
});
|
||||||
|
}, [cart]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cart || !shippingOptions.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = shippingOptions
|
||||||
|
.filter((shippingOption) => shippingOption.price_type === 'calculated')
|
||||||
|
.map((shippingOption) =>
|
||||||
|
fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/shipping-options/${shippingOption.id}/calculate`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
cart_id: cart.id,
|
||||||
|
data: {
|
||||||
|
// pass any data useful for calculation with third-party provider.
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}).then((res) => res.json()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (promises.length) {
|
||||||
|
Promise.allSettled(promises).then((res) => {
|
||||||
|
const pricesMap: Record<string, number> = {};
|
||||||
|
res
|
||||||
|
.filter((r) => r.status === 'fulfilled')
|
||||||
|
.forEach((p) => (pricesMap[p.value?.shipping_option.id || ''] = p.value?.shipping_option.amount));
|
||||||
|
|
||||||
|
setCalculatedPrices(pricesMap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [shippingOptions, cart]);
|
||||||
|
|
||||||
|
const setShipping = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
if (!cart || !selectedShippingOption) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/carts/${cart.id}/shipping-methods`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || 'temp',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
option_id: selectedShippingOption,
|
||||||
|
data: {
|
||||||
|
// TODO add any data necessary for
|
||||||
|
// fulfillment provider
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ cart: updatedCart }) => {
|
||||||
|
setCart(updatedCart);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (amount: number): string => {
|
||||||
|
return new Intl.NumberFormat('en-IN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: cart?.currency_code || 'INR',
|
||||||
|
}).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShippingOptionPrice = useCallback(
|
||||||
|
(shippingOption: HttpTypes.StoreCartShippingOption) => {
|
||||||
|
if (shippingOption.price_type === 'flat') {
|
||||||
|
return formatPrice(shippingOption.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!calculatedPrices[shippingOption.id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatPrice(calculatedPrices[shippingOption.id]);
|
||||||
|
},
|
||||||
|
[calculatedPrices],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='px-4 py-8 pt-28'>
|
||||||
|
{loading || (!cart && <span>Loading...</span>)}
|
||||||
|
<form>
|
||||||
|
<select value={selectedShippingOption} onChange={(e) => setSelectedShippingOption(e.target.value)}>
|
||||||
|
{shippingOptions.map((shippingOption) => {
|
||||||
|
const price = getShippingOptionPrice(shippingOption);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<option key={shippingOption.id} value={shippingOption.id} disabled={price === undefined}>
|
||||||
|
{shippingOption.name} - {price}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<button disabled={loading || !cart} onClick={setShipping}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<Link className='w-full mt-4 bg-white' href={'/checkout/payment'} size='lg'>
|
||||||
|
Proceed to Payment
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,13 +20,15 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Navbar />
|
|
||||||
<RegionProvider>
|
<RegionProvider>
|
||||||
<CustomerProvider>
|
<CustomerProvider>
|
||||||
<CartProvider>{children}</CartProvider>
|
<CartProvider>
|
||||||
|
<Navbar />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
</CartProvider>
|
||||||
</CustomerProvider>
|
</CustomerProvider>
|
||||||
</RegionProvider>
|
</RegionProvider>
|
||||||
<Footer />
|
|
||||||
</body>
|
</body>
|
||||||
<GoogleAnalytics gaId='G-R3FGFLLVRX' />
|
<GoogleAnalytics gaId='G-R3FGFLLVRX' />
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ async function fetchProducts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(JSON.stringify(data.products));
|
|
||||||
return data.products;
|
return data.products;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,21 @@ import logoBlack from '/public/images/logo-black.svg';
|
|||||||
|
|
||||||
import styles from './navbar.module.css';
|
import styles from './navbar.module.css';
|
||||||
|
|
||||||
|
import { useCustomer } from '@/providers/customer';
|
||||||
|
|
||||||
// components/Navbar.tsx
|
// components/Navbar.tsx
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
|
const { customer, logout } = useCustomer();
|
||||||
|
|
||||||
const prevScrollY = useRef(0);
|
const prevScrollY = useRef(0);
|
||||||
const [navBarStyle, setNavBarStyle] = useState('initial');
|
const [navBarStyle, setNavBarStyle] = useState('initial');
|
||||||
const [isVisible, setIsVisible] = useState('false');
|
const [isVisible, setIsVisible] = useState('false');
|
||||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// New piece of state for the profile dropdown
|
||||||
|
const [isProfileDropdownOpen, setIsProfileDropdownOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const currentScrollY = window.scrollY;
|
const currentScrollY = window.scrollY;
|
||||||
@@ -33,7 +42,18 @@ export function Navbar() {
|
|||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
}, []); // Empty dependency array
|
}, []);
|
||||||
|
|
||||||
|
// Close dropdown if you click outside of it
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsProfileDropdownOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// const navClasses = `${styles.navBar} ${
|
// const navClasses = `${styles.navBar} ${
|
||||||
// navBarStyle === 'scrolledUp'
|
// navBarStyle === 'scrolledUp'
|
||||||
@@ -56,6 +76,13 @@ export function Navbar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// You can define your logout function here or in the customer provider
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
console.log('User logged out');
|
||||||
|
// Optionally redirect or handle client-side state cleanup
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
@@ -94,9 +121,34 @@ export function Navbar() {
|
|||||||
<button aria-label='Search' className='rounded-full hover:shadow-md'>
|
<button aria-label='Search' className='rounded-full hover:shadow-md'>
|
||||||
<FiSearch size={20} className='m-2.5' />
|
<FiSearch size={20} className='m-2.5' />
|
||||||
</button>
|
</button>
|
||||||
<button aria-label='Profile' className='rounded-full hover:shadow-md'>
|
|
||||||
<FiUser size={20} className='m-2.5' />
|
{/* Show dropdown if user is logged in */}
|
||||||
</button>
|
{customer && customer.id ? (
|
||||||
|
<div ref={dropdownRef} className='relative'>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsProfileDropdownOpen((prev) => !prev)}
|
||||||
|
className='rounded-full hover:shadow-md'
|
||||||
|
>
|
||||||
|
<FiUser size={20} className='m-2.5' />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isProfileDropdownOpen && (
|
||||||
|
<div className='absolute right-0 mt-2 w-44 bg-white border rounded-md shadow-md p-2 text-sm z-50'>
|
||||||
|
<Link href='/account/profile' className='block px-2 py-1 hover:bg-gray-100'>
|
||||||
|
Profile
|
||||||
|
</Link>
|
||||||
|
<button onClick={handleLogout} className='block w-full text-left px-2 py-1 hover:bg-gray-100'>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* If user is not logged in, show the link to login */
|
||||||
|
<Link href='/login' className='rounded-full hover:shadow-md'>
|
||||||
|
<FiUser size={20} className='m-2.5' />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<a aria-label='Shopping Cart' href='/cart' className='rounded-full hover:shadow-md'>
|
<a aria-label='Shopping Cart' href='/cart' className='rounded-full hover:shadow-md'>
|
||||||
<HiShoppingBag size={20} className='m-2.5' />
|
<HiShoppingBag size={20} className='m-2.5' />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { cn } from '@/lib/utils';
|
|||||||
const Separator = React.forwardRef<
|
const Separator = React.forwardRef<
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
|
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
+27
-26
@@ -5,9 +5,11 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
|
|||||||
|
|
||||||
interface CustomerContextType {
|
interface CustomerContextType {
|
||||||
customer: HttpTypes.StoreCustomer | undefined;
|
customer: HttpTypes.StoreCustomer | undefined;
|
||||||
|
setCustomer: React.Dispatch<React.SetStateAction<HttpTypes.StoreCustomer | undefined>>;
|
||||||
register: (firstName: string, lastName: string, email: string, password: string) => Promise<void>;
|
register: (firstName: string, lastName: string, email: string, password: string) => Promise<void>;
|
||||||
login: (email: string, password: string) => Promise<void>;
|
login: (email: string, password: string) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
token: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomerContext = createContext<CustomerContextType | null>(null);
|
const CustomerContext = createContext<CustomerContextType | null>(null);
|
||||||
@@ -18,13 +20,12 @@ interface CustomerProviderProps {
|
|||||||
|
|
||||||
export const CustomerProvider = ({ children }: CustomerProviderProps) => {
|
export const CustomerProvider = ({ children }: CustomerProviderProps) => {
|
||||||
const [customer, setCustomer] = useState<HttpTypes.StoreCustomer | undefined>();
|
const [customer, setCustomer] = useState<HttpTypes.StoreCustomer | undefined>();
|
||||||
const [jwt, setJwt] = useState<string | null>();
|
const [jwt, setJwt] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedToken = localStorage.getItem('jwt');
|
const storedToken = localStorage.getItem('jwt');
|
||||||
const storedCustomer = localStorage.getItem('user');
|
const storedCustomer = localStorage.getItem('user');
|
||||||
|
|
||||||
console.log('Stored customer: ', storedCustomer);
|
|
||||||
if (storedToken) {
|
if (storedToken) {
|
||||||
setJwt(storedToken);
|
setJwt(storedToken);
|
||||||
}
|
}
|
||||||
@@ -37,20 +38,16 @@ export const CustomerProvider = ({ children }: CustomerProviderProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (customer) {
|
if (customer) {
|
||||||
localStorage.setItem('user', JSON.stringify(customer));
|
localStorage.setItem('user', JSON.stringify(customer));
|
||||||
console.log('User set to: ', customer);
|
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
console.log('User removed');
|
|
||||||
}
|
}
|
||||||
}, [customer]);
|
}, [customer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
localStorage.setItem('jwt', jwt);
|
localStorage.setItem('jwt', jwt);
|
||||||
console.log('Jwt set to: ', jwt);
|
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('jwt');
|
localStorage.removeItem('jwt');
|
||||||
console.log('Jwt removed');
|
|
||||||
}
|
}
|
||||||
}, [jwt]);
|
}, [jwt]);
|
||||||
|
|
||||||
@@ -66,12 +63,13 @@ export const CustomerProvider = ({ children }: CustomerProviderProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// obtain JWT token
|
// obtain JWT token
|
||||||
await getJwt(email, password);
|
const token = await getJwt(email, password);
|
||||||
|
|
||||||
// get customer
|
// get customer
|
||||||
await getCustomer();
|
const customer = await getCustomer(token);
|
||||||
|
|
||||||
console.log('Logged in');
|
setJwt(token);
|
||||||
|
setCustomer(customer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
// Optionally, handle error (e.g., show error message to user)
|
// Optionally, handle error (e.g., show error message to user)
|
||||||
@@ -98,12 +96,19 @@ export const CustomerProvider = ({ children }: CustomerProviderProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// obtain JWT token
|
// obtain JWT token
|
||||||
await registerEmail(email, password);
|
let token = await registerEmail(email, password);
|
||||||
|
|
||||||
// get customer
|
// get customer
|
||||||
await createCustomer(firstName, lastName, email);
|
let customer = await createCustomer(firstName, lastName, email, token);
|
||||||
|
|
||||||
console.log('Logged in');
|
// obtain updated JWT token
|
||||||
|
token = await getJwt(email, password);
|
||||||
|
|
||||||
|
// get customer
|
||||||
|
customer = await getCustomer(token);
|
||||||
|
|
||||||
|
setJwt(token);
|
||||||
|
setCustomer(customer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
// Optionally, handle error (e.g., show error message to user)
|
// Optionally, handle error (e.g., show error message to user)
|
||||||
@@ -138,7 +143,7 @@ export const CustomerProvider = ({ children }: CustomerProviderProps) => {
|
|||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('Unable to retrieve token');
|
throw new Error('Unable to retrieve token');
|
||||||
}
|
}
|
||||||
setJwt(token);
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error in getJwt: ', error);
|
console.error('Login error in getJwt: ', error);
|
||||||
// Optionally, handle error (e.g., show error message to user)
|
// Optionally, handle error (e.g., show error message to user)
|
||||||
@@ -163,47 +168,41 @@ export const CustomerProvider = ({ children }: CustomerProviderProps) => {
|
|||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('Unable to retrieve token');
|
throw new Error('Unable to retrieve token');
|
||||||
}
|
}
|
||||||
setJwt(token);
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Register error in registerEmail: ', error);
|
console.error('Register error in registerEmail: ', error);
|
||||||
// Optionally, handle error (e.g., show error message to user)
|
// Optionally, handle error (e.g., show error message to user)
|
||||||
throw error; // Re-throw to allow caller to handle it
|
throw error; // Re-throw to allow caller to handle it
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const getCustomer = async () => {
|
const getCustomer = async (token: string) => {
|
||||||
try {
|
try {
|
||||||
if (!jwt) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// get customer
|
// get customer
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/customers/me`, {
|
const response = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/customers/me`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${jwt}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY as string,
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY as string,
|
||||||
},
|
},
|
||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
|
|
||||||
setCustomer(response.customer);
|
return response.customer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
// Optionally, handle error (e.g., show error message to user)
|
// Optionally, handle error (e.g., show error message to user)
|
||||||
throw error; // Re-throw to allow caller to handle it
|
throw error; // Re-throw to allow caller to handle it
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const createCustomer = async (firstName: string, lastName: string, email: string) => {
|
const createCustomer = async (firstName: string, lastName: string, email: string, token: string) => {
|
||||||
try {
|
try {
|
||||||
if (!jwt) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// create customer
|
// create customer
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/customers`, {
|
const response = await fetch(`${process.env.NEXT_PUBLIC_STORE_URL}/store/customers`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${jwt}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY as string,
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY as string,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -213,7 +212,7 @@ export const CustomerProvider = ({ children }: CustomerProviderProps) => {
|
|||||||
}),
|
}),
|
||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
|
|
||||||
setCustomer(response.customer);
|
return response.customer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error('Login error:', error);
|
||||||
// Optionally, handle error (e.g., show error message to user)
|
// Optionally, handle error (e.g., show error message to user)
|
||||||
@@ -224,9 +223,11 @@ export const CustomerProvider = ({ children }: CustomerProviderProps) => {
|
|||||||
<CustomerContext.Provider
|
<CustomerContext.Provider
|
||||||
value={{
|
value={{
|
||||||
customer,
|
customer,
|
||||||
|
setCustomer,
|
||||||
register,
|
register,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
token: jwt,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in new issue
Block a user