Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Job Launcher] Billing system #2485

Merged
merged 13 commits into from
Nov 28, 2024
21 changes: 19 additions & 2 deletions packages/apps/job-launcher/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { ProtectedRoute } from './components/ProtectedRoute';
import Layout from './layouts';
import Dashboard from './pages/Dashboard';
Expand All @@ -8,7 +7,9 @@ import Home from './pages/Home';
import CreateJob from './pages/Job/CreateJob';
import JobDetail from './pages/Job/JobDetail';
import JobList from './pages/Job/JobList';
import Settings from './pages/Profile/Settings';
import TopUpAccount from './pages/Profile/TopUpAccount';
import Transactions from './pages/Profile/Transactions';
import ResetPassword from './pages/ResetPassword';
import ValidateEmail from './pages/ValidateEmail';
import VerifyEmail from './pages/VerifyEmail';
Expand Down Expand Up @@ -65,6 +66,22 @@ export default function App() {
</ProtectedRoute>
}
/>
<Route
path="/profile/transactions"
element={
<ProtectedRoute>
<Transactions />
</ProtectedRoute>
}
/>
<Route
path="/profile/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" />} />
</Route>
</Routes>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import CloseIcon from '@mui/icons-material/Close';
import { LoadingButton } from '@mui/lab';
import {
Box,
Dialog,
IconButton,
MenuItem,
TextField,
Typography,
useTheme,
} from '@mui/material';
import { useEffect, useState } from 'react';
import { countryOptions, vatTypeOptions } from '../../constants/payment';
import { useSnackbar } from '../../providers/SnackProvider';
import { editUserBillingInfo } from '../../services/payment';
import { BillingInfo } from '../../types';

const BillingDetailsModal = ({
open,
onClose,
billingInfo,
setBillingInfo,
}: {
open: boolean;
onClose: () => void;
billingInfo: BillingInfo;
setBillingInfo: (value: BillingInfo) => void;
}) => {
const theme = useTheme();
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState<BillingInfo>(billingInfo);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const { showError } = useSnackbar();

useEffect(() => {
if (billingInfo) {
setFormData(billingInfo);
}
}, [billingInfo]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;

if (['city', 'country', 'line', 'postalCode'].includes(name)) {
setFormData((prevFormData) => ({
...prevFormData,
address: {
...prevFormData.address,
[name]: value,
},
}));
} else {
setFormData({
...formData,
[name]: value,
});
}
};

const validateForm = () => {
let newErrors: { [key: string]: string } = {};

if (!formData.name) {
newErrors.name = 'name required';
}

const addressFields = ['line', 'postalCode', 'city', 'country'];
addressFields.forEach((field) => {
if (!formData.address[field as keyof typeof formData.address]) {
newErrors[field] = `${field} required`;
}
});

if (!formData.vat) {
newErrors.vat = 'Tax ID required';
}
if (!formData.vatType) {
newErrors.vatType = 'Tax ID type required';
}

setErrors(newErrors);

return Object.keys(newErrors).length === 0;
};

const handleSubmit = async () => {
if (validateForm()) {
setIsLoading(true);
try {
delete formData.email;
await editUserBillingInfo(formData);
setBillingInfo(formData);
} catch (err: any) {
showError(
err.message || 'An error occurred while saving billing details.',
);
}
setIsLoading(false);
onClose();
}
};

return (
<Dialog
open={open}
onClose={onClose}
maxWidth={false}
PaperProps={{ sx: { mx: 2, maxWidth: 'calc(100% - 32px)' } }}
>
<Box display="flex" maxWidth="950px">
<Box
width={{ xs: '0', md: '40%' }}
display={{ xs: 'none', md: 'flex' }}
sx={{
background: theme.palette.primary.main,
boxSizing: 'border-box',
flexDirection: 'column',
justifyContent: 'space-between',
}}
px={9}
py={6}
>
<Typography variant="h4" fontWeight={600} color="#fff">
{billingInfo ? 'Edit Billing Details' : 'Add Billing Details'}
</Typography>
</Box>
<Box
sx={{ boxSizing: 'border-box' }}
width={{ xs: '100%', md: '60%' }}
minWidth={{ xs: '340px', sm: '392px' }}
display="flex"
flexDirection="column"
p={{ xs: 2, sm: 4 }}
>
<IconButton sx={{ ml: 'auto' }} onClick={onClose}>
<CloseIcon color="primary" />
</IconButton>

<Box width="100%" display="flex" flexDirection="column" gap={3}>
<Typography variant="h6">Details</Typography>
<TextField
label="Name"
name="name"
value={formData.name}
onChange={handleInputChange}
fullWidth
error={!!errors.name}
helperText={errors.name}
/>
<TextField
label="Address Line"
name="line"
value={formData.address.line}
onChange={handleInputChange}
fullWidth
error={!!errors.line}
helperText={errors.line || ''}
/>
<TextField
label="Postal Code"
name="postalCode"
value={formData.address.postalCode}
onChange={handleInputChange}
fullWidth
error={!!errors.postalCode}
helperText={errors.postalCode || ''}
/>
<TextField
label="City"
name="city"
value={formData.address.city}
onChange={handleInputChange}
fullWidth
error={!!errors.city}
helperText={errors.city || ''}
/>
<TextField
select
label="Country"
name="country"
value={formData.address.country}
onChange={handleInputChange}
fullWidth
error={!!errors.country}
helperText={errors.country || ''}
>
{Object.entries(countryOptions).map(([key, label]) => (
<MenuItem key={key} value={key}>
{label}
</MenuItem>
))}
</TextField>

{/* VAT Section */}
<Box display="flex" gap={2}>
<TextField
select
label="Tax ID Type"
name="vatType"
value={formData.vatType || ''}
onChange={handleInputChange}
fullWidth
error={!!errors.vatType}
helperText={errors.vatType || ''}
>
{Object.entries(vatTypeOptions).map(([key, label]) => (
<MenuItem key={key} value={key}>
{label}
</MenuItem>
))}
</TextField>

<TextField
label="Tax ID Number"
name="vat"
value={formData.vat || ''}
onChange={handleInputChange}
fullWidth
error={!!errors.vat}
helperText={errors.vat || ''}
/>
</Box>
<LoadingButton
color="primary"
variant="contained"
fullWidth
size="large"
onClick={handleSubmit}
loading={isLoading}
>
{billingInfo ? 'Save Changes' : 'Add Billing Details'}
</LoadingButton>
</Box>
</Box>
</Box>
</Dialog>
);
};

