diff --git a/next.config.js b/next.config.js index 12f79d42..5f730dc6 100644 --- a/next.config.js +++ b/next.config.js @@ -37,7 +37,10 @@ const scheme = : "https"; let APPUrl; -if (process.env.NEXT_PUBLIC_VERCEL_ENV === "preview") { +if ( + process.env.NEXT_PUBLIC_VERCEL_ENV === "preview" && + process.env.DISABLE_VERCEL_REDIRECT !== "true" +) { APPUrl = `${scheme}://${process.env.VERCEL_URL}`; } else { APPUrl = process.env.APP_URL; diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 3cf51bfd..3d87e1d9 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -134,6 +134,7 @@ "billingAddress": "Contact Details", "treesInCountry": "{{treeCount}} trees, Plant-for-the-Planet", "fundingPaymentLabel": "Donate {{amount}}, Plant-for-the-Planet", + "pcashPaymentLabel": "Load {{amount}} PlanetCash, Plant-for-the-Planet", "bouquetPaymentLabel": "Donate {{amount}}, Plant-for-the-Planet", "errorOccurred": "Something went wrong. Please feel free to take a screenshot and email us at support@plant-for-the-planet.org.", "dedicatedTo": "Dedicated to", diff --git a/src/Common/Types/QueryParamContextInterface.ts b/src/Common/Types/QueryParamContextInterface.ts index 1648409d..e93aa8bf 100644 --- a/src/Common/Types/QueryParamContextInterface.ts +++ b/src/Common/Types/QueryParamContextInterface.ts @@ -109,8 +109,8 @@ export default interface QueryParamContextInterface { paymentSetupCountry: string; shouldSetPaymentDetails?: boolean; }) => Promise<void>; - isPlanetCashActive: boolean; - setIsPlanetCashActive: Dispatch<SetStateAction<boolean>>; + isPlanetCashActive: boolean | null; + setIsPlanetCashActive: Dispatch<SetStateAction<boolean | null>>; onBehalf: boolean; setOnBehalf: Dispatch<SetStateAction<boolean>>; onBehalfDonor: OnBehalfDonor; diff --git a/src/Donations/Components/DonationsForm.tsx b/src/Donations/Components/DonationsForm.tsx index cab3ba5d..b26266f7 100644 --- a/src/Donations/Components/DonationsForm.tsx +++ b/src/Donations/Components/DonationsForm.tsx @@ -68,6 +68,7 @@ function DonationsForm(): ReactElement { utmMedium, utmSource, isPackageWanted, + setPaymentRequest, } = React.useContext(QueryParamContext); const { t, i18n } = useTranslation(["common", "country", "donate"]); @@ -78,6 +79,16 @@ function DonationsForm(): ReactElement { React.useState(false); const router = useRouter(); + React.useEffect(() => { + setPaymentRequest(null); + }, []); + + React.useEffect(() => { + if (isPlanetCashActive) { + setPaymentRequest(null); + } + }, [isPlanetCashActive]); + React.useEffect(() => { setMinAmt(getMinimumAmountForCurrency(currency)); }, [currency]); @@ -241,6 +252,15 @@ function DonationsForm(): ReactElement { ), }); break; + case "planet-cash": + paymentLabel = t("pcashPaymentLabel", { + amount: getFormatedCurrency( + i18n.language, + currency, + paymentSetup.unitCost * quantity + ), + }); + break; case "bouquet": case "conservation": paymentLabel = t("bouquetPaymentLabel", { diff --git a/src/Donations/Components/PaymentsForm.tsx b/src/Donations/Components/PaymentsForm.tsx index de990ca3..0045585b 100644 --- a/src/Donations/Components/PaymentsForm.tsx +++ b/src/Donations/Components/PaymentsForm.tsx @@ -82,6 +82,7 @@ function PaymentsForm(): ReactElement { utmMedium, utmSource, isPackageWanted, + setPaymentRequest, } = React.useContext(QueryParamContext); const [stripePromise, setStripePromise] = @@ -100,6 +101,7 @@ function PaymentsForm(): ReactElement { React.useEffect(() => { setPaymentType("CARD"); + setPaymentRequest(null); }, []); const sofortCountries = ["AT", "BE", "DE", "IT", "NL", "ES"]; @@ -111,7 +113,7 @@ function PaymentsForm(): ReactElement { | string | PaymentMethod | PaypalApproveData - | PaypalErrorData, + | PaypalErrorData ) => { if (!paymentSetup || !donationID) { console.log("Missing payment options"); //TODOO - better error handling @@ -144,7 +146,7 @@ function PaymentsForm(): ReactElement { // Seems to work only for native pay. Should this be removed? const onPaymentFunction = async ( paymentMethod: PaymentMethod, - paymentRequest: PaymentRequest, + paymentRequest: PaymentRequest ) => { setPaymentType(paymentRequest._activeBackingLibraryName); //TODOO --_activeBackingLibraryName is a private variable? const gateway = "stripe"; @@ -289,7 +291,7 @@ function PaymentsForm(): ReactElement { query: { ...router.query, step: CONTACT }, }, undefined, - { shallow: true }, + { shallow: true } ); }} className="d-flex" @@ -420,7 +422,7 @@ function PaymentsForm(): ReactElement { totalCost={getFormatedCurrency( i18n.language, currency, - paymentSetup?.unitCost * quantity, + paymentSetup?.unitCost * quantity )} onPaymentFunction={(providerObject: PaymentMethod) => onSubmitPayment("stripe", "card", providerObject) diff --git a/src/Donations/Micros/PlanetCashSelector.tsx b/src/Donations/Micros/PlanetCashSelector.tsx index eadba14a..15cdeacb 100644 --- a/src/Donations/Micros/PlanetCashSelector.tsx +++ b/src/Donations/Micros/PlanetCashSelector.tsx @@ -19,7 +19,6 @@ const PlanetCashSelector: FC = () => { country, setcountry, frequency, - paymentRequest, } = useContext(QueryParamContext); const router = useRouter(); @@ -37,20 +36,28 @@ const PlanetCashSelector: FC = () => { }, [paymentSetup?.unitCost, quantity, setIsPlanetCashActive]); useEffect(() => { - // On Load If selected country is planetCash Country and balance is sufficient activate planetCash. - - if ( - country === profile?.planetCash?.country && - paymentSetup && - paymentSetup.unitCost * quantity <= - profile.planetCash.balance / 100 + profile.planetCash.creditLimit / 100 - ) { - setIsPlanetCashActive(true); - } - if (frequency !== "once") { + if (frequency !== "once" && isPlanetCashActive !== null) { setIsPlanetCashActive(false); + } else { + if ( + isPlanetCashActive === null && + country === profile?.planetCash?.country && + paymentSetup && + paymentSetup.unitCost * quantity <= + (profile.planetCash.balance + profile.planetCash.creditLimit) / 100 + ) { + setIsPlanetCashActive(true); + } } - }, [paymentRequest, frequency]); + }, [ + country, + profile, + paymentSetup, + quantity, + frequency, + isPlanetCashActive, + frequency, + ]); useEffect(() => { // This is done to lock the transaction with PlanetCash in a single currency. @@ -208,11 +215,13 @@ const PlanetCashSelector: FC = () => { </div> <div title={disabledReason() ? disabledReason() : ""}> <ToggleSwitch - checked={isPlanetCashActive} + checked={isPlanetCashActive === true} disabled={shouldPlanetCashDisable()} - onChange={() => - setIsPlanetCashActive((isPlanetCashActive) => !isPlanetCashActive) - } + onChange={() => { + setIsPlanetCashActive( + (isPlanetCashActive) => !isPlanetCashActive + ); + }} /> </div> </div> diff --git a/src/Donations/PaymentMethods/PaymentMethodTabs.tsx b/src/Donations/PaymentMethods/PaymentMethodTabs.tsx index 9e735a13..f60a22fd 100644 --- a/src/Donations/PaymentMethods/PaymentMethodTabs.tsx +++ b/src/Donations/PaymentMethods/PaymentMethodTabs.tsx @@ -111,6 +111,15 @@ export default function PaymentMethodTabs({ ), }); break; + case "planet-cash": + paymentLabel = t("pcashPaymentLabel", { + amount: getFormatedCurrency( + i18n.language, + currency, + paymentSetup.unitCost * quantity + ), + }); + break; case "bouquet": case "conservation": paymentLabel = t("bouquetPaymentLabel", { diff --git a/src/Donations/PaymentMethods/PaymentRequestCustomButton.tsx b/src/Donations/PaymentMethods/PaymentRequestCustomButton.tsx index 376175f0..11682608 100644 --- a/src/Donations/PaymentMethods/PaymentRequestCustomButton.tsx +++ b/src/Donations/PaymentMethods/PaymentRequestCustomButton.tsx @@ -15,6 +15,7 @@ import { } from "@stripe/stripe-js/types/stripe-js/payment-request"; import { PaymentMethod } from "@stripe/stripe-js/types/api/payment-methods"; import { Stripe } from "@stripe/stripe-js/types/stripe-js/stripe"; +import Skeleton from "@mui/material/Skeleton"; interface PaymentButtonProps { country: string; @@ -50,10 +51,8 @@ export const PaymentRequestCustomButton = ({ const stripe = useStripe(); const [canMakePayment, setCanMakePayment] = useState(false); const [paymentLoading, setPaymentLoading] = useState(false); - - useEffect(() => { - setPaymentRequest(null); - }, []); + // Tracks if native pay buttons were shown at least once to prevent layout jerks + const [wasNativePayInit, setWasNativePayInit] = useState(false); useEffect(() => { if ( @@ -75,17 +74,16 @@ export const PaymentRequestCustomButton = ({ pr.canMakePayment().then((result) => { if (result) { setPaymentRequest(pr); + setWasNativePayInit(true); } }); } }, [stripe, paymentRequest, country, currency, amount]); useEffect(() => { - if (stripe && paymentRequest) { - setPaymentRequest(null); - setCanMakePayment(false); - setPaymentLoading(false); - } + setPaymentRequest(null); + setCanMakePayment(false); + setPaymentLoading(false); }, [country, currency, amount]); useEffect(() => { @@ -121,7 +119,10 @@ export const PaymentRequestCustomButton = ({ ); } return () => { - if (paymentRequest && !paymentLoading) { + if ( + paymentRequest && + paymentRequest.hasRegisteredListener("paymentmethod") + ) { paymentRequest.off("paymentmethod", () => { setPaymentLoading(false); }); @@ -194,8 +195,25 @@ export const PaymentRequestCustomButton = ({ <div className="separator-text mb-10">{t("or")}</div> )} </div> - ) : null - ) : null} + ) : ( + <></> + ) + ) : wasNativePayInit ? ( + //Loader shown if native pay was initiated at least once to avoid a jerky effect when payment details change + <div className="w-100"> + <Skeleton + className="mb-10" + variant="rectangular" + width={"100%"} + height={40} + /> + {!isPaymentPage && ( + <div className="separator-text mb-10">{t("or")}</div> + )} + </div> + ) : ( + <></> + )} {!isPaymentPage && ( <button diff --git a/src/Layout/QueryParamContext.tsx b/src/Layout/QueryParamContext.tsx index a738e250..0e068f50 100644 --- a/src/Layout/QueryParamContext.tsx +++ b/src/Layout/QueryParamContext.tsx @@ -138,7 +138,9 @@ const QueryParamProvider: FC = ({ children }) => { const [transferDetails, setTransferDetails] = useState<BankTransferDetails | null>(null); - const [isPlanetCashActive, setIsPlanetCashActive] = useState(false); + const [isPlanetCashActive, setIsPlanetCashActive] = useState<boolean | null>( + null + ); // Only used when planetCash is active const [onBehalf, setOnBehalf] = useState(false); @@ -151,7 +153,7 @@ const QueryParamProvider: FC = ({ children }) => { const [donation, setDonation] = useState<Donation | null>(null); const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>( - null, + null ); const [errors, setErrors] = React.useState<SerializedError[] | null>(null); @@ -163,8 +165,9 @@ const QueryParamProvider: FC = ({ children }) => { setshowErrorCard, shouldQueryParamAdd: false, }; - const response: { data: Record<string, string> } = - await apiRequest(requestParams); + const response: { data: Record<string, string> } = await apiRequest( + requestParams + ); setEnabledCurrencies(response.data); } catch (err) { console.log(err); @@ -198,7 +201,7 @@ const QueryParamProvider: FC = ({ children }) => { function testURL(url: string) { const pattern = new RegExp( - /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g, + /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g ); // regex source https://tutorial.eyehunts.com/js/url-regex-validation-javascript-example-code/ return !!pattern.test(url); @@ -242,7 +245,7 @@ const QueryParamProvider: FC = ({ children }) => { const projects = response.data as Project[]; if (projects) { const allowedDonationsProjects = projects.filter( - (project) => project.properties.allowDonations === true, + (project) => project.properties.allowDonations === true ); setAllProjects(allowedDonationsProjects); if (allowedDonationsProjects?.length < 6) { @@ -373,7 +376,7 @@ const QueryParamProvider: FC = ({ children }) => { const found = countriesData.some( (arrayCountry) => arrayCountry.countryCode?.toUpperCase() === - config.data.country?.toUpperCase(), + config.data.country?.toUpperCase() ); if (found) { // This is to make sure donations which are already created with some country do not get affected by country from user config @@ -492,8 +495,9 @@ const QueryParamProvider: FC = ({ children }) => { tenant, locale: i18n.language, }; - const paymentSetupData: { data: PaymentOptions } = - await apiRequest(requestParams); + const paymentSetupData: { data: PaymentOptions } = await apiRequest( + requestParams + ); if (paymentSetupData.data) { const paymentSetup = paymentSetupData.data; if (shouldSetPaymentDetails) {