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

feat: [P4PU-886] implement notifications and enhance cart functionality with item count badge #250

Merged
merged 3 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/components/Cart/CartDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import CloseIcon from '@mui/icons-material/Close';
import { Alert, Divider, useTheme } from '@mui/material';
import { Alert, Divider, useTheme, Link } from '@mui/material';
import { toggleCartDrawer } from 'store/CartStore';
import { ButtonNaked } from '@pagopa/mui-italia/dist/components/ButtonNaked';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { ArcRoutes } from 'routes/routes';
import { cartDrawerStyles } from './CartDrawer.styles';
Expand Down Expand Up @@ -78,7 +78,19 @@ export const CartDrawer = () => {
{/* Cart Content */}
{cart.items.length > 0 && (
<Stack sx={styles.items}>
<Alert severity="info">{t('app.cart.items.alert')}</Alert>
<Alert severity="info">
<Trans
i18nKey="app.cart.items.alert"
components={{
link1: (
<Link
target="_blank"
href="https://assistenza.ioapp.it/hc/it/articles/31008000237585-L-importo-%C3%A8-diverso-da-quello-previsto"
/>
)
}}
/>
</Alert>
<Stack mt={2} divider={<Divider orientation="horizontal" flexItem />}>
{cart.items.map((item) => (
<CartItem
Expand Down
11 changes: 10 additions & 1 deletion src/components/Header/SubHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { toggleCartDrawer } from 'store/CartStore';
import { useTranslation } from 'react-i18next';
import utils from 'utils'; // Adjust the import path as necessary
import { useStore } from 'store/GlobalStore';
import { Badge } from '@mui/material';

export const SubHeader = () => {
const { spacing } = useTheme();
Expand All @@ -28,7 +29,15 @@ export const SubHeader = () => {
<Typography variant="inherit" aria-hidden="true" id="header-cart-amount">
{utils.converters.toEuro(state.cart.amount)}
</Typography>
<ShoppingCartIcon fontSize="small" aria-hidden="true" />
<Badge
badgeContent={state.cart.items.length}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
color="primary">
<ShoppingCartIcon fontSize="small" aria-hidden="true" />
</Badge>
</Button>
);
};
Expand Down
11 changes: 10 additions & 1 deletion src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Container, Grid } from '@mui/material';
import { Alert, Container, Grid, Snackbar } from '@mui/material';
import { grey } from '@mui/material/colors';
import { Footer } from './Footer';
import { Sidebar } from './Sidebar/Sidebar';
Expand Down Expand Up @@ -41,6 +41,15 @@ export function Layout() {

return (
<>
<Snackbar
autoHideDuration={6000}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
onClose={utils.notify.dismiss}
open={utils.notify.status.isVisible.value}>
<Alert severity={utils.notify.status.payload.value?.severity} variant="outlined">
{utils.notify.status.payload.value?.text}
</Alert>
</Snackbar>
<ModalSystem />
<Container
maxWidth={false}
Expand Down
9 changes: 6 additions & 3 deletions src/components/PaymentNotice/Detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from 'models/PaymentNotice';
import { addItem, deleteItem, toggleCartDrawer, isItemInCart } from 'store/CartStore';
import { useStore } from 'store/GlobalStore';
import notify from 'utils/notify';
/**
* This component is considered private and should not be used directly.
* Instead, use `PaymentNotice.Card` for rendering the payment notice card.
Expand All @@ -42,8 +43,7 @@ export const _Detail = ({ paymentNotice }: { paymentNotice: PaymentNoticeDetails
// this is not a problem, because we not manage multiple payment options in any case
const paymentNoticeSigleOption = paymentNotice.paymentOptions as PaymentOptionsDetailsType;
const { iuv, amountValue: amount, nav, description } = paymentNoticeSigleOption;
// add a notification if the cart is full
if (cart.items.length >= 5) return;
if (cart.items.length >= 5) return notify.emit(t('app.cart.items.full'), 'error');
if (isItemInCart(iuv)) return deleteItem(iuv);
addItem({
amount,
Expand Down Expand Up @@ -293,7 +293,7 @@ export const _Detail = ({ paymentNotice }: { paymentNotice: PaymentNoticeDetails
</Typography>
</Grid>
</Grid>
<Grid container>
<Grid container justifyContent={'center'}>
<Grid item xs={12}>
<Button
id="payment-notice-add-button"
Expand All @@ -320,6 +320,9 @@ export const _Detail = ({ paymentNotice }: { paymentNotice: PaymentNoticeDetails
</Typography>
</Button>
</Grid>
<Typography variant="body1" fontSize={16} mt={2}>
{t('app.cart.items.info')}
</Typography>
</Grid>
</Stack>
</CardActions>
Expand Down
34 changes: 6 additions & 28 deletions src/components/Transactions/TransactionDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,23 @@
import { Download } from '@mui/icons-material';
import {
Alert,
Box,
Button,
Divider,
Grid,
Snackbar,
Stack,
Typography,
useTheme
} from '@mui/material';
import { Alert, Box, Button, Divider, Grid, Stack, Typography, useTheme } from '@mui/material';
import { CopyToClipboardButton } from '@pagopa/mui-italia';
import BRAND from './Brand';
import { BRANDS } from '../../models/NoticeDetail';
import paypal from '../../assets/paypal.png';
import { type NoticeDetail as NoticeDetailType } from '../../models/NoticeDetail';
import React, { useState } from 'react';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { downloadReceiptPDF } from 'utils/files';
import utils from 'utils';

export default function TransactionDetail({ noticeData }: { noticeData: NoticeDetailType }) {
const theme = useTheme();
const { t } = useTranslation();
const [toastOpen, setToastOpen] = useState(false);

const getReceipt = async (transactionId: string) => {
try {
await downloadReceiptPDF(transactionId);
} catch (err) {
setToastOpen(true);
await utils.files.downloadReceiptPDF(transactionId);
} catch {
utils.notify.emit(t('app.transactionDetail.downloadReceiptError'));
}
};

Expand Down Expand Up @@ -74,17 +63,6 @@ export default function TransactionDetail({ noticeData }: { noticeData: NoticeDe
</Alert>
)}
<Stack spacing={2} mt={3} width={'100%'}>
<Snackbar
autoHideDuration={6000}
onClose={() => {
setToastOpen(false);
}}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
open={toastOpen}>
<Alert severity="error" variant="outlined">
{t('app.transactionDetail.downloadReceiptError')}
</Alert>
</Snackbar>
<Grid container>
{/* LEFT COLUMN: PAID NOTICE INFO */}
<Grid container item xs={12} md={7}>
Expand Down
11 changes: 5 additions & 6 deletions src/components/Transactions/transactionDetail.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import * as React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import { dummyTransactionsData } from 'stories/utils/mocks';
import { TransactionDetails } from './';
import '@testing-library/jest-dom';
import { downloadReceiptPDF } from 'utils/files';
import utils from 'utils';
import { i18nTestSetup } from '__tests__/i18nTestSetup';

i18nTestSetup({});

vi.mock('utils/files');

const mockUseReceiptData = vi.mocked(downloadReceiptPDF);
const mockUseReceiptData = vi.mocked(utils.files.downloadReceiptPDF);

describe('TransactionDetails component', () => {
it('should render as expected', () => {
Expand All @@ -22,11 +22,10 @@ describe('TransactionDetails component', () => {
mockUseReceiptData.mockImplementation(() => {
throw new Error();
});
const notifySpy = vi.spyOn(utils.notify, 'emit');
render(<TransactionDetails noticeData={dummyTransactionsData.transactionData} />);
fireEvent.click(screen.getByTestId('receipt-download-btn'));
await waitFor(() =>
expect(screen.queryByText('app.transactionDetail.downloadReceiptError')).toBeInTheDocument()
);
expect(notifySpy).toHaveBeenCalledWith('app.transactionDetail.downloadReceiptError');
});

it('should truncate transactionId if longer than 20 ', () => {
Expand Down
6 changes: 4 additions & 2 deletions src/translations/it/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
"button": "Torna agli avvisi"
},
"items": {
"alert": "L’importo è aggiornato in automatico, così paghi sempre quanto dovuto ed eviti more o altri interessi.",
"alert": "L’importo si aggiorna in automatico, così paghi sempre quanto dovuto ed eviti more o altri interessi. <link1>Vuoi saperne di più?</link1>",
"back": "Torna agli avvisi",
"pay": "Vai al pagamento"
"pay": "Vai al pagamento",
"full": "Non puoi aggiungere più di 5 avvisi al carrello",
"info": "Puoi pagare fino a 5 avvisi insieme"
}
},
"assistance": {
Expand Down
5 changes: 2 additions & 3 deletions src/utils/files.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import '@testing-library/jest-dom';
import { downloadReceiptPDF } from './files';
import utils from 'utils';

describe('downloadReceiptPDF function', () => {
Expand All @@ -11,11 +10,11 @@ describe('downloadReceiptPDF function', () => {

URL.createObjectURL = vitest.fn();
URL.revokeObjectURL = vitest.fn();
expect(downloadReceiptPDF('1')).resolves.toBeUndefined();
expect(utils.files.downloadReceiptPDF('1')).resolves.toBeUndefined();
});

it('should trhow an Error when something goes wrong', () => {
vi.spyOn(utils.loaders, 'getReceiptPDF').mockResolvedValue(null);
expect(downloadReceiptPDF('1')).rejects.toThrowError('Error getting the PDF');
expect(utils.files.downloadReceiptPDF('1')).rejects.toThrowError('Error getting the PDF');
});
});
6 changes: 5 additions & 1 deletion src/utils/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import utils from 'utils';
/**
* Downloads pdf for a transaction
*/
export const downloadReceiptPDF = async (transactionId: string) => {
const downloadReceiptPDF = async (transactionId: string) => {
const response = await utils.loaders.getReceiptPDF(transactionId);
if (!response) {
throw new Error('Error getting the PDF');
Expand All @@ -28,3 +28,7 @@ export const downloadReceiptPDF = async (transactionId: string) => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
};

export default {
downloadReceiptPDF
};
4 changes: 4 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import converters from './converters';
import { datetools } from './datetools';
import loaders from './loaders';
import modal from './modal';
import notify from './notify';
import sidemenu from './sidemenu';
import storage from './storage';
import style from './style';
import files from './files';

export default {
apiClient: new Api({ baseURL: config.baseURL, timeout: config.apiTimeout }),
Expand All @@ -21,6 +23,8 @@ export default {
datetools,
loaders,
modal,
notify,
files,
sidemenu,
storage,
style,
Expand Down
31 changes: 31 additions & 0 deletions src/utils/notify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { signal } from '@preact/signals-react';
import { AlertProps } from '@mui/material';

/** emits a notification */
const emit = (text: string, severity: AlertProps['severity'] = 'error') => {
isVisible.value = true;
payload.value.text = text;
payload.value.severity = severity;
};

/** dismiss a notification */
const dismiss = () => {
isVisible.value = false;
};

interface notificationPayload {
text?: string;
severity?: AlertProps['severity'];
}

const isVisible = signal<boolean>(false);
const payload = signal<notificationPayload>({});

export default {
emit,
dismiss,
status: {
isVisible,
payload
}
};
Loading