export default BillingDetailsModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import CloseIcon from '@mui/icons-material/Close';
import { Box, Dialog, IconButton, Typography, useTheme } from '@mui/material';
import { CardSetupForm } from './CardSetupForm';

const AddCardModal = ({
open,
onClose,
onComplete,
}: {
open: boolean;
onClose: () => void;
onComplete: () => void;
}) => {
const theme = useTheme();

const handleCardAdded = () => {
onComplete();
onClose();
};

return (
<Dialog
open={open}
onClose={onClose}
maxWidth={false}
PaperProps={{ sx: { mx: 2, maxWidth: 'calc(100% - 32px)' } }}
>
<Box display="flex" maxWidth="950px">
<Box
width={{ xs: '0', md: '40%' }}
display={{ xs: 'none', md: 'flex' }}
sx={{
background: theme.palette.primary.main,
boxSizing: 'border-box',
flexDirection: 'column',
justifyContent: 'space-between',
}}
px={9}
py={6}
>
<Typography variant="h4" fontWeight={600} color="#fff">
Add Credit Card Details
</Typography>
<Typography color="text.secondary" variant="caption">
We need you to add a credit card in order to comply with HUMAN’s
Abuse Mechanism. Learn more about it here.
<br />
<br />
This card will be used for funding the jobs requested.
</Typography>
</Box>
<Box
sx={{ boxSizing: 'border-box' }}
width={{ xs: '100%', md: '60%' }}
minWidth={{ xs: '340px', sm: '392px' }}
display="flex"
flexDirection="column"
p={{ xs: 2, sm: 4 }}
>
<IconButton sx={{ ml: 'auto' }} onClick={onClose}>
<CloseIcon color="primary" />
</IconButton>
<Box width="100%" display="flex" flexDirection="column" gap={3}>
<CardSetupForm onComplete={handleCardAdded} />
</Box>
</Box>
</Box>
</Dialog>
);
};

export default AddCardModal;
Loading