diff --git a/.all-contributorsrc b/.all-contributorsrc
index eab6c0a6a..8f241dcad 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -904,6 +904,24 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "nikolay-yankov",
+ "name": "Nikolay Yankov",
+ "avatar_url": "https://avatars.githubusercontent.com/u/36303598?v=4",
+ "profile": "https://github.com/nikolay-yankov",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "katina-anachkova",
+ "name": "Katina Anachkova",
+ "avatar_url": "https://avatars.githubusercontent.com/u/82702355?v=4",
+ "profile": "https://github.com/katina-anachkova",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 10,
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index b80f5a889..c4b830882 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -120,6 +120,7 @@ jobs:
name: playwright-report
path: ./frontend/e2e/test-results/
retention-days: 14
+ overwrite: true
- uses: actions/upload-artifact@v4
if: always()
diff --git a/README.md b/README.md
index 67bd26945..b49bfc908 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,9 @@ Watch releases of this repository to be notified about future updates:
## Contributors ✨
-[![All Contributors](https://img.shields.io/badge/all_contributors-84-orange.svg?style=flat-square)](#contributors-)
+
+[![All Contributors](https://img.shields.io/badge/all_contributors-86-orange.svg?style=flat-square)](#contributors-)
+
Please check [contributors guide](https://github.com/podkrepi-bg/frontend/blob/master/CONTRIBUTING.md) for:
@@ -226,6 +228,8 @@ Thanks goes to these wonderful people:
Martin Kovachki 💻 ⚠️ |
Viktor Stefanov 💻 |
velnachev 💻 |
+ Nikolay Yankov 💻 |
+ Katina Anachkova 💻 |
diff --git a/e2e/README.md b/e2e/README.md
index 0f3b4bde9..ba43be251 100644
--- a/e2e/README.md
+++ b/e2e/README.md
@@ -52,3 +52,24 @@ Options:
```shell
yarn test:e2e --headed --debug -x -g support
```
+
+### Tests with Authenticated user
+
+### Writing
+
+To auth a user we rely on the Storage where the session is stored. And the storage is filled in and accessed via a `test.extend` [fixture](https://playwright.dev/docs/auth#authenticate-with-api-request). It takes care to login and store the session and then use it in the tests. See the `e2e/utils/fixtures.ts` file
+
+This is the process for writing tests for auth user.
+
+- import the desired user `test` from the fixture
+
+ `import { expect, giverTest as test } from '../../../utils/fixtures'` for the `giver` user
+
+- write your e2e tests ...
+
+> [Examples] `e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts` and `e2e/tests/regression/campaign-application/campaign-application-admin.spec.ts`
+
+### Running
+
+- [Locally] run the 'docker compose -d keycloak pg-db', the api (`yarn dev` in the api repo folder), the app (`yarn dev` in the frontend repo folder),
+- in the `frontend/e2e` folder run `yarn e2e:tests --ui` to start the playwright visual testing tool
diff --git a/e2e/pages/web-pages/donation/donation.page.ts b/e2e/pages/web-pages/donation/donation.page.ts
index a5d58287d..df4a4e392 100644
--- a/e2e/pages/web-pages/donation/donation.page.ts
+++ b/e2e/pages/web-pages/donation/donation.page.ts
@@ -56,7 +56,7 @@ export class DonationPage extends CampaignsPage {
bgLocalizationValidation['informed-agree-with'] + ' ' + bgLocalizationValidation.gdpr
private readonly enPrivacyCheckboxText =
enLocalizationValidation['informed-agree-with'] + ' ' + enLocalizationValidation.gdpr
- private readonly bgStripeErrorNoBalanceText = 'Картата Ви не разполага с достатъчно средства.'
+ private readonly bgStripeErrorNoBalanceText = 'В картата ви няма достатъчно средства. Опитайте с друга.'
async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise {
await this.page.waitForTimeout(1000)
diff --git a/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts b/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts
index 1a916af60..942e7c5c8 100644
--- a/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts
+++ b/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts
@@ -1,8 +1,3 @@
-import {
- CampaignApplicationResponse,
- CampaignApplicationExisting,
- CampaignApplicationAdminResponse,
-} from '../../../../src/gql/campaign-applications'
import { Page } from 'playwright/test'
import { expect, giverTest as test } from '../../../utils/fixtures'
import { textLocalized } from '../../../utils/texts-localized'
@@ -169,6 +164,41 @@ test.describe('Campaign application giver', () => {
await expect(page.getByText(t.steps.application['campaign-end'].options.funds)).toBeVisible()
await expect(page.getByText('goal')).toBeVisible()
})
+
+ test('should see the edit campaign application and be able to delete a selected file ', async ({
+ page,
+ baseURL,
+ }) => {
+ // arrange
+ await setupMeAndCampaignTypes(page)
+ await setupCampaignApplicationForEdit(page)
+ await page.goto(`${baseURL}/campaigns/application/1234`)
+ const t = await textLocalized().campaign.bg()
+ await page.getByRole('button', { name: t.cta.next }).click()
+ await page.getByRole('button', { name: t.cta.next }).click()
+
+ // expect to see 2 files
+ await expect(page.getByText('1234.txt')).toBeVisible()
+ await expect(page.getByText('document.pdf')).toBeVisible()
+
+ // act
+ // hit the delete button ...
+ await page.locator('li').filter({ hasText: '1234.txt' }).getByLabel('delete').click()
+ const [editCamAppReq, fileDeleteReq] = await Promise.all([
+ // the edit request to edit the CamApp entity
+ page.waitForRequest((r) => r.method() === 'PATCH'),
+ // the delete request to remove one of the files
+ page.waitForRequest((r) => r.method() === 'DELETE'),
+ // ... and when submit
+ page.getByRole('button', { name: t.cta.submit }).click(),
+ ])
+
+ await expect(editCamAppReq.postDataJSON()).toBeDefined()
+
+ const fileDelRes = await fileDeleteReq.response()
+
+ await expect(fileDelRes?.json()).resolves.toEqual({ id: 'ok' })
+ })
})
function defaultCampaignApplication() {
@@ -222,9 +252,9 @@ async function setupMeAndCampaignTypes(page: Page) {
},
{
id: '34b501f0-b3c3-43d9-9be0-7f7258eeb247',
- name: 'Membership',
- slug: 'membership',
- description: 'Membership Campaigns',
+ name: 'Elderly',
+ slug: 'elderly',
+ description: 'Help elderly people',
parentId: null,
category: 'others',
},
@@ -232,3 +262,42 @@ async function setupMeAndCampaignTypes(page: Page) {
}),
)
}
+
+async function setupCampaignApplicationForEdit(
+ page: Page,
+ application: Partial> = {},
+) {
+ await page.route('*/**/api/v1/campaign-application/byId/*', (req) =>
+ req.fulfill({
+ json: {
+ ...defaultCampaignApplication(),
+ id: 'forEdit',
+ documents: [
+ { filename: '1234.txt', id: '1234' },
+ { filename: 'document.pdf', id: 'doc-id-123123' },
+ ],
+ ...application,
+ },
+ }),
+ )
+
+ // on submit at the end of edit this patch request needs to be sent
+ await page.route('*/**/api/v1/campaign-application/forEdit', (req) =>
+ req.fulfill({
+ json: {
+ ...defaultCampaignApplication(),
+ id: 'forEdit',
+ ...application,
+ },
+ }),
+ )
+
+ // delete file successful
+ await page.route('*/**/api/v1/campaign-application/fileById/*', (req) =>
+ req.fulfill({
+ json: {
+ id: 'ok',
+ },
+ }),
+ )
+}
diff --git a/e2e/utils/fixtures.ts b/e2e/utils/fixtures.ts
index ea13fef30..a51ad18a1 100644
--- a/e2e/utils/fixtures.ts
+++ b/e2e/utils/fixtures.ts
@@ -1,3 +1,7 @@
+/**
+ * This is logic for authenticating and storing the session to be used in the tests. See the e2e/Readme.md - the section about Authenticated user
+ */
+
import { test, test as base } from '@playwright/test'
import dotenv from 'dotenv'
import fs from 'fs'
diff --git a/e2e/yarn.lock b/e2e/yarn.lock
index d77e05353..626bd5bd9 100644
--- a/e2e/yarn.lock
+++ b/e2e/yarn.lock
@@ -235,13 +235,13 @@ __metadata:
linkType: hard
"cross-spawn@npm:^7.0.0":
- version: 7.0.3
- resolution: "cross-spawn@npm:7.0.3"
+ version: 7.0.6
+ resolution: "cross-spawn@npm:7.0.6"
dependencies:
path-key: ^3.1.0
shebang-command: ^2.0.0
which: ^2.0.1
- checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52
+ checksum: 8d306efacaf6f3f60e0224c287664093fa9185680b2d195852ba9a863f85d02dcc737094c6e512175f8ee0161f9b87c73c6826034c2422e39de7d6569cf4503b
languageName: node
linkType: hard
diff --git a/public/locales/bg/donation-flow.json b/public/locales/bg/donation-flow.json
index 8514038a1..e039106a0 100644
--- a/public/locales/bg/donation-flow.json
+++ b/public/locales/bg/donation-flow.json
@@ -102,7 +102,7 @@
},
"noregister": {
"label": "Продължете без регистрация ",
- "description": "Продължавайки без регистрация, нямате възможност да запазите дарението в историята на профила си както и да правите месечни дарения по избрана кампания"
+ "description": "Продължавайки без регистрация, нямате възможност да запазите дарението в историята на профила си както и да правите месечни дарения по избрана кампания."
},
"field": {
"password": "Парола",
diff --git a/public/locales/en/donation-flow.json b/public/locales/en/donation-flow.json
index b22973a45..2f6c0ca80 100644
--- a/public/locales/en/donation-flow.json
+++ b/public/locales/en/donation-flow.json
@@ -56,7 +56,7 @@
},
"card-data": {
"name-label": "Cardholder name",
- "error": {
+ "errors": {
"email": "Please enter your email",
"name": "Please enter your name"
}
@@ -106,7 +106,7 @@
},
"noregister": {
"label": "Continue without registration",
- "description": "You will not be able to get a donation certificate or a list of your donations. If you still want to receive a receipt, please share your email - it will not be visible in the platform"
+ "description": "You will not be able to get a donation certificate or a list of your donations. If you still want to receive a receipt, please share your email - it will not be visible in the platform."
},
"field": {
"password": "Password",
@@ -128,7 +128,7 @@
"donation": "Donation",
"transaction": {
"title": "Transaction",
- "description": "The transaction is only to compensate the transfer and is calculated based on your method of payment. \"Podkrepi.bg\" works with 0% commission"
+ "description": "The transaction is only to compensate the transfer and is calculated based on your method of payment. \"Podkrepi.bg\" works with 0% commission."
},
"total": "Total",
"field": {
diff --git a/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx b/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx
index 4d230a824..d6d3b3f78 100644
--- a/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx
+++ b/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx
@@ -115,7 +115,8 @@ export const useCampaignsList = () => {
const { data, isLoading } = fetchMutation()
return {
- list: data?.sort((a, b) => b?.updatedAt?.localeCompare(a?.updatedAt ?? '') ?? 0),
+ // the data array is strict mode (sometimes) it throws a Readonly array error on the sort so create a shallow copy
+ list: [...(data ?? [])].sort((a, b) => b?.updatedAt?.localeCompare(a?.updatedAt ?? '') ?? 0),
isLoading,
}
}
diff --git a/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx b/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx
index 864dcd8f9..807e746b2 100644
--- a/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx
+++ b/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx
@@ -58,7 +58,7 @@ export default function CampaignApplicationBasic() {
/>
-
+
{t('steps.details.title')}
-
-
-
+
+
+
{t('campaigns:campaign.type')}
- {data?.map((campaignType, index) => (
-
- ))}
+ {data
+ ?.filter((campaignType) => !hideSystemTypes || !systemTypes.includes(campaignType?.name))
+ ?.map((campaignType, index) => (
+
+ ))}
{helperText && {helperText}}
diff --git a/src/components/client/donation-flow/DonationFlowForm.tsx b/src/components/client/donation-flow/DonationFlowForm.tsx
index de6b63a4a..7ee93d06a 100644
--- a/src/components/client/donation-flow/DonationFlowForm.tsx
+++ b/src/components/client/donation-flow/DonationFlowForm.tsx
@@ -101,6 +101,18 @@ export const validationSchema: yup.SchemaOf = yup
export function DonationFlowForm() {
const formikRef = useRef | null>(null)
const { t } = useTranslation('donation-flow')
+ const { campaign, setupIntent, paymentError, setPaymentError, idempotencyKey } = useDonationFlow()
+ const stripe = useStripe()
+ const elements = useElements()
+ const router = useRouter()
+ const updateSetupIntentMutation = useUpdateSetupIntent()
+ const cancelSetupIntentMutation = useCancelSetupIntent()
+ const paymentMethodSectionRef = React.useRef(null)
+ const authenticationSectionRef = React.useRef(null)
+ const stripeChargeRef = React.useRef(idempotencyKey)
+ const [showCancelDialog, setShowCancelDialog] = React.useState(false)
+ const [submitPaymentLoading, setSubmitPaymentLoading] = React.useState(false)
+ const { data: { user: person } = { user: null } } = useCurrentPerson()
const { data: session } = useSession({
required: false,
onUnauthenticated: () => {
@@ -117,17 +129,6 @@ export function DonationFlowForm() {
formikRef.current?.setFieldValue('email', '')
formikRef.current?.setFieldValue('isAnonymous', true, false)
}, [session])
- const { campaign, setupIntent, paymentError, setPaymentError, idempotencyKey } = useDonationFlow()
- const stripe = useStripe()
- const elements = useElements()
- const router = useRouter()
- const updateSetupIntentMutation = useUpdateSetupIntent()
- const cancelSetupIntentMutation = useCancelSetupIntent()
- const paymentMethodSectionRef = React.useRef(null)
- const authenticationSectionRef = React.useRef(null)
- const [showCancelDialog, setShowCancelDialog] = React.useState(false)
- const [submitPaymentLoading, setSubmitPaymentLoading] = React.useState(false)
- const { data: { user: person } = { user: null } } = useCurrentPerson()
return (
{
- cancelSetupIntentMutation.mutate({ id: setupIntent.id })
- router.push(routes.campaigns.viewCampaignBySlug(campaign.slug))
+ setShowCancelDialog(false)
}}
title={t('cancel-dialog.title')}
content={t('cancel-dialog.content')}
confirmButtonLabel={t('cancel-dialog.btn-continue')}
cancelButtonLabel={t('cancel-dialog.btn-cancel')}
handleConfirm={() => {
- setShowCancelDialog(false)
+ cancelSetupIntentMutation.mutate({ id: setupIntent.id })
+ router.push(routes.campaigns.viewCampaignBySlug(campaign.slug))
}}
/>
-
+
diff --git a/src/components/client/donation-flow/alerts/AlertsColumn.tsx b/src/components/client/donation-flow/alerts/AlertsColumn.tsx
index 8bfba674a..335772d81 100644
--- a/src/components/client/donation-flow/alerts/AlertsColumn.tsx
+++ b/src/components/client/donation-flow/alerts/AlertsColumn.tsx
@@ -60,6 +60,7 @@ function AlertsColumn({
<>
{updatedRefArray.map((ref, index) => {
const alert = alerts[ref.current?.id as keyof typeof alerts]
+ if (!alert) return null
return
})}
>
diff --git a/src/components/client/donation-flow/contexts/DonationFlowProvider.tsx b/src/components/client/donation-flow/contexts/DonationFlowProvider.tsx
index c8676bb1d..c1a8167be 100644
--- a/src/components/client/donation-flow/contexts/DonationFlowProvider.tsx
+++ b/src/components/client/donation-flow/contexts/DonationFlowProvider.tsx
@@ -10,7 +10,7 @@ type DonationContext = {
paymentError: StripeError | null
setPaymentError: React.Dispatch>
campaign: CampaignResponse
- stripe: StripeType | null
+ stripe: Promise
idempotencyKey: string
}
diff --git a/src/components/client/donation-flow/steps/Amount.tsx b/src/components/client/donation-flow/steps/Amount.tsx
index f5280e756..986862668 100644
--- a/src/components/client/donation-flow/steps/Amount.tsx
+++ b/src/components/client/donation-flow/steps/Amount.tsx
@@ -27,10 +27,10 @@ export const initialAmountFormValues = {
export const amountValidation = {
amountChosen: yup.string().when('payment', {
is: 'card',
- then: yup.string().required(),
+ then: yup.string().optional(),
}),
finalAmount: yup.number().when('payment', {
- is: 'card',
+ is: (payment: string | null) => ['card', null].includes(payment),
then: () =>
yup.number().min(1, 'donation-flow:step.amount.field.final-amount.error').required(),
}),
@@ -70,6 +70,9 @@ export default function Amount({ disabled, sectionRef, error }: SelectDonationAm
? toMoney(Number(formik.values.otherAmount))
: Number(formik.values.amountChosen)
+ // Do not perform calculations if amount is not set
+ if (amountChosen === 0) return
+
if (formik.values.cardIncludeFees) {
formik.setFieldValue('amountWithoutFees', amountChosen)
formik.setFieldValue(
diff --git a/src/components/client/donation-flow/steps/authentication/InlineRegisterForm.tsx b/src/components/client/donation-flow/steps/authentication/InlineRegisterForm.tsx
index 098637d80..3da6606db 100644
--- a/src/components/client/donation-flow/steps/authentication/InlineRegisterForm.tsx
+++ b/src/components/client/donation-flow/steps/authentication/InlineRegisterForm.tsx
@@ -83,7 +83,6 @@ export default function InlineRegisterForm() {
try {
setLoading(true)
// Register in Keycloak
-
if (values.terms && values.gdpr && values.password === values.confirmPassword) {
await register(values)
} else if (!values.terms) {
@@ -143,7 +142,7 @@ export default function InlineRegisterForm() {
@@ -155,21 +154,17 @@ export default function InlineRegisterForm() {
)}
-
+
{!formik.values.registerTerms && formik.touched.registerTerms && (
{t('validation:terms-of-use')}
)}
-
+
{!formik.values.registerGdpr && formik.touched.registerGdpr && (
{t('validation:terms-of-service')}
)}
-
-
-
-
)
diff --git a/src/components/common/SocialShareListButton.tsx b/src/components/common/SocialShareListButton.tsx
index ffe3bc4ab..b8fc77091 100644
--- a/src/components/common/SocialShareListButton.tsx
+++ b/src/components/common/SocialShareListButton.tsx
@@ -8,7 +8,7 @@ import {
PopoverProps,
ButtonProps,
} from '@mui/material'
-import { ContentCopy, Facebook, LinkedIn, Share, Twitter } from '@mui/icons-material'
+import { ContentCopy, Facebook, LinkedIn, Share, Twitter, X } from '@mui/icons-material'
import { AlertStore } from 'stores/AlertStore'
import theme from 'common/theme'
@@ -25,7 +25,7 @@ export default function SocialShareListButton({
}) {
const { t } = useTranslation('common')
const [anchorEl, setAnchorEl] = React.useState(null)
- const serializedUrl = new URLSearchParams(url).toString()
+
const handleClick = (event: React.MouseEvent) => {
setAnchorEl(event.currentTarget)
}
@@ -62,20 +62,20 @@ export default function SocialShareListButton({
{t('components.social-share.copy')}
-
+
{t('components.social-share.share')} Facebook
{t('components.social-share.share')} LinkedIn
-
- {t('components.social-share.share')} Twitter
-
+
+ {t('components.social-share.share')} X
+
diff --git a/src/components/common/form/AcceptPrivacyPolicyField.tsx b/src/components/common/form/AcceptPrivacyPolicyField.tsx
index 37ecce0b7..99a4629b3 100644
--- a/src/components/common/form/AcceptPrivacyPolicyField.tsx
+++ b/src/components/common/form/AcceptPrivacyPolicyField.tsx
@@ -1,5 +1,6 @@
import { useTranslation } from 'next-i18next'
import { Typography } from '@mui/material'
+import { useRouter } from 'next/router'
import { routes } from 'common/routes'
import ExternalLink from 'components/common/ExternalLink'
@@ -18,6 +19,8 @@ export default function AcceptPrivacyPolicyField({
...rest
}: AcceptGDPRFieldProps) {
const { t } = useTranslation()
+ const { locale } = useRouter()
+
return (
{t('validation:informed-agree-with')}{' '}
- {t('validation:gdpr')}
+
+ {t('validation:gdpr')}
+
}
{...rest}
diff --git a/src/service/stripeClient.ts b/src/service/stripeClient.ts
index 8ba7c964c..adda96283 100644
--- a/src/service/stripeClient.ts
+++ b/src/service/stripeClient.ts
@@ -4,4 +4,4 @@ const {
publicRuntimeConfig: { STRIPE_PUBLISHABLE_KEY },
} = getConfig()
-export const stripe = await loadStripe(STRIPE_PUBLISHABLE_KEY)
+export const stripe = loadStripe(STRIPE_PUBLISHABLE_KEY)
diff --git a/yarn.lock b/yarn.lock
index 240a11f4e..820395f12 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6625,13 +6625,13 @@ __metadata:
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3":
- version: 7.0.3
- resolution: "cross-spawn@npm:7.0.3"
+ version: 7.0.6
+ resolution: "cross-spawn@npm:7.0.6"
dependencies:
path-key: ^3.1.0
shebang-command: ^2.0.0
which: ^2.0.1
- checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52
+ checksum: 8d306efacaf6f3f60e0224c287664093fa9185680b2d195852ba9a863f85d02dcc737094c6e512175f8ee0161f9b87c73c6826034c2422e39de7d6569cf4503b
languageName: node
linkType: hard