diff --git a/e2e/data/donation-test.data.ts b/e2e/data/donation-test.data.ts new file mode 100644 index 000000000..65e306565 --- /dev/null +++ b/e2e/data/donation-test.data.ts @@ -0,0 +1,23 @@ +export const stripeSuccessFormData = { + cardNumber: '4242 4242 4242 4242', + email: 'e2e_test_mail@test.bg', + expiryDate: '04 / 42', + cvc: '424', + country: 'BG', +} + +export const stripeErrorNoBalanceFormData = { + cardNumber: '4000 0000 0000 9995', + email: 'e2e_test_mail@test.bg', + expiryDate: '04 / 42', + cvc: '424', + country: 'BG', +} + +export const stripeAuthenticationRequiredFormData = { + cardNumber: '4000 0027 6000 3184', + email: 'e2e_test_mail@test.bg', + expiryDate: '04 / 42', + cvc: '424', + country: 'BG', +} diff --git a/e2e/data/localization.ts b/e2e/data/localization.ts index 6397cbabd..8ce2194c2 100644 --- a/e2e/data/localization.ts +++ b/e2e/data/localization.ts @@ -13,8 +13,8 @@ import enLocalizationValidationJson from '../../public/locales/en/validation.jso import bgLocalizationCampaignsJson from '../../public/locales/bg/campaigns.json' import enLocalizationCampaignsJson from '../../public/locales/en/campaigns.json' -import bgLocalizationOneTimeDonationJson from '../../public/locales/bg/one-time-donation.json' -import enLocalizationOneTimeDonationJson from '../../public/locales/en/one-time-donation.json' +import bgLocalizationDonationFlowJson from '../../public/locales/bg/donation-flow.json' +import enLocalizationDonationFlowJson from '../../public/locales/en/donation-flow.json' // All these constants are used in the E2E test pages to manipulate web elements in a respective language // Common localization terms @@ -30,8 +30,8 @@ export const enLocalizationSupport = enLocalizationSupportJson export const bgLocalizationCampaigns = bgLocalizationCampaignsJson export const enLocalizationCampaigns = enLocalizationCampaignsJson // Donations -export const bgLocalizationOneTimeDonation = bgLocalizationOneTimeDonationJson -export const enLocalizationOneTimeDonation = enLocalizationOneTimeDonationJson +export const bgLocalizationDonationFlow = bgLocalizationDonationFlowJson +export const enLocalizationDonationFlow = enLocalizationDonationFlowJson // Validations export const bgLocalizationValidation = bgLocalizationValidationJson export const enLocalizationValidation = enLocalizationValidationJson diff --git a/e2e/data/support-page-tests.data.ts b/e2e/data/support-page-tests.data.ts index d93cddc8e..d08c6b1ad 100644 --- a/e2e/data/support-page-tests.data.ts +++ b/e2e/data/support-page-tests.data.ts @@ -5,11 +5,3 @@ export const supportPageVolutneerTestData = { phone: '+359888000000', comment: 'E2E Test comment', } - -export const anonDonationTestData = { - cardNumber: '4242 4242 4242 4242', - cardExpDate: '04 / 42', - cardCvc: '424', - billingName: 'E2E Test Anonymous Donation', - country: 'BG', -} diff --git a/e2e/pages/web-pages/campaigns/donation-old.page.ts b/e2e/pages/web-pages/campaigns/donation-old.page.ts deleted file mode 100644 index 2ee58807b..000000000 --- a/e2e/pages/web-pages/campaigns/donation-old.page.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { Page, expect } from '@playwright/test' -import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { LanguagesEnum } from '../../../data/enums/languages.enum' -import { - bgLocalizationOneTimeDonation, - enLocalizationOneTimeDonation, -} from '../../../data/localization' -import { SLUG_REGEX } from '../../../utils/helpers' -import { CampaignsPage } from './campaigns.page' - -export class DonationPage extends CampaignsPage { - constructor(page: Page) { - super(page) - } - - // -> Select amount section <- - private readonly otherAmountInputField = ".MuiCollapse-entered input[name='otherAmount']" - private readonly allAmountsSelector = '.MuiBox-root strong' - private readonly regionsDropdownRootElement = '.MuiInputBase-root .MuiSelect-select' - private readonly regionsMenuList = '#menu-cardRegion ul.MuiMenu-list li' - private readonly gridRootSelector = '.MuiGrid-root' - private readonly forwardGridButton = this.gridRootSelector + ' button.MuiButton-contained' - // Section labels - private readonly bgSelectAmountSectionText = bgLocalizationOneTimeDonation['step-labels'].amount - private readonly enSelectAmountSectionText = enLocalizationOneTimeDonation['step-labels'].amount - // TODO Add these three IDs into the component (if possible) and update the test methods - private readonly donationAmount = this.allAmountsSelector + ' #donationAmount' - private readonly feeAmount = this.allAmountsSelector + ' #feeAmount' - private readonly totalChargedAmount = this.allAmountsSelector + ' #totalChargedAmount' - // Grid navigation buttons localization - private readonly bgForwardButtonText = bgLocalizationOneTimeDonation.btns.next - private readonly enForwardButtonText = enLocalizationOneTimeDonation.btns.next - - // -> Personal profile section <- - private readonly buttonsContainer = '.MuiTabs-flexContainer button' - private readonly bgDonateAnonymouslyText = - bgLocalizationOneTimeDonation['second-step']['donate-anonymously'] - private readonly enDonateAnonymouslyText = - enLocalizationOneTimeDonation['second-step']['donate-anonymously'] - private readonly inputRootSelector = '.MuiInputBase-root' - private readonly donateAnonymouslyEmailField = - this.inputRootSelector + " input[name='personsEmail']" - // Section labels - private readonly bgPersonalProfileSectionText = - bgLocalizationOneTimeDonation['step-labels']['personal-profile'] - private readonly enPersonalProfileSectionText = - enLocalizationOneTimeDonation['step-labels']['personal-profile'] - - // -> Send a wish section <- - // Section labels - private readonly sendAWishField = this.inputRootSelector + " textarea[name='message']" - private readonly bgSendAWishSectionText = bgLocalizationOneTimeDonation['step-labels'].wish - private readonly enSendAWishSectionText = enLocalizationOneTimeDonation['step-labels'].wish - - // -> Payment <- - // Section labels - private readonly bgPaymentSectionText = bgLocalizationOneTimeDonation['step-labels'].payment - private readonly enPaymentSectionText = enLocalizationOneTimeDonation['step-labels'].payment - private readonly bgFinishButtonText = bgLocalizationOneTimeDonation.btns.end - private readonly enFinishButtonText = enLocalizationOneTimeDonation.btns.end - private readonly bgSuccessfulDonationTitle = bgLocalizationOneTimeDonation.success.title - private readonly enSuccessfulDonationTitle = enLocalizationOneTimeDonation.success.title - - async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { - await this.page.waitForTimeout(1000) - await expect(this.page, 'The URL is not correct!').toHaveURL( - new RegExp(urlRegExpAsString || `^(.*?)/campaigns/donation-old/${SLUG_REGEX}`), - { - timeout: timeoutParam, - }, - ) - } - - /** - * Is "Select amount" step active - */ - async isSelectAmountStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - return this.isStepActiveByLabelText(this.bgSelectAmountSectionText) - } else if (language === LanguagesEnum.EN) { - return this.isStepActiveByLabelText(this.enSelectAmountSectionText) - } else { - throw new Error('Language not found!') - } - } - - /** - * Is "Personal profile" step active - */ - async isPersonalProfileStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - return this.isStepActiveByLabelText(this.bgPersonalProfileSectionText) - } else if (language === LanguagesEnum.EN) { - return this.isStepActiveByLabelText(this.enPersonalProfileSectionText) - } else { - throw new Error('Language not found!') - } - } - - /** - * Is "Send a wish" step active - */ - async isSendAWishStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - return this.isStepActiveByLabelText(this.bgSendAWishSectionText) - } else if (language === LanguagesEnum.EN) { - return this.isStepActiveByLabelText(this.enSendAWishSectionText) - } else { - throw new Error('Language not found!') - } - } - - /** - * Is "Payment" step active - */ - async isPaymentStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - return this.isStepActiveByLabelText(this.bgPaymentSectionText) - } else if (language === LanguagesEnum.EN) { - return this.isStepActiveByLabelText(this.enPaymentSectionText) - } else { - throw new Error('Language not found!') - } - } - - /** - * Fill in the desired amount of money for donation into the Other Amount input field - * @param {string} amountMoney - */ - async fillOtherAmountInputField(amountMoney: string): Promise { - await this.waitForElementToBePresentedBySelector(this.otherAmountInputField) - await this.setInputFieldBySelector(this.otherAmountInputField, amountMoney) - } - - /** - * Set donation region from the dropdown menu - * @param {string} desiredRegion - */ - async setDonationRegionFromTheDropdown(desiredRegion: DonationRegions): Promise { - await this.clickElement(this.regionsDropdownRootElement) - await this.clickElement(this.regionsMenuList + `[data-value=${desiredRegion}]`) - } - - /** - * Get Total charged amounts as text - */ - async getTotalChargedAmountsAsText(): Promise { - const donationAmount = this.page.locator(this.allAmountsSelector).nth(0) - return this.getTextOfElementByLocator(donationAmount) - // TODO Uncomment when the IDs are added - // return this.getTextOfElementBySelector(this.totalChargedAmount); - } - - /** - * Get Fee amounts as text - */ - async getFeeAmountsAsText(): Promise { - const donationAmount = this.page.locator(this.allAmountsSelector).nth(1) - return this.getTextOfElementByLocator(donationAmount) - // TODO Uncomment when the IDs are added - // return this.getTextOfElementBySelector(this.feeAmount); - } - - /** - * Get Donation amounts as text - */ - async getDonationAmountsAsText(): Promise { - const donationAmount = this.page.locator(this.allAmountsSelector).nth(2) - return this.getTextOfElementByLocator(donationAmount) - // TODO Uncomment when the IDs are added - // return this.getTextOfElementBySelector(this.donationAmount); - } - - /** - * Click Forward/Next button into the donation grid - * @param {LanguagesEnum} language - */ - async clickForwardButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - await this.clickElement(this.forwardGridButton, { hasText: this.bgForwardButtonText }) - } else if (language === LanguagesEnum.EN) { - await this.clickElement(this.forwardGridButton, { hasText: this.enForwardButtonText }) - } else { - throw new Error('Language not found!') - } - } - - /** - * Click Finish/Go to payment button into the donation grid - * @param {LanguagesEnum} language - */ - async clickFinishButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - await this.clickElement(this.forwardGridButton, { hasText: this.bgFinishButtonText }) - } else if (language === LanguagesEnum.EN) { - await this.clickElement(this.forwardGridButton, { hasText: this.enFinishButtonText }) - } else { - throw new Error('Language not found!') - } - } - - /** - * Click Donate Anonymously button into the donation grid (Personal Profile step) - * @param {LanguagesEnum} language - */ - async clickDonateAnonymouslyButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { - if (language === LanguagesEnum.BG) { - await this.clickElement(this.buttonsContainer, { hasText: this.bgDonateAnonymouslyText }) - } else if (language === LanguagesEnum.EN) { - await this.clickElement(this.buttonsContainer, { hasText: this.enDonateAnonymouslyText }) - } else { - throw new Error('Language not found!') - } - } - - /** - * Fill Donate anonymously E-mail input field - * @param {string} emailText - */ - async fillDonateAnonymouslyEmailField(emailText: string): Promise { - await this.setInputFieldBySelector(this.donateAnonymouslyEmailField, emailText) - } - - /** - * Fill Send a wish input field - * @param {string} wishText - */ - async fillSendAWishField(wishText: string): Promise { - await this.setInputFieldBySelector(this.sendAWishField, wishText) - } - - /** - * Is "We thank you for your help and trust!" title visible - * @param {LanguagesEnum} language - the default value is BG - */ - async isSuccessfulDonationTitleVisible( - language: LanguagesEnum = LanguagesEnum.BG, - ): Promise { - return this.isH4HeadingVisible( - language, - this.bgSuccessfulDonationTitle, - this.enSuccessfulDonationTitle, - ) - } -} diff --git a/e2e/pages/web-pages/donation/donation-status.page.ts b/e2e/pages/web-pages/donation/donation-status.page.ts new file mode 100644 index 000000000..6061a711b --- /dev/null +++ b/e2e/pages/web-pages/donation/donation-status.page.ts @@ -0,0 +1,40 @@ +import { Page, expect } from '@playwright/test' +import { LanguagesEnum } from '../../../data/enums/languages.enum' +import { bgLocalizationDonationFlow, enLocalizationDonationFlow } from '../../../data/localization' +import { SLUG_REGEX } from '../../../utils/helpers' +import { CampaignsPage } from '../campaigns/campaigns.page' +export class DonationStatusPage extends CampaignsPage { + constructor(page: Page) { + super(page) + } + + // -> Status titles <- + private readonly bgSuccessTitle = bgLocalizationDonationFlow.status.success.title + private readonly enSuccessTitle = enLocalizationDonationFlow.status.success.title + + // -> Wish form <- + private readonly wishSendText = bgLocalizationDonationFlow.status.success.wish.send + + async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { + await expect(this.page, 'The URL is not correct!').toHaveURL( + new RegExp(urlRegExpAsString || `^(.*?)/campaigns/donation/${SLUG_REGEX}/status?.+$`), + { + timeout: timeoutParam, + }, + ) + } + + async isSucceededStatusTitleDisplayed(): Promise { + return this.isH4HeadingVisible(LanguagesEnum.BG, this.bgSuccessTitle, this.enSuccessTitle) + } + + async submitWishForm(): Promise { + const wishAreaLocator = await this.page.locator('textarea[name="wish"]') + await this.waitForElementToBeReadyByLocator(wishAreaLocator) + await wishAreaLocator.fill('e2e_test_wish') + const buttonLocator = await this.page.locator('button[type="submit"]', { + hasText: this.wishSendText, + }) + await this.clickElementByLocator(buttonLocator) + } +} diff --git a/e2e/pages/web-pages/donation/donation.page.ts b/e2e/pages/web-pages/donation/donation.page.ts new file mode 100644 index 000000000..b5e571ff1 --- /dev/null +++ b/e2e/pages/web-pages/donation/donation.page.ts @@ -0,0 +1,199 @@ +import { Page, expect } from '@playwright/test' +import { + DonationFormAuthState, + DonationFormPaymentMethod, +} from 'components/donation-flow/helpers/types' +import { + stripeSuccessFormData, + stripeErrorNoBalanceFormData, +} from '../../../data/donation-test.data' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { LanguagesEnum } from '../../../data/enums/languages.enum' +import { + bgLocalizationDonationFlow, + bgLocalizationValidation, + enLocalizationDonationFlow, +} from '../../../data/localization' +import { SLUG_REGEX } from '../../../utils/helpers' +import { CampaignsPage } from '../campaigns/campaigns.page' +export class DonationPage extends CampaignsPage { + constructor(page: Page) { + super(page) + } + + // -> Select amount section <- + private readonly bgSelectAmountSectionText = bgLocalizationDonationFlow.step.amount.title + private readonly enSelectAmountSectionText = enLocalizationDonationFlow.step.amount.title + private readonly otherAmountInputField = ".MuiCollapse-entered input[name='otherAmount']" + + // -> Payment method section <- + private readonly regionsDropdownRootElement = '.MuiInputBase-root .MuiSelect-select' + private readonly regionsMenuList = '#menu-cardRegion ul.MuiMenu-list li' + private readonly bgBankTransferText = + bgLocalizationDonationFlow.step['payment-method'].field.method.bank + private readonly bgCardText = bgLocalizationDonationFlow.step['payment-method'].field.method.card + + // -> Authentication section <- + private readonly bgLoginText = bgLocalizationDonationFlow.step.authentication.login.label + private readonly bgRegisterText = bgLocalizationDonationFlow.step.authentication.register.label + private readonly bgNoRegitserText = + bgLocalizationDonationFlow.step.authentication.noregister.label + + // -> Summary section <- + private readonly totalAmountSelector = '[data-testid="total-amount"]' + private readonly bgSubmitButtonText = bgLocalizationDonationFlow.action.submit + private readonly privacyCheckboxText = + bgLocalizationValidation['informed-agree-with'] + ' ' + bgLocalizationValidation.gdpr + private readonly bgStripeErrorNoBalanceText = 'Картата Ви не разполага с достатъчно средства.' + + async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { + await this.page.waitForTimeout(1000) + await expect(this.page, 'The URL is not correct!').toHaveURL( + new RegExp(urlRegExpAsString || `^(.*?)/campaigns/donation/${SLUG_REGEX}`), + { + timeout: timeoutParam, + }, + ) + } + + /** + * Is "Select amount" step active + */ + async isSelectAmountStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + return this.isStepActiveByLabelText(this.bgSelectAmountSectionText) + } else if (language === LanguagesEnum.EN) { + return this.isStepActiveByLabelText(this.enSelectAmountSectionText) + } else { + throw new Error('Language not found!') + } + } + + /** + * Fill in the desired amount of money for donation into the Other Amount input field + * @param {string} amountMoney + */ + async fillOtherAmountInputField(amountMoney: string): Promise { + await this.waitForElementToBePresentedBySelector(this.otherAmountInputField) + await this.setInputFieldBySelector(this.otherAmountInputField, amountMoney) + } + + /** + * Set donation region from the dropdown menu + * @param {string} desiredRegion + */ + async setDonationRegionFromTheDropdown(desiredRegion: DonationRegions): Promise { + await this.clickElement(this.regionsDropdownRootElement) + await this.clickElement(this.regionsMenuList + `[data-value=${desiredRegion}]`) + } + + /** + * Select payment method + * @param {DonationFormPaymentMethod} method + */ + async selectPaymentMethod(method: DonationFormPaymentMethod): Promise { + if (method === DonationFormPaymentMethod.BANK) { + await this.page + .getByText(this.bgBankTransferText, { + exact: true, + }) + .click() + } else if (method === DonationFormPaymentMethod.CARD) { + await this.page + .getByText(this.bgCardText, { + exact: true, + }) + .click() + } else { + throw new Error('Payment method not found!') + } + } + + /** + * Fill in the Stripe form with the test card data + */ + async fillCardForm(options: { fail?: boolean }): Promise { + const data = options.fail ? stripeErrorNoBalanceFormData : stripeSuccessFormData + const baseEmailLocator = this.page + .locator('[data-testid="stripe-payment-form"]') + .frameLocator('iframe') + .first() + const baseCardPaymentLocator = this.page + .locator('[data-testid="stripe-payment-form"]') + .frameLocator('iframe') + .last() + const emailField = baseEmailLocator.locator('input[name="email"]') + const cardNumberField = baseCardPaymentLocator.locator('input[name="number"]') + const cardExpiryField = baseCardPaymentLocator.locator('input[name="expiry"]') + const cvcField = baseCardPaymentLocator.locator('input[name="cvc"]') + const countrySelect = baseCardPaymentLocator.locator('select[name="country"]') + await emailField.fill(data.email) + await cardNumberField.fill(data.cardNumber) + await cardExpiryField.fill(data.expiryDate) + await cvcField.fill(data.cvc) + await countrySelect.selectOption(data.country) + } + + /** + * Fill in email field on a NOREGISTER authentication step + */ + async fillEmailField(): Promise { + const emailField = this.page.locator('.MuiInputBase-root>input[name="email"]') + await emailField.fill(stripeSuccessFormData.email) + } + + /** + * Set donation region from the radio cards + * @param {number} amount + */ + async hasPaymentErrorMessage(): Promise { + const errorAlert = await this.page.locator('.MuiAlert-message', { + hasText: this.bgStripeErrorNoBalanceText, + }) + await this.waitForElementToBePresentedByLocator(errorAlert) + return errorAlert.isVisible() + } + + /** + * Select authentication method + * @param {DonationFormAuthState} auth + */ + async selectAuthentication(auth: DonationFormAuthState): Promise { + const baseLocator = this.page.locator('span.MuiFormControlLabel-label') + if (auth === DonationFormAuthState.LOGIN) { + await baseLocator + .getByText(this.bgLoginText, { + exact: true, + }) + .click() + } else if (auth === DonationFormAuthState.REGISTER) { + await baseLocator.getByText(this.bgRegisterText, { + exact: true, + }) + } else if (auth === DonationFormAuthState.NOREGISTER) { + await baseLocator + .getByText(this.bgNoRegitserText, { + exact: true, + }) + .click() + } + } + + /** + * Set donation region from the radio cards + * @param {number} amount + */ + async checkTotalAmount(amount: number): Promise { + const totalAmount = await this.page.locator(this.totalAmountSelector).textContent() + expect(totalAmount).toBe(`${amount.toLocaleString('BG')} лв.`) + } + + async checkPrivacyCheckbox(): Promise { + await this.selectCheckboxByLabelText([this.privacyCheckboxText]) + } + + async submitForm(): Promise { + const button = this.page.locator(`button:has-text("${this.bgSubmitButtonText}")`) + button.click() + } +} diff --git a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts index 6ae95857e..6632fdb01 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts @@ -2,11 +2,14 @@ import { test, expect, Page } from '@playwright/test' import { HeaderPage } from '../../../pages/web-pages/header.page' import { HomePage } from '../../../pages/web-pages/home.page' import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' -import { bgLocalizationOneTimeDonation } from '../../../data/localization' -import { DonationPage } from '../../../pages/web-pages/campaigns/donation-old.page' +import { DonationPage } from '../../../pages/web-pages/donation/donation.page' import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' -import { anonDonationTestData } from '../../../data/support-page-tests.data' +import { bgLocalizationDonationFlow } from '../../../data/localization' +import { + DonationFormAuthState, + DonationFormPaymentMethod, +} from 'components/donation-flow/helpers/types' +import { DonationStatusPage } from '../../../pages/web-pages/donation/donation-status.page' // This spec contains E2E tests related to anonymous donation flow - custom amount // The tests are dependent, the whole describe should be runned @@ -18,11 +21,11 @@ test.describe.serial( let headerPage: HeaderPage let campaignsPage: CampaignsPage let donationPage: DonationPage - let stripeCheckoutPage: StripeCheckoutPage - const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' + let statusPage: DonationStatusPage // Localization texts - const otherAmountText = bgLocalizationOneTimeDonation['first-step'].other - const bgCardIncludeFeesText = bgLocalizationOneTimeDonation['third-step']['card-include-fees'] + const otherAmountText = bgLocalizationDonationFlow.step.amount.field['other-amount'].label + const bgCardIncludeFeesText = + bgLocalizationDonationFlow.step['payment-method'].field['include-fees'].label test.beforeAll(async ({ browser }) => { page = await browser.newPage() @@ -30,7 +33,7 @@ test.describe.serial( headerPage = new HeaderPage(page) campaignsPage = new CampaignsPage(page) donationPage = new DonationPage(page) - stripeCheckoutPage = new StripeCheckoutPage(page) + statusPage = new DonationStatusPage(page) // For local executions use method navigateToLocalhostHomepage(); // await homepage.navigateToLocalhostHomepage(); await homepage.navigateToEnvHomepage() @@ -54,81 +57,38 @@ test.describe.serial( test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { await campaignsPage.clickDonationSupportButton() await donationPage.checkPageUrlByRegExp() - expect - .soft(await donationPage.isSelectAmountStepActive(), 'Select Amount step is not active.') - .toBeTruthy() await donationPage.selectRadioButtonByLabelText([otherAmountText]) await donationPage.fillOtherAmountInputField('7.50') + await donationPage.selectPaymentMethod(DonationFormPaymentMethod.CARD) await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) - // Expected pattern: - // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. - const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const donationAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(totalChargedAmountText).toEqual('8,10 лв.') - expect.soft(feeAmountText).toEqual('0,60 лв.') - expect(donationAmountText).toEqual('7,50 лв.') }) test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { await donationPage.fillOtherAmountInputField('12.90') - // Expected pattern: - // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. - const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const donationAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(totalChargedAmountText).toEqual('13,56 лв.') - expect.soft(feeAmountText).toEqual('0,66 лв.') - expect(donationAmountText).toEqual('12,90 лв.') + await donationPage.checkTotalAmount(13.56) }) - test('The user is able to fill in e-mail for anonymous donation', async () => { - await donationPage.clickForwardButton() - expect - .soft( - await donationPage.isPersonalProfileStepActive(), - 'Personal Profile step is not active.', - ) - .toBeTruthy() - await donationPage.clickDonateAnonymouslyButton() - await donationPage.fillDonateAnonymouslyEmailField(testEmail) - await donationPage.clickForwardButton() - expect( - await donationPage.isSendAWishStepActive(), - 'Send a wish step is not active.', - ).toBeTruthy() + test('Fill in the stripe card form', async () => { + await donationPage.fillCardForm({ + fail: false, + }) }) - test('After sending a wish, the user is redirected to Stripe', async () => { - await donationPage.fillSendAWishField('E2E test - anonymous donation.') - await donationPage.clickFinishButton() - const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() - const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() - expect - .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') - .toContain('13.56') - expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( - testEmail, - ) + test('The user is able to fill in e-mail for anonymous donation', async () => { + await donationPage.selectAuthentication(DonationFormAuthState.NOREGISTER) + await donationPage.fillEmailField() }) - test('The user is able to pay via Stripe', async () => { - await stripeCheckoutPage.fillPaymentForm([ - anonDonationTestData.cardNumber, - anonDonationTestData.cardExpDate, - anonDonationTestData.cardCvc, - anonDonationTestData.billingName, - anonDonationTestData.country, - ]) + test('The user can submit the form', async () => { + await donationPage.checkPrivacyCheckbox() + await donationPage.submitForm() + await page.waitForEvent('domcontentloaded') + }) - expect - .soft( - await donationPage.isSuccessfulDonationTitleVisible(), - "'We thank you for your help and trust!' title is not visible.", - ) - .toBeTruthy() - expect(await donationPage.isPaymentStepActive(), 'Payment step is not active.').toBeTruthy() + test('The user is redirected to succes page', async () => { + await statusPage.checkPageUrlByRegExp() + expect(await statusPage.isSucceededStatusTitleDisplayed()).toBe(true) }) }, ) diff --git a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts index 0244dfc5b..0fad485c8 100644 --- a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts +++ b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts @@ -2,39 +2,40 @@ import { test, expect, Page } from '@playwright/test' import { HeaderPage } from '../../../pages/web-pages/header.page' import { HomePage } from '../../../pages/web-pages/home.page' import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' -import { enLocalizationOneTimeDonation } from '../../../data/localization' -import { DonationPage } from '../../../pages/web-pages/campaigns/donation-old.page' +import { DonationPage } from '../../../pages/web-pages/donation/donation.page' import { DonationRegions } from '../../../data/enums/donation-regions.enum' -import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' -import { anonDonationTestData } from '../../../data/support-page-tests.data' -import { LanguagesEnum } from '../../../data/enums/languages.enum' +import { bgLocalizationDonationFlow } from '../../../data/localization' +import { + DonationFormAuthState, + DonationFormPaymentMethod, +} from 'components/donation-flow/helpers/types' +import { DonationStatusPage } from '../../../pages/web-pages/donation/donation-status.page' -// This spec contains E2E tests related to anonymous donation flow - fixed amount +// This spec contains E2E tests related to anonymous donation flow - custom amount // The tests are dependent, the whole describe should be runned test.describe.serial( - 'Anonymous contributor is able to donate fixed amount - EN language version', + 'Anonymous contributor is able to donate fixed amount - BG language version', async () => { let page: Page let homepage: HomePage let headerPage: HeaderPage let campaignsPage: CampaignsPage let donationPage: DonationPage - let stripeCheckoutPage: StripeCheckoutPage - const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' + let statusPage: DonationStatusPage // Localization texts - const enCardIncludeFeesText = enLocalizationOneTimeDonation['third-step']['card-include-fees'] + const bgCardIncludeFeesText = + bgLocalizationDonationFlow.step['payment-method'].field['include-fees'].label - test.beforeAll(async ({ browser, baseURL }) => { + test.beforeAll(async ({ browser }) => { page = await browser.newPage() homepage = new HomePage(page) headerPage = new HeaderPage(page) campaignsPage = new CampaignsPage(page) donationPage = new DonationPage(page) - stripeCheckoutPage = new StripeCheckoutPage(page) + statusPage = new DonationStatusPage(page) // For local executions use method navigateToLocalhostHomepage(); // await homepage.navigateToLocalhostHomepage(); await homepage.navigateToEnvHomepage() - await headerPage.changeanguageHeaderButtonToBe(LanguagesEnum.EN) }) test.afterAll(async () => { @@ -46,8 +47,6 @@ test.describe.serial( await campaignsPage.clickCampaignCardByIndex(0) // We move from the common Campaigns page to the particular campain page // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} - // expect to not break - expect( await campaignsPage.checkPageUrlByRegExp(), 'The url is not changed after clicking on the campaign card.', @@ -57,86 +56,37 @@ test.describe.serial( test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { await campaignsPage.clickDonationSupportButton() await donationPage.checkPageUrlByRegExp() - expect - .soft( - await donationPage.isSelectAmountStepActive(LanguagesEnum.EN), - 'Select Amount step is not active.', - ) - .toBeTruthy() await donationPage.selectRadioButtonByLabelText(['10']) + await donationPage.selectPaymentMethod(DonationFormPaymentMethod.CARD) await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) - await donationPage.selectCheckboxByLabelText([enCardIncludeFeesText]) - // Expected pattern: - // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. - const donationAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const totalChargedAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(donationAmountText).toMatch('10.00') - expect.soft(feeAmountText).toMatch('0.63') - expect(totalChargedAmountText).toMatch('10.63') + await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) }) test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { await donationPage.selectRadioButtonByLabelText(['20']) - // Expected pattern: - // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. - const donationAmountText = await donationPage.getTotalChargedAmountsAsText() - const feeAmountText = await donationPage.getFeeAmountsAsText() - const totalChargedAmountText = await donationPage.getDonationAmountsAsText() - expect.soft(donationAmountText).toMatch('20.00') - expect.soft(feeAmountText).toMatch('0.75') - expect(totalChargedAmountText).toMatch('20.75') + await donationPage.checkTotalAmount(20.75) + }) + + test('Fill in the stripe card form', async () => { + await donationPage.fillCardForm({ + fail: false, + }) }) test('The user is able to fill in e-mail for anonymous donation', async () => { - await donationPage.clickForwardButton(LanguagesEnum.EN) - expect - .soft( - await donationPage.isPersonalProfileStepActive(LanguagesEnum.EN), - 'Personal Profile step is not active.', - ) - .toBeTruthy() - await donationPage.clickDonateAnonymouslyButton(LanguagesEnum.EN) - await donationPage.fillDonateAnonymouslyEmailField(testEmail) - await donationPage.clickForwardButton(LanguagesEnum.EN) - expect( - await donationPage.isSendAWishStepActive(LanguagesEnum.EN), - 'Send a wish step is not active.', - ).toBeTruthy() + await donationPage.selectAuthentication(DonationFormAuthState.NOREGISTER) + await donationPage.fillEmailField() }) - test('After sending a wish, the user is redirected to Stripe', async () => { - await donationPage.fillSendAWishField('E2E test - anonymous donation.') - await donationPage.clickFinishButton(LanguagesEnum.EN) - const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() - const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() - expect - .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') - .toContain('20.75') - expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( - testEmail, - ) + test('The user can submit the form', async () => { + await donationPage.checkPrivacyCheckbox() + await donationPage.submitForm() + await page.waitForEvent('domcontentloaded') }) - test('The user is able to pay via Stripe', async () => { - await stripeCheckoutPage.fillPaymentForm([ - anonDonationTestData.cardNumber, - anonDonationTestData.cardExpDate, - anonDonationTestData.cardCvc, - anonDonationTestData.billingName, - anonDonationTestData.country, - ]) - // Now we're redirected to the Donation page - expect - .soft( - await donationPage.isSuccessfulDonationTitleVisible(LanguagesEnum.EN), - "'We thank you for your help and trust!' title is not visible.", - ) - .toBeTruthy() - expect( - await donationPage.isPaymentStepActive(LanguagesEnum.EN), - 'Payment step is not active.', - ).toBeTruthy() + test('The user is redirected to succes page', async () => { + await statusPage.checkPageUrlByRegExp() + expect(await statusPage.isSucceededStatusTitleDisplayed()).toBe(true) }) }, ) diff --git a/e2e/tests/regression/donation-flow/donation-fail.spec.ts b/e2e/tests/regression/donation-flow/donation-fail.spec.ts new file mode 100644 index 000000000..1e487a2ec --- /dev/null +++ b/e2e/tests/regression/donation-flow/donation-fail.spec.ts @@ -0,0 +1,89 @@ +import { test, expect, Page } from '@playwright/test' +import { HeaderPage } from '../../../pages/web-pages/header.page' +import { HomePage } from '../../../pages/web-pages/home.page' +import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' +import { DonationPage } from '../../../pages/web-pages/donation/donation.page' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { bgLocalizationDonationFlow } from '../../../data/localization' +import { + DonationFormAuthState, + DonationFormPaymentMethod, +} from 'components/donation-flow/helpers/types' + +// This spec contains E2E tests related to anonymous donation flow - custom amount +// The tests are dependent, the whole describe should be runned +test.describe.serial( + 'Anonymous contributor is able to donate custom amount - BG language version', + async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let campaignsPage: CampaignsPage + let donationPage: DonationPage + // Localization texts + const bgCardIncludeFeesText = + bgLocalizationDonationFlow.step['payment-method'].field['include-fees'].label + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + campaignsPage = new CampaignsPage(page) + donationPage = new DonationPage(page) + // For local executions use method navigateToLocalhostHomepage(); + // await homepage.navigateToLocalhostHomepage(); + await homepage.navigateToEnvHomepage() + }) + + test.afterAll(async () => { + await page.close() + }) + + test('Particular campaign can be opened through the Campaign page', async () => { + await headerPage.clickDonateHeaderNavButton() + await campaignsPage.clickCampaignCardByIndex(0) + // We move from the common Campaigns page to the particular campain page + // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} + expect( + await campaignsPage.checkPageUrlByRegExp(), + 'The url is not changed after clicking on the campaign card.', + ) + }) + + test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { + await campaignsPage.clickDonationSupportButton() + await donationPage.checkPageUrlByRegExp() + await donationPage.selectRadioButtonByLabelText(['10']) + await donationPage.selectPaymentMethod(DonationFormPaymentMethod.CARD) + await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) + await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) + }) + + test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { + await donationPage.selectRadioButtonByLabelText(['20']) + await donationPage.checkTotalAmount(20.75) + }) + + test('Fill in the stripe card form', async () => { + await donationPage.fillCardForm({ + fail: true, + }) + }) + + test('The user is able to fill in e-mail for anonymous donation', async () => { + await donationPage.selectAuthentication(DonationFormAuthState.NOREGISTER) + await donationPage.fillEmailField() + }) + + test('The user can submit the form', async () => { + await donationPage.checkPrivacyCheckbox() + await donationPage.submitForm() + }) + + test('Submit error is visible', async () => { + await donationPage.submitForm() + const message = await donationPage.hasPaymentErrorMessage() + expect(message).toBe(true) + }) + }, +) diff --git a/e2e/utils/helpers.ts b/e2e/utils/helpers.ts index 285e098a0..24ceda9d9 100644 --- a/e2e/utils/helpers.ts +++ b/e2e/utils/helpers.ts @@ -27,4 +27,4 @@ export const expectCopied = async (page: Page, textToCheck: string) => { * - (?:-[a-z0-9]+)* matches the characters - and a-z0-9 between one and unlimited times, as many times as possible, giving back as needed (greedy) and does not remember the match * - $ asserts position at the end of the string */ -export const SLUG_REGEX = `[a-z0-9]+(?:-[a-z0-9]+)*$` +export const SLUG_REGEX = `[a-z0-9]+(?:-[a-z0-9]+)*` diff --git a/public/locales/bg/donation-flow.json b/public/locales/bg/donation-flow.json index 9cfe2d6c6..e0ca4bf7c 100644 --- a/public/locales/bg/donation-flow.json +++ b/public/locales/bg/donation-flow.json @@ -83,7 +83,12 @@ "label": "Продължете без регистрация ", "description": "Продължавайки без регистрация, нямате възможност да запазите дарението в историята на профила си както и да правите месечни дарения по избрана кампания" }, - "password": "Парола", + "field": { + "password": "Парола", + "email": { + "error": "Трябва да въведете валиден имейл" + } + }, "alert": { "authenticate": { "title": "Избирайки да се впишете, ще можете да", @@ -104,8 +109,14 @@ "anonymous": { "label": "Искам да съм анонимен", "description": "Ако останете анонимен името ви няма да бъде показано на кампанията" + }, + "privacy": { + "error": "Трябва да приемете политиката за поверителност" } }, + "alerts": { + "error": "Нещо се обърка, моля опитайте пак или презаредете страницата" + }, "total": "Общо" } }, diff --git a/public/locales/bg/one-time-donation.json b/public/locales/bg/one-time-donation.json deleted file mode 100644 index 014d90d1e..000000000 --- a/public/locales/bg/one-time-donation.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "step-labels": { - "amount": "Изберете сума", - "personal-profile": "Личен профил", - "wish": "Пожелайте нещо", - "payment": "Плащане" - }, - "anonymous-menu": { - "checkbox-label": "Дарение без регистрация", - "info-start": "При дарение без регистрация, няма да можем да Ви изпратим сертификат за дарение, който да използвате за данъчни облекчения. Ако искате да получите сертификат, регистрирайте се или влезте в профила си.", - "firstName": "Име", - "lastName": "Фамилия", - "phone": "Телефон", - "info-end": "Данните Ви няма да бъдат споделяни с никого.", - "want-anonymous-donation": "Искам да даря анонимно!" - }, - "first-step": { - "wish": "Искате ли да пожелаете нещо на бенефициента?", - "message": "Вашето послание", - "check-box-label": "Анонимно дарение", - "info-anonymous": "Анонимно дарение означава, че организаторът и бенефициентът на кампания няма да узнаят Вашето име", - "amount": "Каква сума желаете да дарите?", - "other": "Друга сума", - "BGN": "лв." - }, - "second-step": { - "login": "Влизане в профил", - "password": "Парола", - "checkbox-label": "Запомни", - "btn-login": "Влизане", - "new-create": "Или", - "new-create-profile": "Създайте нов профил", - "donate-anonymously": "Дарете анонимно", - "intro-text": "Можете да дарите с личен профил или анонимно.", - "logged-user": "Вече сте влезли във Вашия профил", - "info-logged-user": "Вашето дарение ще бъде свързано с име: {{fullName}} и email: {{email}}, освен ако не решите да дарите анонимно." - }, - "success": { - "title": "Благодарим за доверието и подкрепата!", - "title-bank": "Ще очакваме Вашето дарение!", - "subtitle": "Вашето дарение ще помогне на кампанията по-бързо да постигне своята цел!", - "subtitle-bank": "Благодарим за доверието и запомнете да впишете кода на дарението в основанието на превода си!", - "say-to-us": "Вашата обратна връзка е важна за нас!", - "share-to": "Подкрепете кампанията, като споделите информация за нея в социалните мрежи.", - "btn-generate": "Генерирай Сертификат", - "btn-say-to-us": "Oбратна връзка", - "btn-other-campaign": "Oще кампании", - "btn-back-to-campaign": "Виж кампанията" - }, - "third-step": { - "title": "Как желаете да дарите?", - "card": "Карта", - "card-include-fees": "Искам да покрия таксата за карта издадена в: ", - "card-fees": "При дарение с карта използваме услугите на Stripe, като за всеки трансфер Stripe начисляват такса според региона на картоиздателя ви. За да се ориентирате за нетното дарение, след като изберете желаната сума, ще ви покажем изчислената такса. Повече за таксите от Страйп вижте тук: ", - "card-calculated-fees": "За вашия превод от {{totalAmount}}, таксата на Stripe ще е {{fees}}, а кампанията ще получи {{amount}}", - "card-region": { - "title": "регион", - "EU": "Европа", - "UK": "Великобритания", - "Other": "други" - }, - "bank-payment": "Банков превод", - "bank-instructions1": "За дарение по банков път, моля използвайте приложението препоръчано от Вашата банка като въведете данните посочени по-долу. Дарението Ви няма да се отрази веднага в системата, тъй като все още предстои разработка на интеграцията с банката.", - "bank-instructions2": "Разчитаме на Вас да си направите профил или да ни оставите email на следващaтa стъпкa, за да можем да свържем дарението с Вашия потребителски профил. Благодарим предварително!!!", - "bank-details": "Детайли на банкова сметка:", - "btn-copy": "Копирай", - "owner": "Сдружение Подкрепи БГ", - "bank": "Уникредит Булбанк", - "reason-donation": "Като основание за превод въведете:", - "message-warning": "Ако не въведете точно основанието, може да не успеем да разпределим парите към предназначената кампания.", - "recurring-donation": "Дарявай повторно всеки месец тази сума до края на кампанията! Може да се откажете по всяко време." - }, - "alerts": { - "success": "Дарението е направено успешно!", - "error": "Възникна грешка в процеса на обработка!" - }, - "fail": { - "title": "За съжаление, възникна проблем!", - "subtitle": "Трансакцията не можа да бъде осъществена. Причините могат да бъдат няколко, включително проблем с Вашата интернет връзка.", - "btn-again": "Опитайте пак", - "btn-connect": "Пишете ни", - "btn-back-to-campaign": "Виж кампанията" - }, - "errors-fields": { - "checkbox-anonymous": "Моля изберете опция Анонимно дарение или Дарение без регистрация ако искате да продължите!", - "bank-payment": "Съжаляваме за създаденото неудобство временно може да дарите само чрез банков превод!", - "amount": "Моля изберете сумата, която желаете да дарите!", - "other-amount": "Минималната сума която може да дарите с карта е 1лв." - }, - "btns": { - "back": "Назад", - "next": "Напред", - "end": "Премини към плащане" - } -} diff --git a/public/locales/en/donation-flow.json b/public/locales/en/donation-flow.json index 0f8554cf5..c5ebb4c1b 100644 --- a/public/locales/en/donation-flow.json +++ b/public/locales/en/donation-flow.json @@ -87,7 +87,12 @@ "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" }, - "password": "Password", + "field": { + "password": "Password", + "email": { + "error": "You have to enter a valid email" + } + }, "alert": { "authenticate": { "title": "Choosing to login you will be able to", @@ -109,6 +114,9 @@ "anonymous": { "label": "I want to be anonymous", "description": "If you choose to be anonymous, your name will not be visible in the campaign" + }, + "privacy": { + "error": "You have to accept the privacy policy" } } } diff --git a/public/locales/en/one-time-donation.json b/public/locales/en/one-time-donation.json deleted file mode 100644 index e20f2eece..000000000 --- a/public/locales/en/one-time-donation.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "step-labels": { - "amount": "Select amount", - "personal-profile": "Personal profile", - "wish": "Send a wish", - "payment": "Payment" - }, - "anonymous-menu": { - "checkbox-label": "Donate anonymously", - "info-start": "When donating without registration we won't be able to send you back a donation certificate or a list of your donations. If you still want to receive a certificate, please share at least your email - it will not be visible in the platform.", - "firstName": "First name", - "lastName": "Last name", - "phone": "Telephone", - "info-end": "Your data would not be shared with anyone", - "want-anonymous-donation": "I want to donate anonymously!" - }, - "first-step": { - "wish": "Would you like to wish something to the beneficiary?", - "message": "Your wish", - "check-box-label": "Anonymous donation", - "info-anonymous": "Anonymous donation means that the organisator and beneficiery would not be aware of your identity", - "amount": "How much would you like to donate?", - "other": "Other amount", - "BGN": "BGN" - }, - "second-step": { - "login": "Log in", - "password": "Password", - "checkbox-label": "Remember me", - "btn-login": "Log in", - "new-create": "Or", - "new-create-profile": "Create new profile", - "donate-anonymously": "Donate anonymously", - "intro-text": "You can donate with personal profile or anonymously.", - "logged-user": "You are already logged in to your account", - "info-logged-user": "Your donation would be connected with name: {{fullName}} and email: {{email}}, unless you decide to donate anonymously." - }, - "success": { - "title": "We thank you for your help and trust!", - "title-bank": "We look forward to your donation!", - "subtitle": "Your donation would help the campaign get to it's target sooner!", - "subtitle-bank": "Thank you for your trust and don't forget to enter the donation code as the reason for your transfer!", - "say-to-us": "Your feedback is important to us!", - "share-to": "Please support this campaign by sharing its link to social networks for reaching more people!", - "btn-generate": "Generate a certificate", - "btn-say-to-us": "Feedback", - "btn-other-campaign": "See more campaigns", - "btn-back-to-campaign": "See the campaign" - }, - "third-step": { - "title": "How would you like to pay", - "card": "Card", - "card-include-fees": "I want to cover transaction fees for card issued in:", - "card-fees": "For donations by card we use the services of Stripe and for every transfer they charge a fee depending on region of your card issuer. To orient you about the net donation, after you choose the desired amount, we will show you the calculated fee below. More details for Stripe fees can be found here: ", - "card-calculated-fees": "For your donation of {{amount}}, the fee from Stripe will be {{fees}}, and the total charged amount will be {{totalAmount}}", - "card-region": { - "title": "region", - "EU": "Europe", - "UK": "Great Britain", - "Other": "other" - }, - "bank-payment": "Bank transfer", - "bank-instructions1": "To donate via bank transfer, please use your bank recommonded application with entering our bank details from below. The donation will not be updated immediatelly as the implemnetation of the automated bank integration is still pending.", - "bank-instructions2": "We trust you to register or to leave an email on the next step, so that we can link your donation to your account. Thank you!!!", - "bank-details": "Details of our bank account:", - "btn-copy": "Copy", - "owner": "Association Podkrepi BG", - "bank": "Unicredit Bulbank", - "reason-donation": "For payment reference use:", - "message-warning": "If you don't enter the exact reference we may not be able to assign the money to the desired campaign.", - "recurring-donation": "Donate the same amount every month until the end of the campaign! Cancel anytime." - }, - "alerts": { - "success": "Donation was processed successfully!", - "error": "Error ocurred during processing of the donation!" - }, - "fail": { - "title": "Unfortunately there was a problem!", - "subtitle": "The transaction could not be done. The reasons for that many, including a problem with your internet connection.", - "btn-again": "Try again", - "btn-connect": "Contact us", - "btn-back-to-campaign": "See the campaign" - }, - "errors-fields": { - "checkbox-anonymous": "Please select an option for anonymous donation or donation without registering if you would like to continue!", - "bank-payment": "We are sorry for the inconvenience, but you can currently donate only through a bank transfer", - "amount": "Please select the amount you would like to donate!", - "other-amount": "The minimum amount you can donate with a card is BGN 1." - }, - "btns": { - "back": "Back", - "next": "Next", - "end": "Finish" - } -} diff --git a/src/common/routes.ts b/src/common/routes.ts index 50fff4509..0811c972c 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -55,7 +55,6 @@ export const routes = { index: '/campaigns', create: '/campaigns/create', viewCampaignBySlug: (slug: string) => `/campaigns/${slug}`, - oneTimeDonation: (slug: string) => `/campaigns/donation-old/${slug}`, donation: (slug: string) => `/campaigns/donation/${slug}`, donationStatus: (slug: string) => `/campaigns/donation/${slug}/status`, }, diff --git a/src/components/campaigns/CampaignCard.tsx b/src/components/campaigns/CampaignCard.tsx index fc50d6e95..644f2f08b 100644 --- a/src/components/campaigns/CampaignCard.tsx +++ b/src/components/campaigns/CampaignCard.tsx @@ -164,7 +164,7 @@ export default function CampaignCard({ campaign, index }: Props) { } diff --git a/src/components/common/form/CircleCheckboxField.tsx b/src/components/common/form/CircleCheckboxField.tsx index 36fcf09f2..193f28f9d 100644 --- a/src/components/common/form/CircleCheckboxField.tsx +++ b/src/components/common/form/CircleCheckboxField.tsx @@ -22,7 +22,7 @@ export type CircleCheckboxField = { } export default function CircleCheckboxField({ name, label, labelProps }: CircleCheckboxField) { - const { t } = useTranslation('one-time-donation') + const { t } = useTranslation() const [field, meta] = useField(name) const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' return ( diff --git a/src/components/common/form/RadioButton.tsx b/src/components/common/form/RadioButton.tsx index 2375890f5..941bc55ac 100644 --- a/src/components/common/form/RadioButton.tsx +++ b/src/components/common/form/RadioButton.tsx @@ -1,6 +1,6 @@ import { Check } from '@mui/icons-material' import { styled, lighten } from '@mui/material/styles' -import { FormControlLabel, Radio, Typography, RadioProps } from '@mui/material' +import { FormControlLabel, Radio, Typography, RadioProps, Skeleton } from '@mui/material' import theme from 'common/theme' import React from 'react' @@ -84,6 +84,7 @@ type RadioButtonProps = { label: string value: string | number disabled?: boolean + loading?: boolean muiRadioButtonProps?: Partial } @@ -103,27 +104,45 @@ const disabledCheckStlyes = { borderRadius: theme.borders.round, color: theme.palette.text.disabled, } -function RadioButton({ checked, label, muiRadioButtonProps, value, disabled }: RadioButtonProps) { +function RadioButton({ + checked, + label, + muiRadioButtonProps, + value, + disabled, + loading, +}: RadioButtonProps) { return ( - {label}} - control={ - } - checkedIcon={ - - } - {...muiRadioButtonProps} - /> - } - /> + {loading ? ( + + ) : ( + {label}} + control={ + } + checkedIcon={ + + } + {...muiRadioButtonProps} + /> + } + /> + )} ) } diff --git a/src/components/common/form/RadioButtonGroup.tsx b/src/components/common/form/RadioButtonGroup.tsx index 47fb1ecdb..69035cd00 100644 --- a/src/components/common/form/RadioButtonGroup.tsx +++ b/src/components/common/form/RadioButtonGroup.tsx @@ -22,6 +22,7 @@ export type RadioButtonGroup = { name: string options: RadioButtonGroupOptions[] disabled?: boolean + loading?: boolean columns?: number muiRadioGroupProps?: Partial /** @@ -39,11 +40,12 @@ export default function RadioButtonGroup({ name, options, disabled, + loading, columns = 2, muiRadioGroupProps, muiRadioButtonGridProps, }: RadioButtonGroup) { - const { t } = useTranslation('one-time-donation') + const { t } = useTranslation() const [field, meta, { setValue }] = useField(name) const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' return ( @@ -69,6 +71,7 @@ export default function RadioButtonGroup({ = yup @@ -94,13 +94,21 @@ export const validationSchema: yup.SchemaOf = yup export function DonationFlowForm() { const formikRef = useRef | null>(null) const { t, i18n } = useTranslation('donation-flow') - const { data: session } = useSession() + const { data: session } = useSession({ + required: false, + onUnauthenticated: () => { + formikRef.current?.setFieldValue('authentication', null) + }, + }) useEffect(() => { if (session?.user) { formikRef.current?.setFieldValue('email', session.user.email) + formikRef.current?.setFieldValue('authentication', DonationFormAuthState.AUTHENTICATED) + formikRef.current?.setFieldValue('isAnonymous', false) return } formikRef.current?.setFieldValue('email', '') + formikRef.current?.setFieldValue('isAnonymous', true) }, [session]) const { campaign, stripePaymentIntent, paymentError, setPaymentError } = useDonationFlow() const stripe = useStripe() @@ -140,7 +148,7 @@ export function DonationFlowForm() { setSubmitPaymentLoading(false) setPaymentError({ type: 'invalid_request_error', - message: t('alerts.error'), + message: t('step.summary.alerts.error'), }) return } @@ -161,7 +169,7 @@ export function DonationFlowForm() { setSubmitPaymentLoading(false) setPaymentError({ type: 'invalid_request_error', - message: t('alerts.error'), + message: t('step.summary.alerts.error'), }) return } @@ -180,7 +188,7 @@ export function DonationFlowForm() { setSubmitPaymentLoading(false) setPaymentError({ type: 'invalid_request_error', - message: t('alerts.error'), + message: t('step.summary.alerts.error'), }) return } diff --git a/src/components/donation-flow/DonationFlowStatusPage.tsx b/src/components/donation-flow/DonationFlowStatusPage.tsx index 4ca7a9726..98ac482b5 100644 --- a/src/components/donation-flow/DonationFlowStatusPage.tsx +++ b/src/components/donation-flow/DonationFlowStatusPage.tsx @@ -82,7 +82,7 @@ export default function DonationFlowStatusPage({ slug }: { slug: string }) { if (bank_payment === 'true') { // If we are redirected on that page means that the payment is a bank payment and we can clear the form state sessionStorage.removeItem('donation-form') - setStatus(DonationFormPaymentStatus.REQUIRES_PAYMENT) + setStatus(DonationFormPaymentStatus.SUCCEEDED) return } if (!stripe || !payment_intent_client_secret) { @@ -113,7 +113,7 @@ export default function DonationFlowStatusPage({ slug }: { slug: string }) { const Success = () => ( - + {session.data?.user ? `${session.data?.user?.given_name} ${session.data.user.family_name}, ${t( 'status.success.title-logged', diff --git a/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx b/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx index 7f31b206f..c37540406 100644 --- a/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx +++ b/src/components/donation-flow/alerts/PaymentSummaryAlert.tsx @@ -5,7 +5,7 @@ import { Alert, Box, BoxProps, IconButton, Stack, Tooltip, Typography } from '@m import { styled } from '@mui/styles' import theme from 'common/theme' import { moneyPublicDecimals2 } from 'common/util/money' -import { stripeFeeCalculator } from 'components/one-time-donation/helpers/stripe-fee-calculator' +import { stripeFeeCalculator } from 'components/donation-flow/helpers/stripe-fee-calculator' import { CardRegion } from 'gql/donations.enums' const StyledTypography = styled(Typography)(({ theme }) => ({ @@ -69,7 +69,9 @@ function PaymentSummaryAlert({ icon={false}> {t('step.summary.total')}: - {moneyPublicDecimals2(donationAmount)} + + {moneyPublicDecimals2(donationAmount)} + diff --git a/src/components/donation-flow/common/RadioCardGroup.tsx b/src/components/donation-flow/common/RadioCardGroup.tsx index 23695939d..128c5ee8f 100644 --- a/src/components/donation-flow/common/RadioCardGroup.tsx +++ b/src/components/donation-flow/common/RadioCardGroup.tsx @@ -10,6 +10,7 @@ import { RadioGroupProps, Stack, Grid, + Skeleton, } from '@mui/material' import { styled, lighten } from '@mui/material/styles' import theme from 'common/theme' @@ -29,10 +30,18 @@ interface StyledRadioCardItemProps extends CardProps { control: React.ReactNode icon: React.ReactNode disabled?: boolean + loading?: boolean selected?: boolean } -function RadioCardItem({ control, icon, selected, disabled, ...rest }: StyledRadioCardItemProps) { +function RadioCardItem({ + control, + icon, + selected, + disabled, + loading, + ...rest +}: StyledRadioCardItemProps) { const selectedStyles = { backgroundColor: selected ? lighten(theme.palette.primary.light, 0.7) : 'inherit', } @@ -50,7 +59,9 @@ function RadioCardItem({ control, icon, selected, disabled, ...rest }: StyledRad styles = selectedStyles } - return ( + return loading ? ( + + ) : ( {icon} @@ -71,6 +82,7 @@ export interface RadioCardGroupProps extends RadioGroupProps { options: Option[] name: string columns: 1 | 2 | 3 | 4 | 6 | 12 + loading?: boolean } /** @@ -91,7 +103,7 @@ export interface RadioCardGroupProps extends RadioGroupProps { * icon: , * }, */ -function RadioCardGroup({ options, name, columns }: RadioCardGroupProps) { +function RadioCardGroup({ options, name, columns, loading }: RadioCardGroupProps) { const [field, meta, { setValue }] = useField(name) const handleChange = (event: React.ChangeEvent) => { setValue(event.target.value) @@ -129,6 +141,7 @@ function RadioCardGroup({ options, name, columns }: RadioCardGroupProps) { icon={option.icon} selected={field.value === option.value && !option.disabled} disabled={option.disabled} + loading={loading} /> ))} diff --git a/src/components/donation-flow/steps/Amount.tsx b/src/components/donation-flow/steps/Amount.tsx index ee9f42305..653378ef9 100644 --- a/src/components/donation-flow/steps/Amount.tsx +++ b/src/components/donation-flow/steps/Amount.tsx @@ -14,6 +14,7 @@ import FormTextField from 'components/common/form/FormTextField' import { stripeFeeCalculator, stripeIncludeFeeCalculator } from '../helpers/stripe-fee-calculator' import { DonationFormData } from '../helpers/types' +import { useSession } from 'next-auth/react' export const initialAmountFormValues = { amountChosen: '', @@ -57,9 +58,10 @@ export default function Amount({ disabled?: boolean sectionRef?: React.MutableRefObject }) { - const { data: prices } = useSinglePriceList() - const formik = useFormikContext() const { t } = useTranslation('donation-flow') + const { status } = useSession() + const formik = useFormikContext() + const { data: prices } = useSinglePriceList() const mobile = useMediaQuery('(max-width:600px)') const [{ value }] = useField('amountChosen') @@ -97,6 +99,7 @@ export default function Amount({ diff --git a/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx b/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx index 2a5970224..d2a477128 100644 --- a/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx +++ b/src/components/donation-flow/steps/payment-method/PaymentDetailsStripeForm.tsx @@ -1,6 +1,7 @@ import { useSession } from 'next-auth/react' import { LinkAuthenticationElement, PaymentElement } from '@stripe/react-stripe-js' import { Box, BoxProps } from '@mui/material' +import { useEffect, useState } from 'react' export type PaymentDetailsStripeFormProps = { containerProps?: BoxProps @@ -9,12 +10,16 @@ export default function PaymentDetailsStripeForm({ containerProps, }: PaymentDetailsStripeFormProps) { const { data: session } = useSession() + const [email, setEmail] = useState(session?.user?.email || '') + useEffect(() => { + setEmail(session?.user?.email || '') + }, [session]) return ( - + diff --git a/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx b/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx index 8f0d82c24..6de453ac9 100644 --- a/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx +++ b/src/components/donation-flow/steps/payment-method/PaymentMethod.tsx @@ -1,5 +1,6 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { useSession } from 'next-auth/react' import { Alert, Box, Collapse, Typography, useMediaQuery } from '@mui/material' import { useField } from 'formik' @@ -22,6 +23,7 @@ export default function PaymentMethod({ const { t } = useTranslation('donation-flow') const isSmall = useMediaQuery(theme.breakpoints.down('md')) const [payment] = useField('payment') + const { status } = useSession() const options = [ { value: 'card', @@ -55,6 +57,7 @@ export default function PaymentMethod({ + ), }, @@ -83,8 +86,13 @@ export default function PaymentMethod({ ) : ( <> - - + + diff --git a/src/components/donation-flow/steps/payment-method/TaxesCheckbox.tsx b/src/components/donation-flow/steps/payment-method/TaxesCheckbox.tsx index c90cf6d1b..f5b2cd8e2 100644 --- a/src/components/donation-flow/steps/payment-method/TaxesCheckbox.tsx +++ b/src/components/donation-flow/steps/payment-method/TaxesCheckbox.tsx @@ -15,7 +15,7 @@ export const TaxesCheckbox = () => { return ( <> - + { } /> - + - - {t('anonymous-menu.checkbox-label')} - - - - - {t('anonymous-menu.info-start')} - - - - - - - ) -} diff --git a/src/components/one-time-donation/FormikStepper.tsx b/src/components/one-time-donation/FormikStepper.tsx deleted file mode 100644 index 3a270b8fa..000000000 --- a/src/components/one-time-donation/FormikStepper.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React, { PropsWithChildren, useCallback, useContext, useEffect } from 'react' -import { styled } from '@mui/material/styles' -import { useTranslation } from 'next-i18next' -import { useRouter } from 'next/router' -import { Form, Formik, FormikConfig, FormikValues } from 'formik' -import { LoadingButton } from '@mui/lab' -import { Box, Button, Grid, Step, StepLabel, Stepper, useMediaQuery } from '@mui/material' -import { StepsContext } from './helpers/stepperContext' -import { OneTimeDonation } from 'gql/donations' -import { useSession } from 'next-auth/react' - -const PREFIX = 'FormikStepper' - -const classes = { - container: `${PREFIX}-container`, - stepIcon: `${PREFIX}-stepIcon`, -} - -const StyledStepper = styled('div')(() => ({ - [`& .${classes.stepIcon}`]: { - transform: 'scale(1.15)', - }, -})) - -export interface FormikStepProps - extends Pick, 'children' | 'validationSchema'> { - label?: string -} - -export function FormikStep({ children }: FormikStepProps) { - return <>{children} -} - -export type GenericFormProps = PropsWithChildren> - -export function FormikStepper({ children, ...props }: GenericFormProps) { - const childrenArray = React.Children.toArray(children) as React.ReactElement[] - const { step, setStep } = useContext(StepsContext) - const router = useRouter() - const mobile = useMediaQuery('(max-width:568px)') - useEffect(() => { - router.query.success === 'false' || router.query.success === 'true' ? setStep(3) : null - }, [router.query.success]) - const currentChild = childrenArray[step] - const { data: session } = useSession() - - function isLoginStep() { - return step === childrenArray.length - 3 - } - - function isLastStep() { - return step === childrenArray.length - 2 - } - - function isLogged() { - if (!session?.accessToken) { - return false - } - - return true - } - const { t } = useTranslation('one-time-donation') - const hideNextButton = useCallback( - (isAnonymous: boolean) => { - if (isLoginStep() && !isLogged() && !isAnonymous) { - return true - } - return false - }, - [step], - ) - return ( - { - if (isLastStep()) { - values.isAnonymous = isLogged() === false ? true : values.isAnonymous ?? !isLogged() - await props.onSubmit(values, helpers) - } else { - setStep((s) => s + 1) - helpers.setTouched({}) - } - }} - validateOnMount - validateOnBlur> - {({ isSubmitting, handleSubmit, isValid, values: { isAnonymous } }) => ( -
- - - {childrenArray.map((child, index) => ( - - - {!mobile && child.props.label} - - - ))} - - - {currentChild} - {/* Controls of the form */} - {step === 3 ? null : ( - - - - - - - {isSubmitting ? 'Потвърждение' : isLastStep() ? t('btns.end') : t('btns.next')} - - - - )} -
- )} -
- ) -} diff --git a/src/components/one-time-donation/LoggedUserDialog.tsx b/src/components/one-time-donation/LoggedUserDialog.tsx deleted file mode 100644 index afda80c86..000000000 --- a/src/components/one-time-donation/LoggedUserDialog.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { useSession } from 'next-auth/react' -import { useTranslation } from 'next-i18next' -import { Grid, Typography } from '@mui/material' -import theme from 'common/theme' - -function LoggedUserDialog() { - const { t } = useTranslation('one-time-donation') - const { data: session } = useSession() - - return ( - - - - {t('second-step.logged-user')} - - - - {session && session.user ? ( - - {t('second-step.info-logged-user', { - fullName: session.user.name, - email: session.user.email, - })} - - ) : ( - '' - )} - - - ) -} - -export default LoggedUserDialog diff --git a/src/components/one-time-donation/LoginForm.tsx b/src/components/one-time-donation/LoginForm.tsx deleted file mode 100644 index 615af598a..000000000 --- a/src/components/one-time-donation/LoginForm.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useContext, useState } from 'react' -import { useTranslation } from 'next-i18next' -import { useFormikContext } from 'formik' -import { Box, Button, CircularProgress, Grid, Typography } from '@mui/material' -import theme from 'common/theme' -import Google from 'common/icons/Google' -import { OneTimeDonation } from 'gql/donations' -import EmailField from '../common/form/EmailField' -import { signIn } from 'next-auth/react' -import { StepsContext } from './helpers/stepperContext' -import { AlertStore } from 'stores/AlertStore' -import PasswordField from 'components/common/form/PasswordField' - -const onGoogleLogin = () => { - const resp = signIn('google') -} - -function LoginForm() { - const { t } = useTranslation('one-time-donation') - const [loading, setLoading] = useState(false) - const { setStep } = useContext(StepsContext) - const formik = useFormikContext() - - const onClick = async () => { - try { - setLoading(true) - - const resp = await signIn<'credentials'>('credentials', { - email: formik.values.loginEmail, - password: formik.values.loginPassword, - redirect: false, - }) - if (resp?.error) { - throw new Error(resp.error) - } - if (resp?.ok) { - setLoading(false) - formik.setFieldValue('isAnonymous', false) - setStep(2) - AlertStore.show(t('auth:alerts.welcome'), 'success') - } - } catch (error) { - console.error(error) - setLoading(false) - AlertStore.show(t('auth:alerts.invalid-login'), 'error') - } - } - return ( - - - - {t('second-step.login')} - - - - - - - - - - - - ) -} - -export default LoginForm diff --git a/src/components/one-time-donation/OneTimeDonationPage.tsx b/src/components/one-time-donation/OneTimeDonationPage.tsx deleted file mode 100644 index 1a046fdeb..000000000 --- a/src/components/one-time-donation/OneTimeDonationPage.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import Link from 'next/link' -import Image from 'next/image' -import { styled } from '@mui/material/styles' -import { Box, Grid, Typography, useMediaQuery } from '@mui/material' - -import theme from 'common/theme' -import { routes } from 'common/routes' -import { - backgroundCampaignPictureUrl, - beneficiaryCampaignPictureUrl, -} from 'common/util/campaignImageUrls' -import Layout from 'components/layout/Layout' -import { useViewCampaign } from 'common/hooks/campaigns' -import CenteredSpinner from 'components/common/CenteredSpinner' - -import DonationStepper from './Steps' -// import RadioAccordionGroup, { testRadioOptions } from 'components/donation-flow/common/RadioAccordionGroup' -// import RadioCardGroup, { testRadioOptions } from 'components/donation-flow/common/RadioCardGroup' -// import PaymentDetailsStripeForm from 'components/donations/stripe/PaymentDetailsStripeForm' - -const PREFIX = 'OneTimeDonationPage' - -const classes = { - bannerWrapper: `${PREFIX}-bannerWrapper`, - banner: `${PREFIX}-banner`, - beneficiaryAvatarWrapper: `${PREFIX}-beneficiaryAvatarWrapper`, - beneficiaryAvatar: `${PREFIX}-beneficiaryAvatar`, - stepperWrapper: `${PREFIX}-stepperWrapper`, -} - -const StyledLayout = styled(Layout)(({ theme }) => ({ - [`& .${classes.bannerWrapper}`]: { - '& span': { - position: 'inherit !important', - }, - }, - - [`& .${classes.banner}`]: { - zIndex: -1, - maxHeight: '350px !important', - marginTop: `${theme.spacing(10)} !important`, - [theme.breakpoints.up('md')]: { - marginTop: `${theme.spacing(14)} !important`, - }, - objectFit: 'cover', - }, - - [`& .${classes.beneficiaryAvatarWrapper}`]: { - textAlign: 'center', - [theme.breakpoints.up('md')]: { - textAlign: 'center', - }, - }, - - [`& .${classes.beneficiaryAvatar}`]: { - borderRadius: '50%', - border: `4px solid ${theme.palette.common.white} !important`, - textAlign: 'center', - }, - - [`& .${classes.stepperWrapper}`]: { - gap: theme.spacing(2), - display: 'grid', - }, -})) - -const scrollWindow = () => { - const bannerWrapper = document.getElementsByClassName(classes.bannerWrapper)[0] - const avatarWrapper = document.getElementsByClassName(classes.beneficiaryAvatarWrapper)[0] - let calculatedScrollY = 0 - if (bannerWrapper && avatarWrapper) { - calculatedScrollY = bannerWrapper.clientHeight + avatarWrapper.clientHeight / 2 - } - window.scrollTo({ top: calculatedScrollY, behavior: 'smooth' }) -} - -export default function OneTimeDonation({ slug }: { slug: string }) { - const { data, isLoading } = useViewCampaign(slug) - const matches = useMediaQuery('sm') - // const paymentIntentMutation = useCreatePaymentIntent({ - // amount: 100, - // currency: 'BGN', - // }) - // useEffect(() => { - // paymentIntentMutation.mutate() - // }, []) - if (isLoading || !data) return - const { campaign } = data - - const bannerSource = backgroundCampaignPictureUrl(campaign) - const beneficiaryAvatarSource = beneficiaryCampaignPictureUrl(campaign) - - return ( - - - - {/* A11Y TODO: Translate alt text */} - Campaign banner image - - - - - - - - - {campaign.title} - - - {/* {paymentIntentMutation.isLoading ? ( - - ) : ( - - )} */} - - {/* */} - {/* */} - - - - ) -} diff --git a/src/components/one-time-donation/RegisterDialog.tsx b/src/components/one-time-donation/RegisterDialog.tsx deleted file mode 100644 index 564277aaf..000000000 --- a/src/components/one-time-donation/RegisterDialog.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Button, CircularProgress, Grid, Typography } from '@mui/material' -import React, { useContext, useState } from 'react' -import { signIn } from 'next-auth/react' -import { useTranslation } from 'next-i18next' -import theme from 'common/theme' -import { useRegister } from 'service/auth' -import { AlertStore } from 'stores/AlertStore' -import FormTextField from 'components/common/form/FormTextField' -import PasswordField from 'components/common/form/PasswordField' -import EmailField from 'components/common/form/EmailField' -import { useFormikContext } from 'formik' -import { OneTimeDonation } from 'gql/donations' -import { RegisterFormData } from 'components/auth/register/RegisterForm' -import { StepsContext } from './helpers/stepperContext' - -export default function RegisterForm() { - const { t } = useTranslation() - const [loading, setLoading] = useState(false) - const { mutateAsync: register } = useRegister() - const formik = useFormikContext() - const { setStep } = useContext(StepsContext) - - const values: RegisterFormData = { - firstName: formik.values.registerFirstName as string, - lastName: formik.values.registerLastName as string, - email: formik.values.registerEmail as string, - password: formik.values.registerPassword as string, - confirmPassword: formik.values.confirmPassword as string, - terms: formik.values.terms as boolean, - gdpr: formik.values.gdpr as boolean, - } - const onClick = async () => { - try { - setLoading(true) - - // Register in Keycloak - await register(values) - - // Authenticate - const resp = await signIn<'credentials'>('credentials', { - email: values.email, - password: values.password, - redirect: false, - }) - if (resp?.error) { - throw new Error(resp.error) - } - if (resp?.ok) { - setLoading(false) - AlertStore.show(t('auth:alerts.welcome'), 'success') - formik.setFieldValue('isAnonymous', false) - setStep(2) - } - } catch (error) { - console.error(error) - setLoading(false) - AlertStore.show(t('auth:alerts.invalid-login'), 'error') - } - } - - return ( - <> - - {t('one-time-donation:second-step.new-create-profile')} - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/src/components/one-time-donation/Steps.tsx b/src/components/one-time-donation/Steps.tsx deleted file mode 100644 index 63b77518c..000000000 --- a/src/components/one-time-donation/Steps.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import * as React from 'react' -import { useTranslation } from 'next-i18next' -import { useRouter } from 'next/router' -import { useSession } from 'next-auth/react' -import { CircularProgress } from '@mui/material' -import { AxiosError } from 'axios' -import { FormikHelpers } from 'formik' - -import { CardRegion, PaymentProvider } from 'gql/donations.enums' -import { OneTimeDonation, DonationStep as StepType } from 'gql/donations' -import { createDonationWish } from 'service/donationWish' -import { ApiErrors, isAxiosError, matchValidator } from 'service/apiErrors' -import { useCurrentPerson } from 'common/util/useCurrentPerson' -import CenteredSpinner from 'components/common/CenteredSpinner' -import { useDonationSession } from 'common/hooks/donation' -import { useViewCampaign } from 'common/hooks/campaigns' -import { baseUrl, routes } from 'common/routes' - -import FirstStep from './steps/FirstStep' -import SecondStep from './steps/SecondStep' -import ThirdStep from './steps/ThirdStep' -import Success from './steps/Success' -import Fail from './steps/Fail' -import { FormikStep, FormikStepper } from './FormikStepper' -import { validateFirst, validateSecond, validateThird } from './helpers/validation-schema' -import { StepsContext } from './helpers/stepperContext' - -const initialValues: OneTimeDonation = { - message: '', - isAnonymous: false, - amount: '', - amountWithFees: 0, - cardIncludeFees: false, - cardRegion: CardRegion.EU, - otherAmount: 0, - personsFirstName: '', - personsLastName: '', - personsEmail: '', - personsPhone: '', - payment: 'card', - loginEmail: '', - loginPassword: '', - registerEmail: '', - registerLastName: '', - registerFirstName: '', - registerPassword: '', - isRecurring: false, -} -interface DonationStepperProps { - onStepChange: () => void -} - -export default function DonationStepper({ onStepChange }: DonationStepperProps) { - const { t, i18n } = useTranslation('one-time-donation') - const router = useRouter() - const success = router.query.success === 'true' ? true : false - initialValues.amount = (router.query.price as string) || '' - const slug = String(router.query.slug) - const { data, isLoading } = useViewCampaign(slug) - const mutation = useDonationSession() - const { data: session } = useSession() - const { data: { user: person } = { user: null } } = useCurrentPerson() - if (isLoading || !data) return - const { campaign } = data - - function isLogged() { - return session && session.accessToken ? true : false - } - - initialValues.isAnonymous = !isLogged() - initialValues.isRecurring = false - - const userEmail = session?.user?.email - const donate = React.useCallback( - async (amount?: number, values?: OneTimeDonation) => { - const { data } = await mutation.mutateAsync({ - mode: values?.isRecurring ? 'subscription' : 'payment', - amount, - campaignId: campaign.id, - personId: person ? person?.id : '', - firstName: values?.personsFirstName ? values.personsFirstName : 'Anonymous', - lastName: values?.personsLastName ? values.personsLastName : 'Donor', - personEmail: values?.personsEmail ? values.personsEmail : userEmail, - isAnonymous: values?.isAnonymous !== undefined ? values.isAnonymous : true, - phone: values?.personsPhone ? values.personsPhone : null, - successUrl: `${baseUrl}/${i18n.language}/${routes.campaigns.oneTimeDonation( - campaign.slug, - )}?success=true`, - cancelUrl: `${baseUrl}/${i18n.language}/${routes.campaigns.oneTimeDonation( - campaign.slug, - )}?success=false`, - message: values?.message, - }) - if (values?.payment === PaymentProvider.bank) { - // Do not redirect for bank payments - return - } - if (data.session.url) { - //send the user to payment provider - window.location.href = data.session.url - } - }, - [mutation], - ) - - const onSubmit = async ( - values: OneTimeDonation, - { setFieldError, resetForm }: FormikHelpers, - ) => { - try { - if (values?.payment === PaymentProvider.bank) { - if (values?.message) { - await createDonationWish({ - message: values.message, - campaignId: campaign.id, - personId: !values.isAnonymous && person?.id ? person.id : null, - }) - } - router.push(`${baseUrl}${routes.campaigns.oneTimeDonation(campaign.slug)}?success=true`) - return - } - - const data = { - currency: campaign.currency, - amount: Math.round(values.amountWithFees), - } - await donate(data.amount, values) - resetForm() - } catch (error) { - if (isAxiosError(error)) { - const { response } = error as AxiosError - response?.data.message.map(({ property, constraints }) => { - setFieldError(property, t(matchValidator(constraints))) - }) - } - } - } - const steps: StepType[] = [ - { - label: 'amount', - component: , - validate: validateFirst, - }, - { - label: 'personal-profile', - component: , - validate: validateSecond, - }, - { - label: 'wish', - component: , - validate: validateThird, - }, - { - label: 'payment', - component: success ? : , - validate: null, - }, - ] - const [step, setStep] = React.useState(0) - - React.useEffect(() => { - onStepChange() - }, [step]) - - return ( - - {isLoading ? ( - - ) : ( - - {steps.map(({ label, component, validate }) => ( - - {component} - - ))} - - )} - - ) -} diff --git a/src/components/one-time-donation/helpers/paypalDonationButton.tsx b/src/components/one-time-donation/helpers/paypalDonationButton.tsx deleted file mode 100644 index 125980641..000000000 --- a/src/components/one-time-donation/helpers/paypalDonationButton.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useEffect } from 'react' - -import { PayPalButtons, usePayPalScriptReducer } from '@paypal/react-paypal-js' -import { Box } from '@mui/material' -import { AlertStore } from 'stores/AlertStore' -import { useTranslation } from 'next-i18next' - -export type PaypalDonationButtonOptions = { - campaignId: string - amount: number - currency: string -} - -// Custom component to wrap the PayPalButtons and handle amount¤cy changes -export default function PaypalDonationButton({ - campaignId, - amount, - currency, -}: PaypalDonationButtonOptions) { - // usePayPalScriptReducer can be used only inside children of PayPalScriptProviders - // This is the main reason to wrap the PayPalButtons in a new component - const [{ options, isPending }, dispatch] = usePayPalScriptReducer() - - const { t } = useTranslation('one-time-donation') - - useEffect(() => { - dispatch({ - type: 'resetOptions', - value: { - ...options, - currency: currency, - }, - }) - }, [amount, currency]) - - return ( - - {isPending ?
: null} - { - return actions.order.create({ - purchase_units: [ - { - amount: { - value: amount.toString(), - currency_code: currency, - breakdown: { - item_total: { - currency_code: currency, - value: amount.toString(), - }, - }, - }, - custom_id: campaignId, // Paypal will send this in the webhook too - description: 'donation for campaign: ' + campaignId, - items: [ - { - category: 'DONATION', - name: 'Име на кампания', - description: 'Дарение за кампания', - quantity: '1', - unit_amount: { - currency_code: currency, - value: amount.toString(), - }, - }, - ], - }, - ], - }) - }} - onApprove={(data, actions) => { - if (actions.order) { - return actions.order.capture().then((details) => { - AlertStore.show(t('alerts.success'), 'success') - }) - } else { - return new Promise(() => { - AlertStore.show(t('alerts.error'), 'error') - }) - } - }} - /> - - ) -} diff --git a/src/components/one-time-donation/helpers/stepperContext.ts b/src/components/one-time-donation/helpers/stepperContext.ts deleted file mode 100644 index 327981246..000000000 --- a/src/components/one-time-donation/helpers/stepperContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext } from 'react' -import { CampaignResponse } from 'gql/campaigns' - -type Steps = { - step: number - setStep: React.Dispatch> - campaign: CampaignResponse -} -export const StepsContext = createContext({} as Steps) diff --git a/src/components/one-time-donation/helpers/stripe-fee-calculator.ts b/src/components/one-time-donation/helpers/stripe-fee-calculator.ts deleted file mode 100644 index ed71ff2f2..000000000 --- a/src/components/one-time-donation/helpers/stripe-fee-calculator.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { CardRegion } from 'gql/donations.enums' - -/** - * Calculates total charge amount in stotinki so that donation of netAmount is received after deducting Stripe fees - * References: - * - applied fees https://stripe.com/en-bg/pricing - * - formula for including fees: https://support.stripe.com/questions/passing-the-stripe-fee-on-to-customers - * - testing with International cards: https://stripe.com/docs/testing#international-cards - * @param netAmount expected in stotinki - * @returns - */ -export function stripeIncludeFeeCalculator(netAmount: number, region: CardRegion) { - switch (region) { - case CardRegion.EU: { - return stripeIncludeFeeCalculatorEU(netAmount) - } - case CardRegion.UK: { - return stripeIncludeFeeCalculatorUK(netAmount) - } - case CardRegion.Other: { - return stripeIncludeFeeCalculatorOther(netAmount) - } - } -} - -export function stripeIncludeFeeCalculatorEU(netAmount: number) { - return (netAmount + 50) / (1 - 0.012) -} - -export function stripeIncludeFeeCalculatorUK(netAmount: number) { - return (netAmount + 50) / (1 - 0.025) -} - -export function stripeIncludeFeeCalculatorOther(netAmount: number) { - return (netAmount + 50) / (1 - 0.029) -} - -/** - * Calculates Stripe fees based on card region - * References: - * - applied fees https://stripe.com/en-bg/pricing - * - formula for including fees: https://support.stripe.com/questions/passing-the-stripe-fee-on-to-customers - * - testing with International cards: https://stripe.com/docs/testing#international-cards - * @param chargedAmount expected in stotinki - * @returns - */ -export function stripeFeeCalculator(chargedAmount: number, region: CardRegion) { - switch (region) { - case CardRegion.EU: { - return chargedAmount * 0.012 + 50 - } - case CardRegion.UK: { - return chargedAmount * 0.025 + 50 - } - case CardRegion.Other: { - return chargedAmount * 0.029 + 50 - } - } -} diff --git a/src/components/one-time-donation/helpers/validation-schema.ts b/src/components/one-time-donation/helpers/validation-schema.ts deleted file mode 100644 index ed5f09e20..000000000 --- a/src/components/one-time-donation/helpers/validation-schema.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as yup from 'yup' -import { name, phone, email, password } from 'common/form/validation' -import { FirstStep, SecondStep, ThirdStep } from 'gql/donations' - -export const validateFirst: yup.SchemaOf = yup - .object() - .defined() - .shape({ - payment: yup.string().required().oneOf(['card', 'bank']), - amount: yup.string().when('payment', { - is: 'card', - // Here we should fetch the possible payments to put into the oneOf, but it's not that important - then: yup.string().required(), - }), - otherAmount: yup.number().when('amount', { - is: 'other', - then: yup.number().min(1, 'one-time-donation:errors-fields.other-amount').required(), - }), - }) - -export const validateSecond: yup.SchemaOf = yup.object().defined().shape({ - isAnonymous: yup.boolean().required(), - personsEmail: email.notRequired(), - personsFirstName: name.notRequired(), - personsLastName: name.notRequired(), - personsPhone: phone.notRequired(), - loginEmail: email.notRequired(), - loginPassword: password.notRequired(), - registerEmail: email.notRequired(), - registerFirstName: yup.string().notRequired(), - registerLastName: yup.string().notRequired(), - registerPassword: password.notRequired(), -}) - -export const validateThird: yup.SchemaOf = yup.object().defined().shape({ - message: yup.string().notRequired(), -}) diff --git a/src/components/one-time-donation/steps/Fail.tsx b/src/components/one-time-donation/steps/Fail.tsx deleted file mode 100644 index 86e92d0ef..000000000 --- a/src/components/one-time-donation/steps/Fail.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useContext, useEffect } from 'react' -import { useTranslation } from 'next-i18next' -import { useRouter } from 'next/router' -import { Grid, Typography, Button } from '@mui/material' -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' -import theme from 'common/theme' -import { routes } from 'common/routes' -import LinkButton from 'components/common/LinkButton' -import { StepsContext } from '../helpers/stepperContext' - -type Props = { - campaignSlug: string -} -export default function Fail({ campaignSlug }: Props) { - const { t } = useTranslation('one-time-donation') - const { setStep } = useContext(StepsContext) - const router = useRouter() - // Clear query so that the first step renders instead of success or fail page - useEffect(() => { - router.push(`${router.asPath.split('?')[0]}`) - }, []) - return ( - - - - - - - - {t('fail.title')} - - - - - - - - - - {t('fail.btn-back-to-campaign')} - - - - - {t('fail.btn-connect')} - - - - - ) -} diff --git a/src/components/one-time-donation/steps/FirstStep.tsx b/src/components/one-time-donation/steps/FirstStep.tsx deleted file mode 100644 index b363cdfc2..000000000 --- a/src/components/one-time-donation/steps/FirstStep.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import React, { useContext, useEffect } from 'react' -import { styled } from '@mui/material/styles' -import { Trans, useTranslation } from 'next-i18next' -import { useField, useFormikContext } from 'formik' -import { Box, Collapse, Divider, Grid, InputAdornment, List, Typography } from '@mui/material' -import theme from 'common/theme' -import { useSinglePriceList } from 'common/hooks/donation' -import RadioButtonGroup from 'components/common/form/RadioButtonGroup' -import { moneyPublic, moneyPublicDecimals2, toMoney } from 'common/util/money' -import { ibanNumber } from 'common/iban' -import { CopyTextButton } from 'components/common/CopyTextButton' -import { StepsContext } from '../helpers/stepperContext' -import FormTextField from 'components/common/form/FormTextField' -import { useMediaQuery } from '@mui/material' -import { OneTimeDonation } from 'gql/donations' -import ExternalLink from 'components/common/ExternalLink' -import { stripeFeeCalculator, stripeIncludeFeeCalculator } from '../helpers/stripe-fee-calculator' -import CheckboxField from 'components/common/form/CheckboxField' -import FormSelectField from 'components/common/form/FormSelectField' -import { CardRegion } from 'gql/donations.enums' -import { PayPalScriptProvider } from '@paypal/react-paypal-js' -import { isAdmin } from 'common/util/roles' -import { useSession } from 'next-auth/react' -import getConfig from 'next/config' - -import dynamic from 'next/dynamic' -const PaypalDonationButton = dynamic(() => import('../helpers/paypalDonationButton'), { - ssr: false, -}) - -const PREFIX = 'FirstStep' - -const classes = { - divider: `${PREFIX}-divider`, -} - -// TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. -const Root = styled('div')(() => ({ - [`& .${classes.divider}`]: { - border: '1px solid #000000', - }, -})) - -export default function FirstStep() { - const { data: session } = useSession() - const { data: oneTimePrices } = useSinglePriceList() - const { t } = useTranslation('one-time-donation') - const mobile = useMediaQuery('(max-width:600px)') - const paymentOptions = [ - { value: 'card', label: t('third-step.card') }, - { value: 'paypal', label: 'PayPal', hidden: !isAdmin(session) }, - { value: 'bank', label: t('third-step.bank-payment') }, - ] - - const [paymentField] = useField('payment') - const [amount] = useField('amount') - const [otherAmount] = useField('otherAmount') - const [amountWithFees] = useField('amountWithFees') - const [amountWithoutFees] = useField('amountWithoutFees') - - const formik = useFormikContext() - - const { campaign } = useContext(StepsContext) - const bankAccountInfo = { - owner: t('third-step.owner'), - bank: t('third-step.bank'), - iban: ibanNumber, - } - - useEffect(() => { - const chosenAmount = - amount.value === 'other' ? toMoney(formik.values.otherAmount) : Number(formik.values.amount) - - if (formik.values.cardIncludeFees) { - formik.setFieldValue('amountWithoutFees', chosenAmount) - formik.setFieldValue( - 'amountWithFees', - stripeIncludeFeeCalculator(chosenAmount, formik.values.cardRegion), - ) - } else { - formik.setFieldValue( - 'amountWithoutFees', - chosenAmount - stripeFeeCalculator(chosenAmount, formik.values.cardRegion), - ) - formik.setFieldValue('amountWithFees', chosenAmount) - } - }, [ - formik.values.otherAmount, - formik.values.amount, - formik.values.cardIncludeFees, - formik.values.cardRegion, - formik.values.isRecurring, - ]) - - function isLogged() { - return session && session.accessToken ? true : false - } - - return ( - - {t('third-step.title')} - - option.hidden != true).length} - options={paymentOptions} - /> - - - - - {t('third-step.bank-details')} - - - {t('third-step.bank-instructions1')} - - - {t('third-step.bank-instructions2')} - - - - - {bankAccountInfo.owner} - - - - {bankAccountInfo.bank} - - - - {ibanNumber} - - - - - - {t('third-step.reason-donation')} - - - - - - {campaign.paymentReference} - - - - - - {t('third-step.message-warning')} - - - - - {t('third-step.card-fees')} - - https://stripe.com/en-bg/pricing - - - - - {t('first-step.amount')} - - - Number(a.unit_amount) - Number(b.unit_amount)) - .map((v) => ({ - label: moneyPublic(Number(v.unit_amount)), - value: String(Number(v.unit_amount)), - })) - .concat({ label: t('first-step.other'), value: 'other' }) || [] - } - /> - - - - {t('first-step.BGN')} - - ), - }} - /> - - - {amount.value ? ( - - - - {t('third-step.card-include-fees')} - } - /> - - - - - - - - {t('third-step.recurring-donation')} - } - /> - - - ) : null} - - - - - - - - Note 1: This is a test Paypal implementation visible only to logged users with admin - rights. Using real cards will not charge any money. Note 2: Paypal transaction fee - is 3.4% + 0.35 euro cents. - - - - - - {t('first-step.BGN')} - - ), - }} - /> - - - - - - - - - - ) -} diff --git a/src/components/one-time-donation/steps/SecondStep.tsx b/src/components/one-time-donation/steps/SecondStep.tsx deleted file mode 100644 index e32111432..000000000 --- a/src/components/one-time-donation/steps/SecondStep.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { TabContext, TabList } from '@mui/lab' -import TabPanel from '@mui/lab/TabPanel' -import { Box, Tab, Typography, useMediaQuery } from '@mui/material' -import { OneTimeDonation } from 'gql/donations' -import { useSession } from 'next-auth/react' -import { useTranslation } from 'next-i18next' -import React, { useState } from 'react' -import AnonymousMenu from '../AnonymousForm' -import LoggedUserDialog from '../LoggedUserDialog' -import LoginForm from '../LoginForm' -import RegisterForm from '../RegisterDialog' -import { useFormikContext } from 'formik' - -enum Tabs { - Login = '1', - Register = '2', - Anonymous = '3', -} -export default function SecondStep() { - const { t } = useTranslation('one-time-donation') - const mobile = useMediaQuery('(max-width:575px)') - const { data: session } = useSession() - - const [value, setValue] = useState('1') - const formik = useFormikContext() - const handleChange = (event: React.SyntheticEvent, newTab: string) => { - if (newTab === Tabs.Anonymous) { - formik.setFieldValue('isAnonymous', true) - } else { - formik.setFieldValue('isAnonymous', false) - } - setValue(newTab) - } - - return ( - - {t('step-labels.personal-profile')} - {t('second-step.intro-text')} - - - - - {formik.values.isRecurring ? null : ( - - )} - - - - {session && session.accessToken ? : } - - - - - {formik.values.isRecurring ? null : ( - - - - )} - - ) -} diff --git a/src/components/one-time-donation/steps/Success.tsx b/src/components/one-time-donation/steps/Success.tsx deleted file mode 100644 index 3b15f6515..000000000 --- a/src/components/one-time-donation/steps/Success.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useTranslation } from 'next-i18next' -import { Grid, Typography } from '@mui/material' -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' -import { routes } from 'common/routes' -import LinkButton from 'components/common/LinkButton' -import ExternalLinkButton from 'components/common/ExternalLinkButton' -import { useField } from 'formik' -import { PaymentProvider } from 'gql/donations.enums' - -type Props = { - campaignSlug: string - donationId?: string -} -export default function Success({ campaignSlug, donationId }: Props) { - const { t } = useTranslation('one-time-donation') - const [field] = useField('payment') - return ( - - - - - - - - {(field.value === PaymentProvider.bank && t('success.title-bank')) || - t('success.title')} - - - - - {(field.value === PaymentProvider.bank && t('success.subtitle-bank')) || - t('success.subtitle')} - - - - {t('success.share-to')} - - - {t('success.say-to-us')} - - - - {donationId && ( - - - {t('success.btn-generate')} - - - )} - - - {t('success.btn-back-to-campaign')} - - - - - {t('success.btn-say-to-us')} - - - - - {t('success.btn-other-campaign')} - - - - - ) -} diff --git a/src/components/one-time-donation/steps/ThirdStep.tsx b/src/components/one-time-donation/steps/ThirdStep.tsx deleted file mode 100644 index 750890298..000000000 --- a/src/components/one-time-donation/steps/ThirdStep.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useTranslation } from 'next-i18next' -import { Grid, Typography } from '@mui/material' -import theme from 'common/theme' -import FormTextField from 'components/common/form/FormTextField' - -export default function ThirdStep() { - const { t } = useTranslation('one-time-donation') - return ( - - - - {t('first-step.wish')} - - - - - - - ) -} diff --git a/src/components/support-us-form/SupportUsForm.tsx b/src/components/support-us-form/SupportUsForm.tsx index 9f85f6aed..b90da67f0 100644 --- a/src/components/support-us-form/SupportUsForm.tsx +++ b/src/components/support-us-form/SupportUsForm.tsx @@ -1,6 +1,5 @@ -import { useTranslation } from 'next-i18next' import React from 'react' -import { styled } from '@mui/material/styles' +import { useTranslation } from 'next-i18next' import { Grid, List, Typography, Divider } from '@mui/material' import theme from 'common/theme' @@ -12,19 +11,12 @@ const classes = { divider: `${PREFIX}-divider`, } -// TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. -const Root = styled('div')(() => ({ - [`& .${classes.divider}`]: { - border: '1px solid #000000', - }, -})) - export default function SupportUsForm() { - const { t } = useTranslation('one-time-donation') + const { t } = useTranslation('donation-flow') const bankAccountInfo = { - owner: t('third-step.owner'), - bank: t('third-step.bank'), + owner: t('step.payment-method.bank.owner'), + bank: t('step.payment-method.bank.bank'), iban: ibanNumber, } @@ -33,13 +25,13 @@ export default function SupportUsForm() { {t('support_us:support-info')} - {t('third-step.bank-details')} + {t('step.payment-method.bank.bank-details')} {bankAccountInfo.owner} {bankAccountInfo.bank} {ibanNumber} - {t('third-step.reason-donation')} + {t('step.payment-method.bank.reason-donation')} @@ -77,7 +69,7 @@ export default function SupportUsForm() { variant="contained" color="info" size="small" - label={t('third-step.btn-copy')} + label={t('step.payment-method.bank.btn-copy')} /> diff --git a/src/pages/campaigns/donation-old/[slug].tsx b/src/pages/campaigns/donation-old/[slug].tsx deleted file mode 100644 index 774dc9413..000000000 --- a/src/pages/campaigns/donation-old/[slug].tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { QueryClient, dehydrate } from '@tanstack/react-query' -import { GetServerSideProps, GetServerSidePropsContext } from 'next' -import { serverSideTranslations } from 'next-i18next/serverSideTranslations' - -import OneTimeDonation from 'components/one-time-donation/OneTimeDonationPage' -import { endpoints } from 'service/apiEndpoints' -import { queryFnFactory } from 'service/restRequests' -import { CampaignResponse } from 'gql/campaigns' - -export const getServerSideProps: GetServerSideProps = async (ctx: GetServerSidePropsContext) => { - const { slug } = ctx.query - const client = new QueryClient() - await client.prefetchQuery( - [endpoints.campaign.viewCampaign(slug as string)], - queryFnFactory(), - ) - return { - props: { - slug, - dehydratedState: dehydrate(client), - ...(await serverSideTranslations(ctx.locale ?? 'bg', [ - 'common', - 'auth', - 'validation', - 'campaigns', - 'one-time-donation', - ])), - }, - } -} - -export default OneTimeDonation diff --git a/src/pages/support_us.tsx b/src/pages/support_us.tsx index be093b62c..e7b79742e 100644 --- a/src/pages/support_us.tsx +++ b/src/pages/support_us.tsx @@ -5,11 +5,7 @@ import SupportUsFormPage from 'components/support-us-form/SupportUsPage' export const getStaticProps: GetStaticProps = async ({ locale }) => ({ props: { - ...(await serverSideTranslations(locale ?? 'bg', [ - 'common', - 'support_us', - 'one-time-donation', - ])), + ...(await serverSideTranslations(locale ?? 'bg', ['common', 'support_us', 'donation-flow'])), }, })