Ritz Token Initialized
This commit is contained in:
@@ -14,6 +14,8 @@ import LoginScreen from './pages/LoginScreen';
|
||||
import ProfileScreen from './pages/ProfileScreen';
|
||||
import ChangePinScreen from './pages/ChangePinScreen';
|
||||
import StallDetailScreen from './pages/StallDetailScreen';
|
||||
import WalletScreen from './pages/WalletScreen';
|
||||
import TopUpScreen from './pages/TopUpScreen';
|
||||
import StockAlert from './components/StockAlert';
|
||||
import './App.css';
|
||||
|
||||
@@ -78,6 +80,16 @@ function App() {
|
||||
<ChangePinScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/wallet" element={
|
||||
<ProtectedRoute>
|
||||
<WalletScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/topup" element={
|
||||
<ProtectedRoute>
|
||||
<TopUpScreen />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</CartProvider>
|
||||
|
||||
@@ -122,7 +122,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
id: updatedUserDto.id,
|
||||
name: updatedUserDto.name,
|
||||
mobileNumber: updatedUserDto.mobileNumber,
|
||||
isLoggedIn: updatedUserDto.loggedIn
|
||||
isLoggedIn: updatedUserDto.loggedIn,
|
||||
ritzTokenBalance: updatedUserDto.ritzTokenBalance
|
||||
};
|
||||
setUser(updatedUser);
|
||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||
|
||||
@@ -28,8 +28,139 @@
|
||||
}
|
||||
|
||||
.address-card.selected, .payment-card.selected {
|
||||
border-color: var(--primary);
|
||||
background-color: var(--primary-light);
|
||||
border-color: #6366f1;
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.ritz-payment-card {
|
||||
padding: 1.2rem;
|
||||
border-radius: 20px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ritz-payment-card.insufficient {
|
||||
opacity: 0.8;
|
||||
border-style: dashed;
|
||||
background: rgba(239, 68, 68, 0.02);
|
||||
}
|
||||
|
||||
.ritz-icon {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.payment-name-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.wallet-balance-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.balance-val {
|
||||
font-weight: 700;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.add-tokens-checkout-btn {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
background: white;
|
||||
border: 1px dashed #6366f1;
|
||||
padding: 1rem;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #6366f1;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-tokens-checkout-btn:active {
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.add-tokens-checkout-btn .btn-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.payment-security-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ritz-text {
|
||||
color: #6366f1 !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.insufficient-warning {
|
||||
margin: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 149, 0, 0.1);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: #cc7700;
|
||||
}
|
||||
|
||||
.warning-text h3 {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.warning-text p {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.ritz-order-btn {
|
||||
background: #6366f1 !important;
|
||||
}
|
||||
|
||||
.ritz-order-btn:disabled {
|
||||
background: var(--bg-secondary) !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.loading-spinner-small {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.address-icon, .payment-icon {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Smartphone } from 'lucide-react';
|
||||
import {
|
||||
CircleDollarSign,
|
||||
Wallet,
|
||||
ChevronRight,
|
||||
AlertCircle,
|
||||
ShieldCheck,
|
||||
PlusCircle
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import Header from '../components/Header';
|
||||
import StockConflictModal from '../components/StockConflictModal';
|
||||
@@ -9,34 +16,52 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { useFood } from '../contexts/FoodContext';
|
||||
import './CheckoutScreen.css';
|
||||
|
||||
const UPI_APPS = [
|
||||
{ id: 'gpay', name: 'Google Pay', icon: 'https://cdn.iconscout.com/icon/free/png-256/free-google-pay-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-vol-3-pack-logos-icons-2944849.png' },
|
||||
{ id: 'phonepe', name: 'PhonePe', icon: 'https://cdn.iconscout.com/icon/free/png-256/free-phonepe-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-vol-5-pack-logos-icons-2945037.png' },
|
||||
{ id: 'paytm', name: 'Paytm', icon: 'https://cdn.iconscout.com/icon/free/png-256/free-paytm-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-vol-5-pack-logos-icons-2945031.png' },
|
||||
{ id: 'bhim', name: 'BHIM UPI', icon: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-6R-pUu-Wj4V_vO8t-qXz9-7vjJ7XvK36A&s' },
|
||||
{ id: 'other', name: 'Others', icon: null }
|
||||
];
|
||||
|
||||
const CheckoutScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { cart, totalPrice, clearCart, removeFromCart, updateQuantity } = useCart();
|
||||
const { user } = useAuth();
|
||||
const { refreshData } = useFood();
|
||||
const [selectedApp, setSelectedApp] = useState('gpay');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [currentBalance, setCurrentBalance] = useState<number>(user?.ritzTokenBalance || 0);
|
||||
const [isLoadingBalance, setIsLoadingBalance] = useState(true);
|
||||
|
||||
// Conflict state
|
||||
const [stockConflicts, setStockConflicts] = useState<any[]>([]);
|
||||
const [showConflictModal, setShowConflictModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBalance();
|
||||
}, [user]);
|
||||
|
||||
const fetchBalance = async () => {
|
||||
if (!user) return;
|
||||
try {
|
||||
setIsLoadingBalance(true);
|
||||
const response = await fetch(`http://${window.location.hostname}:8080/api/wallet/balance/${user.id}`);
|
||||
const data = await response.json();
|
||||
setCurrentBalance(data.balance || 0);
|
||||
} catch (error) {
|
||||
console.error('Error fetching balance:', error);
|
||||
} finally {
|
||||
setIsLoadingBalance(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isInsufficient = currentBalance < totalPrice;
|
||||
|
||||
const handlePlaceOrder = async () => {
|
||||
if (!user) return;
|
||||
if (isInsufficient) {
|
||||
alert('Insufficient Ritz Tokens. Please top up your wallet.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
const orderData = {
|
||||
userId: user.id,
|
||||
totalAmount: totalPrice,
|
||||
paymentMethod: UPI_APPS.find(a => a.id === selectedApp)?.name || 'UPI',
|
||||
paymentMethod: 'RITZ_TOKEN',
|
||||
orderType: 'MY_ORDER',
|
||||
items: cart.map(item => ({
|
||||
productId: Number(item.id),
|
||||
@@ -45,7 +70,8 @@ const CheckoutScreen: React.FC = () => {
|
||||
quantity: item.quantity,
|
||||
stallId: item.stallId ? Number(item.stallId) : null,
|
||||
stallName: item.stallName || null
|
||||
}))
|
||||
})),
|
||||
user: { id: user.id } // Backend needs user object for token deduction
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -66,10 +92,12 @@ const CheckoutScreen: React.FC = () => {
|
||||
}
|
||||
});
|
||||
} else if (data.errorType === 'STOCK_ERROR') {
|
||||
console.error('Final Step Stock Conflict:', data.conflicts);
|
||||
setStockConflicts(data.conflicts || []);
|
||||
setShowConflictModal(true);
|
||||
await refreshData(true); // Sync background stock
|
||||
await refreshData(true);
|
||||
} else if (data.errorType === 'TOKEN_ERROR') {
|
||||
alert(data.message || 'Insufficient Tokens');
|
||||
fetchBalance(); // Sync balance
|
||||
} else {
|
||||
alert(data.message || 'Failed to place order');
|
||||
}
|
||||
@@ -106,56 +134,89 @@ const CheckoutScreen: React.FC = () => {
|
||||
<main className="safe-area-bottom">
|
||||
<section className="checkout-section">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">Pay using UPI</h2>
|
||||
<p className="section-subtitle">Select your preferred UPI app</p>
|
||||
<h2 className="section-title">Payment Method</h2>
|
||||
<p className="section-subtitle">Food orders are paid using Ritz Tokens</p>
|
||||
</div>
|
||||
|
||||
<div className="payment-options">
|
||||
{UPI_APPS.map((app) => (
|
||||
<div
|
||||
key={app.id}
|
||||
className={`payment-card ${selectedApp === app.id ? 'selected' : ''}`}
|
||||
onClick={() => setSelectedApp(app.id)}
|
||||
>
|
||||
<div className="payment-icon">
|
||||
{app.icon ? (
|
||||
<img src={app.icon} alt={app.name} className="upi-app-icon" />
|
||||
) : (
|
||||
<div className="upi-placeholder">
|
||||
<Smartphone size={20} />
|
||||
</div>
|
||||
<div className={`payment-card ritz-payment-card ${isInsufficient ? 'insufficient' : 'selected'}`}>
|
||||
<div className="payment-icon ritz-icon">
|
||||
<CircleDollarSign size={24} />
|
||||
</div>
|
||||
<div className="payment-info">
|
||||
<div className="payment-name-row">
|
||||
<span className="payment-name">Pay with Ritz Tokens</span>
|
||||
{isInsufficient && (
|
||||
<span className="status-badge error">Insufficient Balance</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="payment-info">
|
||||
<span className="payment-name">{app.name}</span>
|
||||
<span className="payment-sub">
|
||||
{app.id === 'other' ? 'Pay via any installed UPI app' : `Fast & secure payments via ${app.name}`}
|
||||
</span>
|
||||
<div className="wallet-balance-info">
|
||||
<Wallet size={14} />
|
||||
<span>Current Balance: </span>
|
||||
<span className="balance-val">R{currentBalance.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
{!isInsufficient && (
|
||||
<div className="selection-radio">
|
||||
<div className="radio-inner" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isInsufficient && (
|
||||
<motion.button
|
||||
className="add-tokens-checkout-btn"
|
||||
onClick={() => navigate('/topup')}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<div className="btn-content">
|
||||
<PlusCircle size={20} />
|
||||
<span>Top up Ritz Tokens</span>
|
||||
</div>
|
||||
<ChevronRight size={18} />
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="payment-security-note">
|
||||
<ShieldCheck size={14} />
|
||||
<span>Secured by Ritz Token Protocol. 1 Token = ₹1.00</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="order-summary-mini">
|
||||
<div className="summary-row">
|
||||
<span>Amount to pay</span>
|
||||
<span className="summary-price">₹{(totalPrice + 0).toFixed(2)}</span>
|
||||
<span>Tokens to be deducted</span>
|
||||
<span className="summary-price ritz-text">R{totalPrice.toLocaleString()}</span>
|
||||
</div>
|
||||
<p className="tax-info">Inclusive of all taxes and charges</p>
|
||||
<p className="tax-info">Exclusive of any platform bonuses</p>
|
||||
</div>
|
||||
|
||||
{isInsufficient && (
|
||||
<div className="insufficient-warning">
|
||||
<AlertCircle size={20} />
|
||||
<div className="warning-text">
|
||||
<strong>Short by R{(totalPrice - currentBalance).toLocaleString()}</strong>
|
||||
<p>Add more tokens to complete your order.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<div className="checkout-footer">
|
||||
<button
|
||||
className="place-order-button"
|
||||
className="place-order-button ritz-order-btn"
|
||||
onClick={handlePlaceOrder}
|
||||
disabled={isProcessing}
|
||||
disabled={isProcessing || isInsufficient}
|
||||
>
|
||||
{isProcessing ? 'Processing...' : `Pay ₹${totalPrice.toFixed(2)} & Place Order`}
|
||||
{isProcessing ? (
|
||||
<div className="loading-spinner-small" />
|
||||
) : isInsufficient ? (
|
||||
'Insufficient Tokens'
|
||||
) : (
|
||||
`Pay R${totalPrice.toLocaleString()} & Place Order`
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
ChevronRight,
|
||||
Phone,
|
||||
ShieldCheck,
|
||||
User,
|
||||
X,
|
||||
User as UserIcon
|
||||
User as UserIcon,
|
||||
Wallet
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
@@ -77,6 +77,7 @@ const ProfileScreen: React.FC = () => {
|
||||
|
||||
const menuItems = [
|
||||
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
|
||||
{ icon: Wallet, label: 'My Wallet', sub: 'Ritz Tokens & History', path: '/wallet' },
|
||||
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
|
||||
{ icon: Settings, label: 'Preferences', sub: 'Notifications, Language', path: '#' },
|
||||
{ icon: HelpCircle, label: 'Help & Support', sub: 'FAQs, Contact Us', path: '#' }
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface User {
|
||||
mobileNumber: string;
|
||||
name: string;
|
||||
isLoggedIn: boolean;
|
||||
ritzTokenBalance: number;
|
||||
}
|
||||
|
||||
export interface Stall {
|
||||
|
||||
Reference in New Issue
Block a user