From 9780665aec649bb52320fb7bf905118e401843df Mon Sep 17 00:00:00 2001 From: Fishbakh-N Date: Thu, 12 Mar 2026 07:50:52 +0200 Subject: [PATCH 1/6] reorder context --- .../widget/context/FundraiserContext.tsx | 4 +++- .../components/widget/context/NonprofitContext.tsx | 10 +++++++--- .../src/components/widget/context/index.tsx | 14 +++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/donate-button-v4/src/components/widget/context/FundraiserContext.tsx b/packages/donate-button-v4/src/components/widget/context/FundraiserContext.tsx index a3577157..26fdca83 100644 --- a/packages/donate-button-v4/src/components/widget/context/FundraiserContext.tsx +++ b/packages/donate-button-v4/src/components/widget/context/FundraiserContext.tsx @@ -18,7 +18,9 @@ export const FundraiserContext = createContext({ fundraiser: FundraiserFetching }); -export const FundraiserContextProvider: FunctionalComponent = ({children}) => { +export const FundraiserContextProvider: FunctionalComponent<{ + fundraiserSlug?: string; +}> = ({children}) => { const {fundraiserSlug, nonprofitSlug} = useConfigContext(); const [fundraiser, setFundraiser] = useState(FundraiserFetching); diff --git a/packages/donate-button-v4/src/components/widget/context/NonprofitContext.tsx b/packages/donate-button-v4/src/components/widget/context/NonprofitContext.tsx index 8a462205..bd200643 100644 --- a/packages/donate-button-v4/src/components/widget/context/NonprofitContext.tsx +++ b/packages/donate-button-v4/src/components/widget/context/NonprofitContext.tsx @@ -1,7 +1,6 @@ import {createContext, FunctionalComponent} from 'preact'; import {useCallback, useEffect, useState} from 'preact/hooks'; import {getNonprofit} from 'src/components/widget/api'; -import {useConfigContext} from 'src/components/widget/hooks/useConfigContext'; import { Nonprofit, NonprofitFetchError, @@ -17,8 +16,9 @@ export const NonprofitContext = createContext({ nonprofit: NonprofitFetching }); -export const NonprofitContextProvider: FunctionalComponent = ({children}) => { - const {nonprofitSlug} = useConfigContext(); +export const NonprofitContextProvider: FunctionalComponent<{ + nonprofitSlug?: string; +}> = ({children, nonprofitSlug}) => { const [nonprofit, setNonprofit] = useState(NonprofitFetching); const [parentNonprofit, setParentNonprofitNonprofit] = @@ -26,6 +26,10 @@ export const NonprofitContextProvider: FunctionalComponent = ({children}) => { const fetchNonprofit = useCallback(async () => { try { + if (!nonprofitSlug) { + throw new Error('No nonprofit slug provided'); + } + const response = await getNonprofit(nonprofitSlug); setNonprofit(response); diff --git a/packages/donate-button-v4/src/components/widget/context/index.tsx b/packages/donate-button-v4/src/components/widget/context/index.tsx index c5828802..3371a128 100644 --- a/packages/donate-button-v4/src/components/widget/context/index.tsx +++ b/packages/donate-button-v4/src/components/widget/context/index.tsx @@ -9,11 +9,11 @@ export const ContextProvider: FunctionComponent<{ options: Partial; hide: () => void; }> = ({options, hide, children}) => ( - - - - {children} - - - + + + + {children} + + + ); From 5d5b25d8fc08e3d02f10b60c27a6c28b2ac210c0 Mon Sep 17 00:00:00 2001 From: Fishbakh-N Date: Fri, 13 Mar 2026 08:20:08 +0200 Subject: [PATCH 2/6] fetch customization --- .../src/components/widget/api/index.ts | 11 ++++++ .../widget/context/NonprofitContext.tsx | 34 ++++++++++++++--- .../src/components/widget/context/index.tsx | 5 ++- .../widget/types/DonateFlowCustomization.ts | 37 +++++++++++++++++++ .../src/components/widget/types/UrlParams.ts | 5 ++- .../components/widget/types/WidgetConfig.ts | 2 + .../src/helpers/parseDonateUrl.ts | 4 +- 7 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 packages/donate-button-v4/src/components/widget/types/DonateFlowCustomization.ts diff --git a/packages/donate-button-v4/src/components/widget/api/index.ts b/packages/donate-button-v4/src/components/widget/api/index.ts index 258520bd..43a21b2f 100644 --- a/packages/donate-button-v4/src/components/widget/api/index.ts +++ b/packages/donate-button-v4/src/components/widget/api/index.ts @@ -1,3 +1,4 @@ +import {DonateFlowCustomization} from 'src/components/widget/types/DonateFlowCustomization'; import {Fundraiser} from 'src/components/widget/types/Fundraiser'; import {Nonprofit} from 'src/components/widget/types/Nonprofit'; import {BASE_API_URL, BASE_COINGECKO_URL} from 'src/constants/url'; @@ -50,3 +51,13 @@ export async function getCoingeckoRate(coingeckoId: string) { return (data as CoingeckoData).market_data.current_price.usd; } + +export async function getCustomization(nonprofitId: string, code?: string) { + const url = `${BASE_API_URL}/${nonprofitId}/customization${ + code ? `?code=${code}` : '' + }`; + const response: DonateFlowCustomization = await fetch(url).then( + async (response) => response.json() + ); + return response; +} diff --git a/packages/donate-button-v4/src/components/widget/context/NonprofitContext.tsx b/packages/donate-button-v4/src/components/widget/context/NonprofitContext.tsx index bd200643..c07d92c4 100644 --- a/packages/donate-button-v4/src/components/widget/context/NonprofitContext.tsx +++ b/packages/donate-button-v4/src/components/widget/context/NonprofitContext.tsx @@ -1,28 +1,43 @@ import {createContext, FunctionalComponent} from 'preact'; import {useCallback, useEffect, useState} from 'preact/hooks'; -import {getNonprofit} from 'src/components/widget/api'; +import {getCustomization, getNonprofit} from 'src/components/widget/api'; import { Nonprofit, NonprofitFetchError, NonprofitFetching } from 'src/components/widget/types/Nonprofit'; +import { + DonateFlowCustomization, + DonateFlowCustomizationFetchError, + DonateFlowCustomizationFetching +} from 'src/components/widget/types/DonateFlowCustomization'; interface NonprofitContextData { nonprofit: Nonprofit | typeof NonprofitFetchError | typeof NonprofitFetching; parentNonprofit?: Nonprofit; + customization: + | DonateFlowCustomization + | typeof DonateFlowCustomizationFetching + | typeof DonateFlowCustomizationFetchError + | undefined; } export const NonprofitContext = createContext({ - nonprofit: NonprofitFetching + nonprofit: NonprofitFetching, + customization: DonateFlowCustomizationFetching }); export const NonprofitContextProvider: FunctionalComponent<{ nonprofitSlug?: string; -}> = ({children, nonprofitSlug}) => { + code?: string; +}> = ({children, nonprofitSlug, code}) => { const [nonprofit, setNonprofit] = useState(NonprofitFetching); const [parentNonprofit, setParentNonprofitNonprofit] = useState(); + const [customization, setCustomization] = useState< + NonprofitContextData['customization'] + >(DonateFlowCustomizationFetching); const fetchNonprofit = useCallback(async () => { try { @@ -46,10 +61,17 @@ export const NonprofitContextProvider: FunctionalComponent<{ setParentNonprofitNonprofit(undefined); } } + + try { + const customizationResponse = await getCustomization(response.id, code); + setCustomization(customizationResponse); + } catch { + setCustomization(DonateFlowCustomizationFetchError); + } } catch { setNonprofit(NonprofitFetchError); } - }, [nonprofitSlug]); + }, [nonprofitSlug, code]); useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -57,7 +79,9 @@ export const NonprofitContextProvider: FunctionalComponent<{ }, [fetchNonprofit]); return ( - + {children} ); diff --git a/packages/donate-button-v4/src/components/widget/context/index.tsx b/packages/donate-button-v4/src/components/widget/context/index.tsx index 3371a128..b364f216 100644 --- a/packages/donate-button-v4/src/components/widget/context/index.tsx +++ b/packages/donate-button-v4/src/components/widget/context/index.tsx @@ -9,7 +9,10 @@ export const ContextProvider: FunctionComponent<{ options: Partial; hide: () => void; }> = ({options, hide, children}) => ( - + {children} diff --git a/packages/donate-button-v4/src/components/widget/types/DonateFlowCustomization.ts b/packages/donate-button-v4/src/components/widget/types/DonateFlowCustomization.ts new file mode 100644 index 00000000..372cfc2e --- /dev/null +++ b/packages/donate-button-v4/src/components/widget/types/DonateFlowCustomization.ts @@ -0,0 +1,37 @@ +import {DonationFrequency} from 'src/components/widget/types/DonationFrequency'; +import {PaymentMethod} from 'src/components/widget/types/PaymentMethod'; + +export type RequirementLevel = 'required' | 'optional' | null; + +export enum CustomFieldType { + TEXT_AREA = 'textArea', + DROPDOWN = 'dropdown' +} + +export interface CustomField { + type: CustomFieldType; + heading: string; + required: boolean; + placeholder?: string; + options?: string[]; +} + +export interface DonateFlowCustomization { + paymentMethods?: PaymentMethod[]; + address?: RequirementLevel; + fields?: CustomField[]; + themeColor?: string; + designation?: string; + donorContactInfo?: RequirementLevel; + fixedAmount?: number; + frequency?: DonationFrequency; + minAmount?: number; + suggestedAmounts?: number[]; +} + +export const DonateFlowCustomizationFetchError = Symbol( + 'DonateFlowCustomizationFetchError' +); +export const DonateFlowCustomizationFetching = Symbol( + 'DonateFlowCustomizationFetching' +); diff --git a/packages/donate-button-v4/src/components/widget/types/UrlParams.ts b/packages/donate-button-v4/src/components/widget/types/UrlParams.ts index f328214b..72633be2 100644 --- a/packages/donate-button-v4/src/components/widget/types/UrlParams.ts +++ b/packages/donate-button-v4/src/components/widget/types/UrlParams.ts @@ -59,7 +59,10 @@ export enum DonateUrlParameters { SEARCH_META = 'search_meta', - GIFT_CARD_CODE = 'gift_card_code' + GIFT_CARD_CODE = 'gift_card_code', + + // Customization code + CODE = 'code' } export enum UTM_QUERY_PARAM { diff --git a/packages/donate-button-v4/src/components/widget/types/WidgetConfig.ts b/packages/donate-button-v4/src/components/widget/types/WidgetConfig.ts index 500b5cb7..6b5ef6a2 100644 --- a/packages/donate-button-v4/src/components/widget/types/WidgetConfig.ts +++ b/packages/donate-button-v4/src/components/widget/types/WidgetConfig.ts @@ -55,4 +55,6 @@ export interface WidgetConfig { requireShareInfo?: boolean; previewMode?: boolean; + + code?: string; } diff --git a/packages/donate-button-v4/src/helpers/parseDonateUrl.ts b/packages/donate-button-v4/src/helpers/parseDonateUrl.ts index c742e223..49fc82fd 100644 --- a/packages/donate-button-v4/src/helpers/parseDonateUrl.ts +++ b/packages/donate-button-v4/src/helpers/parseDonateUrl.ts @@ -115,6 +115,7 @@ export function parseDonateUrl( undefined; const partnerMetadata = searchParameters.get(DonateUrlParameters.PARTNER_METADATA) ?? undefined; + const code = searchParameters.get(DonateUrlParameters.CODE) ?? undefined; if (!nonprofitSlug) { return; @@ -136,6 +137,7 @@ export function parseDonateUrl( designation, requireShareInfo, webhookToken, - partnerMetadata + partnerMetadata, + code }); } From 65812cd1c45547d0408f5a932dfeec572f9433c0 Mon Sep 17 00:00:00 2001 From: Fishbakh-N Date: Fri, 13 Mar 2026 08:20:59 +0200 Subject: [PATCH 3/6] merge customization with widget options --- .../widget/context/ConfigContext.tsx | 4 +- .../widget/hooks/useCustmization.ts | 22 ++++++++++ .../helpers/customizationToWidgetConfig.ts | 43 +++++++++++++++++++ .../src/helpers/optionsTypes.ts | 29 ++++++++++--- 4 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 packages/donate-button-v4/src/components/widget/hooks/useCustmization.ts create mode 100644 packages/donate-button-v4/src/helpers/customizationToWidgetConfig.ts diff --git a/packages/donate-button-v4/src/components/widget/context/ConfigContext.tsx b/packages/donate-button-v4/src/components/widget/context/ConfigContext.tsx index 344dd0db..d74eaa76 100644 --- a/packages/donate-button-v4/src/components/widget/context/ConfigContext.tsx +++ b/packages/donate-button-v4/src/components/widget/context/ConfigContext.tsx @@ -1,4 +1,5 @@ import {createContext, FunctionalComponent} from 'preact'; +import {useCustomizationOrUndefined} from 'src/components/widget/hooks/useCustmization'; import {WidgetConfig} from 'src/components/widget/types/WidgetConfig'; import {mergeConfig} from 'src/helpers/optionsTypes'; @@ -7,7 +8,8 @@ export const ConfigContext = createContext({} as WidgetConfig); export const ConfigContextProvider: FunctionalComponent<{ options: Partial; }> = ({options, children}) => { - const mergedConfig = mergeConfig(options); + const customization = useCustomizationOrUndefined(); + const mergedConfig = mergeConfig(options, customization); return ( diff --git a/packages/donate-button-v4/src/components/widget/hooks/useCustmization.ts b/packages/donate-button-v4/src/components/widget/hooks/useCustmization.ts new file mode 100644 index 00000000..3d95aa77 --- /dev/null +++ b/packages/donate-button-v4/src/components/widget/hooks/useCustmization.ts @@ -0,0 +1,22 @@ +import {useContext} from 'preact/hooks'; +import {NonprofitContext} from 'src/components/widget/context/NonprofitContext'; +import { + DonateFlowCustomizationFetchError, + DonateFlowCustomizationFetching +} from 'src/components/widget/types/DonateFlowCustomization'; + +export const useCustomization = () => + useContext(NonprofitContext).customization; + +export const useCustomizationOrUndefined = () => { + const customization = useCustomization(); + + if ( + customization === DonateFlowCustomizationFetchError || + customization === DonateFlowCustomizationFetching + ) { + return undefined; + } + + return customization; +}; diff --git a/packages/donate-button-v4/src/helpers/customizationToWidgetConfig.ts b/packages/donate-button-v4/src/helpers/customizationToWidgetConfig.ts new file mode 100644 index 00000000..b0ebc174 --- /dev/null +++ b/packages/donate-button-v4/src/helpers/customizationToWidgetConfig.ts @@ -0,0 +1,43 @@ +import {DonateFlowCustomization} from 'src/components/widget/types/DonateFlowCustomization'; +import {WidgetConfig} from 'src/components/widget/types/WidgetConfig'; + +/** + * Maps a DonateFlowCustomization object to a partial WidgetConfig. + * Only fields that have a direct mapping are converted. + */ +export const customizationToWidgetConfig = ( + customization: DonateFlowCustomization +): Partial => { + const config: Partial = {}; + + if (customization.paymentMethods !== undefined) { + config.methods = customization.paymentMethods; + } + + if (customization.themeColor !== undefined) { + config.primaryColor = customization.themeColor; + } + + if (customization.designation !== undefined) { + config.designation = customization.designation; + } + + if (customization.fixedAmount !== undefined) { + config.amount = customization.fixedAmount; + } + + if (customization.frequency !== undefined) { + config.frequency = customization.frequency; + config.defaultFrequency = customization.frequency; + } + + if (customization.minAmount !== undefined) { + config.minDonationAmount = customization.minAmount; + } + + if (customization.suggestedAmounts !== undefined) { + config.addAmounts = customization.suggestedAmounts; + } + + return config; +}; diff --git a/packages/donate-button-v4/src/helpers/optionsTypes.ts b/packages/donate-button-v4/src/helpers/optionsTypes.ts index 477a51c1..34c57921 100644 --- a/packages/donate-button-v4/src/helpers/optionsTypes.ts +++ b/packages/donate-button-v4/src/helpers/optionsTypes.ts @@ -1,4 +1,5 @@ import deepMerge, {Options as DeepMergeOptions} from 'deepmerge'; +import {DonateFlowCustomization} from 'src/components/widget/types/DonateFlowCustomization'; import {DonationFrequency} from 'src/components/widget/types/DonationFrequency'; import { AvailablePaymentMethods, @@ -6,6 +7,7 @@ import { PaymentMethod } from 'src/components/widget/types/PaymentMethod'; import {WidgetConfig} from 'src/components/widget/types/WidgetConfig'; +import {customizationToWidgetConfig} from 'src/helpers/customizationToWidgetConfig'; import getScriptParameters from 'src/helpers/getScriptParameters'; const PREVIEW_MODE_PARAM = 'previewMode'; @@ -28,23 +30,36 @@ const DEEP_MERGE_OPTIONS: DeepMergeOptions = { arrayMerge: (_, sourceArray) => sourceArray }; -export const mergeConfig = (options: Partial): WidgetConfig => { - const filteredInputMethods = options.methods?.filter((method) => +export const mergeConfig = ( + options: Partial, + customization?: DonateFlowCustomization | null +): WidgetConfig => { + const customizationConfig = customization + ? customizationToWidgetConfig(customization) + : {}; + + // Determine methods: options > customization > defaults + const sourceMethods = + options.methods ?? customizationConfig.methods ?? DefaultPaymentMethods; + + const filteredInputMethods = sourceMethods.filter((method) => AvailablePaymentMethods.includes(method) ); - const additionalMethods = options.showGiftCardOption - ? [PaymentMethod.GIFT_CARD] - : []; + const additionalMethods = + options.showGiftCardOption ?? + (customizationConfig as Partial).showGiftCardOption + ? [PaymentMethod.GIFT_CARD] + : []; const methods = ( - filteredInputMethods && filteredInputMethods.length > 0 + filteredInputMethods.length > 0 ? filteredInputMethods : DefaultPaymentMethods ).concat(additionalMethods); return deepMerge.all( - [defaults, options, {methods}], + [defaults, customizationConfig, options, {methods}], DEEP_MERGE_OPTIONS ); }; From 8b3eb78598da528a767aa6af1d935fb5211ce23c Mon Sep 17 00:00:00 2001 From: Fishbakh-N Date: Fri, 13 Mar 2026 08:21:48 +0200 Subject: [PATCH 4/6] show loading indicator while fetching customization --- .../donate-button-v4/src/components/widget/index.tsx | 12 ++++++++++-- packages/donate-button-v4/src/loaders/FormLoader.tsx | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/donate-button-v4/src/components/widget/index.tsx b/packages/donate-button-v4/src/components/widget/index.tsx index 205fb804..c566bbe4 100644 --- a/packages/donate-button-v4/src/components/widget/index.tsx +++ b/packages/donate-button-v4/src/components/widget/index.tsx @@ -11,10 +11,15 @@ import {PaymentProcess} from 'src/components/widget/components/PaymentProcess'; import {WidgetCard} from 'src/components/widget/components/WidgetCard'; import {WidgetOverlay} from 'src/components/widget/components/WidgetOverlay'; import {useConfigContext} from 'src/components/widget/hooks/useConfigContext'; +import {useCustomization} from 'src/components/widget/hooks/useCustmization'; import {useFundraiser} from 'src/components/widget/hooks/useFundraiser'; import {useNonprofit} from 'src/components/widget/hooks/useNonprofit'; import {useWidgetContext} from 'src/components/widget/hooks/useWidgetContext'; import {LoadingIcon} from 'src/components/widget/icons/LoadingIcon'; +import { + DonateFlowCustomizationFetchError, + DonateFlowCustomizationFetching +} from 'src/components/widget/types/DonateFlowCustomization'; import { FundraiserFetchError, FundraiserFetching @@ -29,6 +34,7 @@ const Widget = () => { const {hideWidget} = useWidgetContext(); const findraiser = useFundraiser(); const nonprofit = useNonprofit(); + const customization = useCustomization(); const hideOnWrapperClick: JSXInternal.MouseEventHandler = ( event @@ -40,7 +46,8 @@ const Widget = () => { if ( nonprofit === NonprofitFetchError || - (fundraiserSlug && findraiser === FundraiserFetchError) + (fundraiserSlug && findraiser === FundraiserFetchError) || + customization === DonateFlowCustomizationFetchError ) { hideWidget(); return null; @@ -48,7 +55,8 @@ const Widget = () => { if ( nonprofit === NonprofitFetching || - (fundraiserSlug && findraiser === FundraiserFetching) + (fundraiserSlug && findraiser === FundraiserFetching) || + customization === DonateFlowCustomizationFetching ) { return ( diff --git a/packages/donate-button-v4/src/loaders/FormLoader.tsx b/packages/donate-button-v4/src/loaders/FormLoader.tsx index d8bf9b97..b4488ddc 100644 --- a/packages/donate-button-v4/src/loaders/FormLoader.tsx +++ b/packages/donate-button-v4/src/loaders/FormLoader.tsx @@ -1,9 +1,11 @@ import {PaymentProcess} from 'src/components/widget/components/PaymentProcess'; import {ContextProvider} from 'src/components/widget/context'; import {useConfigContext} from 'src/components/widget/hooks/useConfigContext'; +import {useCustomization} from 'src/components/widget/hooks/useCustmization'; import {useFundraiser} from 'src/components/widget/hooks/useFundraiser'; import {useNonprofit} from 'src/components/widget/hooks/useNonprofit'; import {LoadingIcon} from 'src/components/widget/icons/LoadingIcon'; +import {DonateFlowCustomizationFetching} from 'src/components/widget/types/DonateFlowCustomization'; import {FundraiserFetching} from 'src/components/widget/types/Fundraiser'; import {NonprofitFetching} from 'src/components/widget/types/Nonprofit'; import {WidgetConfig} from 'src/components/widget/types/WidgetConfig'; @@ -29,10 +31,12 @@ const Form = () => { const {fundraiserSlug} = useConfigContext(); const findraiser = useFundraiser(); const nonprofit = useNonprofit(); + const customization = useCustomization(); if ( nonprofit === NonprofitFetching || - (fundraiserSlug && findraiser === FundraiserFetching) + (fundraiserSlug && findraiser === FundraiserFetching) || + customization === DonateFlowCustomizationFetching ) { return ; } From a58ea88c2e10e190580754212a968174eb013177 Mon Sep 17 00:00:00 2001 From: Fishbakh-N Date: Fri, 13 Mar 2026 12:14:46 +0200 Subject: [PATCH 5/6] add custom fields --- .../PaymentProcess/CryptoFlow/index.tsx | 2 + .../PaymentProcess/CustomFields/index.tsx | 324 ++++++++++++++++++ .../DafFlow/Views/DafAmountView.tsx | 2 + .../PaymentProcess/DefaultFlow/index.tsx | 2 + .../PaymentProcess/GiftCardFlow/index.tsx | 2 + .../PaymentProcess/StocksFlow/index.tsx | 5 +- .../widget/context/WidgetContext.tsx | 11 +- .../widget/hooks/useSubmitDonation.ts | 26 +- .../src/components/widget/types/UrlParams.ts | 4 +- .../src/helpers/constructDonateUrl.ts | 6 +- 10 files changed, 374 insertions(+), 10 deletions(-) create mode 100644 packages/donate-button-v4/src/components/widget/components/PaymentProcess/CustomFields/index.tsx diff --git a/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CryptoFlow/index.tsx b/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CryptoFlow/index.tsx index 8910d65a..8a356d80 100644 --- a/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CryptoFlow/index.tsx +++ b/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CryptoFlow/index.tsx @@ -1,6 +1,7 @@ import {ErrorMessage} from 'src/components/widget/components/ErrorMessage'; import {CryptoAmountInput} from 'src/components/widget/components/PaymentProcess/CryptoFlow/CryptoAmountInput'; import {CryptoSelector} from 'src/components/widget/components/PaymentProcess/CryptoFlow/CryptoSelector'; +import {CustomFields} from 'src/components/widget/components/PaymentProcess/CustomFields'; import { LargePaymentMethodSelect, SmallPaymentMethodSelect @@ -57,6 +58,7 @@ export const CryptoFlow = () => { )} + diff --git a/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CustomFields/index.tsx b/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CustomFields/index.tsx new file mode 100644 index 00000000..5b57289b --- /dev/null +++ b/packages/donate-button-v4/src/components/widget/components/PaymentProcess/CustomFields/index.tsx @@ -0,0 +1,324 @@ +import {Fragment} from 'preact/jsx-runtime'; +import {useEffect, useRef, useState} from 'preact/hooks'; +import {rotateCss} from 'src/components/widget/components/Faq/styles'; +import { + fieldSetCss, + legendCss +} from 'src/components/widget/components/PaymentProcess/styles'; +import {TextArea} from 'src/components/widget/components/TextInput'; +import { + textInputContainerCss, + textInputCss +} from 'src/components/widget/components/TextInput/styles'; +import {useConfigContext} from 'src/components/widget/hooks/useConfigContext'; +import {useCustomizationOrUndefined} from 'src/components/widget/hooks/useCustmization'; +import {useWidgetContext} from 'src/components/widget/hooks/useWidgetContext'; +import {ArrowIcon} from 'src/components/widget/icons/ArrowIcon'; +import {getColoredBorder, Borders} from 'src/components/widget/theme/borders'; +import {BREAKPOINTS} from 'src/components/widget/theme/breakpoints'; +import {COLORS} from 'src/components/widget/theme/colors'; +import {textSize} from 'src/components/widget/theme/font-sizes'; +import {Radii} from 'src/components/widget/theme/radii'; +import {Spacing, verticalStackCss} from 'src/components/widget/theme/spacing'; +import { + CustomField, + CustomFieldType +} from 'src/components/widget/types/DonateFlowCustomization'; +import css from 'src/helpers/css'; +import joinClassNames from 'src/helpers/joinClassNames'; + +const dropdownContainerCss = css({ + position: 'absolute', + width: '100%', + background: COLORS.White, + borderRadius: `0 0 ${Radii.Default} ${Radii.Default}`, + border: getColoredBorder(Borders.Normal, COLORS.LightGray), + borderTop: 'none' +}); + +const dropdownInputRowCss = css({ + display: 'flex', + alignItems: 'center', + gap: Spacing.XXS +}); + +const dropdownContentCss = css({ + overflowY: 'scroll', + height: '100%', + maxHeight: '200px' +}); + +const dropdownItemCss = css({ + border: 'none', + background: 'none', + padding: `${Spacing.S} ${Spacing.M}`, + ...textSize.s, + width: '100%', + display: 'flex', + alignItems: 'center', + gap: Spacing.XS, + ':hover': { + cursor: 'pointer' + } +}); + +const dropdownSuffixCss = (primaryColor: string, open: boolean) => + css({ + marginLeft: 'auto', + ...textSize.xs, + color: COLORS.TextGray, + display: 'flex', + gap: Spacing.XS, + alignItems: 'center', + '> svg': { + color: primaryColor, + ...(open ? rotateCss : {}) + } + }); + +const dropdownOpenContainerCss = css({ + borderRadius: `${Radii.Default} ${Radii.Default} 0 0` +}); + +// Mobile: shown; desktop: hidden +const nativeSelectWrapperCss = css({ + [BREAKPOINTS.TabletUp]: {display: 'none'} +}); + +// Mobile: hidden; desktop: shown +const customDropdownWrapperCss = css({ + display: 'none', + position: 'relative', + [BREAKPOINTS.TabletUp]: {display: 'block'} +}); + +const nativeSelectCss = css({ + fontSize: textSize.s.fontSize, + lineHeight: textSize.s.fontSize, + fontFamily: 'inherit', + flex: 1, + border: 'none', + outline: 'none', + background: 'transparent', + width: '100%', + padding: 0, + margin: 0, + color: COLORS.Text, + cursor: 'pointer', + appearance: 'none' +}); + +const nativeSelectSuffixCss = (primaryColor: string) => + css({ + marginLeft: 'auto', + display: 'flex', + alignItems: 'center', + '> svg': {color: primaryColor} + }); + +const dropdownTriggerCss = css({cursor: 'pointer'}); + +const selectedValueCss = css({ + fontSize: textSize.s.fontSize, + lineHeight: textSize.s.fontSize, + fontFamily: 'inherit', + flex: 1, + background: 'transparent', + padding: 0, + margin: 0, + userSelect: 'none' +}); + +const placeholderValueCss = css({color: COLORS.TextGray}); + +interface DropdownFieldProps { + field: CustomField; + value: string; + onChange: (value: string) => void; + primaryColor: string; +} + +const DropdownField = ({ + field, + value, + onChange, + primaryColor +}: DropdownFieldProps) => { + const [showDropDown, setShowDropDown] = useState(false); + const [selectedOption, setSelectedOption] = useState(value); + const containerRef = useRef(null); + + useEffect(() => { + if (!showDropDown) return; + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setShowDropDown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showDropDown]); + + const displayText = + selectedOption || (field.placeholder ?? 'Select an option'); + + return ( +
+
{ + setShowDropDown((previous) => !previous); + }} + onKeyDown={({key}) => { + if (key === 'Enter' || key === ' ') setShowDropDown((p) => !p); + }} + > + + {displayText} + +
+ +
+
+ {showDropDown && ( +
{ + e.stopPropagation(); + }} + > +
+
    + {(field.options ?? []).map((option) => ( +
  • + +
  • + ))} +
+
+
+ )} +
+ ); +}; + +export const CustomFields = () => { + const customization = useCustomizationOrUndefined(); + const {customFieldValues, setCustomFieldValues} = useWidgetContext(); + const {primaryColor} = useConfigContext(); + + const fields = customization?.fields; + if (!fields || fields.length === 0) return null; + + const handleChange = (heading: string, value: string) => { + setCustomFieldValues((previous) => ({...previous, [heading]: value})); + }; + + console.log('Rendering custom fields with values', customFieldValues); + return ( +
+ {fields.map((field) => ( +
+ + {field.heading} + {field.required && ' *'} + + {field.type === CustomFieldType.TEXT_AREA ? ( +