From 6b640ac29e1ed5d39e5121331b997b3b2dee0084 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Wed, 31 Dec 2025 17:02:16 -0300 Subject: [PATCH 1/6] feat: fields param --- paybutton/src/index.tsx | 1 + react/lib/components/PayButton/PayButton.tsx | 6 +++++- react/lib/components/PaymentDialog/PaymentDialog.tsx | 7 +++++-- react/lib/components/Widget/Widget.tsx | 8 +++++++- react/lib/components/Widget/WidgetContainer.tsx | 4 ++++ react/lib/util/types.ts | 7 +++++++ 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/paybutton/src/index.tsx b/paybutton/src/index.tsx index d6d911bc..0766f88f 100644 --- a/paybutton/src/index.tsx +++ b/paybutton/src/index.tsx @@ -106,6 +106,7 @@ const allowedProps = [ 'transactionText', 'size', 'donationRate', + 'fields' ]; const requiredProps = [ diff --git a/react/lib/components/PayButton/PayButton.tsx b/react/lib/components/PayButton/PayButton.tsx index f46588d1..f853cbcd 100644 --- a/react/lib/components/PayButton/PayButton.tsx +++ b/react/lib/components/PayButton/PayButton.tsx @@ -20,7 +20,8 @@ import { CryptoCurrency, ButtonSize, DEFAULT_DONATION_RATE, - createPayment + createPayment, + Field } from '../../util'; import { PaymentDialog } from '../PaymentDialog'; import { AltpaymentCoin, AltpaymentError, AltpaymentPair, AltpaymentShift } from '../../altpayment'; @@ -59,6 +60,7 @@ export interface PayButtonProps extends ButtonProps { sizeScaleAlreadyApplied?: boolean; donationAddress?: string; donationRate?: number; + fields?: Field[]; } export const PayButton = ({ @@ -86,6 +88,7 @@ export const PayButton = ({ apiBaseUrl, transactionText, disableSound, + fields, autoClose = false, disableAltpayment, contributionOffset, @@ -464,6 +467,7 @@ export const PayButton = ({ donationRate={donationRate} convertedCurrencyObj={convertedCurrencyObj} setConvertedCurrencyObj={setConvertedCurrencyObj} + fields={fields} /> {errorMsg && (

{ const [success, setSuccess] = useState(false); const [internalDisabled, setInternalDisabled] = useState(false); @@ -259,6 +261,7 @@ export const PaymentDialog = ({ donationRate={donationRate} convertedCurrencyObj={convertedCurrencyObj} setConvertedCurrencyObj={setConvertedCurrencyObj} + fields={fields} foot={success && ( = props => { donationRate = DEFAULT_DONATION_RATE, setConvertedCurrencyObj = () => {}, setPaymentId, + fields } = props; const [loading, setLoading] = useState(true); const [draftAmount, setDraftAmount] = useState("") @@ -957,6 +960,9 @@ export const Widget: React.FunctionComponent = props => { const handleQrCodeClick = useCallback((): void => { if (disabled || to === undefined || qrLoading) return if (!url || !copyToClipboard(url)) return + console.log({ + fields + }) setCopied(true) setRecentlyCopied(true) }, [disabled, to, url, setCopied, setRecentlyCopied, qrLoading]) diff --git a/react/lib/components/Widget/WidgetContainer.tsx b/react/lib/components/Widget/WidgetContainer.tsx index e0836d23..21436043 100644 --- a/react/lib/components/Widget/WidgetContainer.tsx +++ b/react/lib/components/Widget/WidgetContainer.tsx @@ -19,6 +19,7 @@ import { shouldTriggerOnSuccess, isPropsTrue, DEFAULT_DONATION_RATE, + Field, } from '../../util'; import Widget, { WidgetProps } from './Widget'; @@ -54,6 +55,7 @@ export interface WidgetContainerProps donationAddress?: string donationRate?: number convertedCurrencyObj?: CurrencyObject; + fields?: Field[]; } const snackbarOptionsSuccess: OptionsObject = { @@ -138,6 +140,7 @@ export const WidgetContainer: React.FunctionComponent = donationRate, convertedCurrencyObj, setConvertedCurrencyObj, + fields, ...widgetProps } = props; const [internalCurrencyObj, setInternalCurrencyObj] = useState(); @@ -327,6 +330,7 @@ export const WidgetContainer: React.FunctionComponent = convertedCurrencyObj={convertedCurrencyObj} setConvertedCurrencyObj={setConvertedCurrencyObj} setPaymentId={setThisPaymentId} + fields={fields} /> ); diff --git a/react/lib/util/types.ts b/react/lib/util/types.ts index 9ca41d6c..8ade811c 100644 --- a/react/lib/util/types.ts +++ b/react/lib/util/types.ts @@ -157,3 +157,10 @@ export interface CheckSuccessInfo { currencyObj?: CurrencyObject, donationRate?: number } + +export type Field = { + name: string + text: string + type: string +} + From c5870f0ed8769cb2be02fd72dfcf36a67688f438 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Wed, 31 Dec 2025 18:42:53 -0300 Subject: [PATCH 2/6] feat: add field inputs --- react/lib/components/Widget/Widget.tsx | 59 ++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index d447775f..098995f8 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -117,7 +117,7 @@ export interface WidgetProps { convertedCurrencyObj?: CurrencyObject; setConvertedCurrencyObj?: Function; setPaymentId?: Function; - fields?: Field[]; + fields?: Field[] | string; } interface StyleProps { @@ -178,8 +178,24 @@ export const Widget: React.FunctionComponent = props => { donationRate = DEFAULT_DONATION_RATE, setConvertedCurrencyObj = () => {}, setPaymentId, - fields + fields: fieldsProp } = props; + + // Parse fields if it's a JSON string + const fields = useMemo(() => { + if (!fieldsProp) return undefined; + if (Array.isArray(fieldsProp)) return fieldsProp; + if (typeof fieldsProp === 'string') { + try { + const parsed = JSON.parse(fieldsProp); + return Array.isArray(parsed) ? parsed : undefined; + } catch { + return undefined; + } + } + return undefined; + }, [fieldsProp]); + const [loading, setLoading] = useState(true); const [draftAmount, setDraftAmount] = useState("") const inputRef = React.useRef(null) @@ -961,7 +977,8 @@ export const Widget: React.FunctionComponent = props => { if (disabled || to === undefined || qrLoading) return if (!url || !copyToClipboard(url)) return console.log({ - fields + fields, + type: typeof fields }) setCopied(true) setRecentlyCopied(true) @@ -1277,6 +1294,42 @@ export const Widget: React.FunctionComponent = props => { ) : null} + {fields && Array.isArray(fields) && fields.length > 0 ? ( + + {fields.map((field, index) => ( + + ))} + + ) : null} + {success ? null : ( { From 82efa39ce34378d2fcea00852f8bad2113869a3f Mon Sep 17 00:00:00 2001 From: lissavxo Date: Tue, 13 Jan 2026 18:03:34 -0300 Subject: [PATCH 3/6] feat: generate paymentid with inputed fields --- react/lib/components/Widget/Widget.tsx | 120 +++++++++++++++++++++++-- react/lib/util/api-client.ts | 4 +- react/lib/util/types.ts | 2 + 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 098995f8..0dfe2bd5 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -118,6 +118,7 @@ export interface WidgetProps { setConvertedCurrencyObj?: Function; setPaymentId?: Function; fields?: Field[] | string; + onFieldsSubmit?: (fieldValues: Record) => void; } interface StyleProps { @@ -178,9 +179,15 @@ export const Widget: React.FunctionComponent = props => { donationRate = DEFAULT_DONATION_RATE, setConvertedCurrencyObj = () => {}, setPaymentId, - fields: fieldsProp + fields: fieldsProp, + onFieldsSubmit } = props; + // State to track field values + const [fieldValues, setFieldValues] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [fieldsSubmitted, setFieldsSubmitted] = useState(false); + // Parse fields if it's a JSON string const fields = useMemo(() => { if (!fieldsProp) return undefined; @@ -657,6 +664,7 @@ export const Widget: React.FunctionComponent = props => { to, apiBaseUrl, ); + console.log("entrou aqui carai") setPaymentId(responsePaymentId); } catch (error) { console.error('Error creating payment ID:', error); @@ -858,10 +866,13 @@ export const Widget: React.FunctionComponent = props => { } setUrl(nextUrl ?? '') } - }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation]) + }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation, paymentId]) useEffect(() => { try { + console.log({ + opReturn: props.opReturn, + }) setOpReturn( encodeOpReturnProps({ opReturn: props.opReturn, @@ -977,12 +988,55 @@ export const Widget: React.FunctionComponent = props => { if (disabled || to === undefined || qrLoading) return if (!url || !copyToClipboard(url)) return console.log({ - fields, - type: typeof fields + url, + paymentId }) setCopied(true) setRecentlyCopied(true) - }, [disabled, to, url, setCopied, setRecentlyCopied, qrLoading]) + }, [disabled, to, url, setCopied, setRecentlyCopied, qrLoading, paymentId]) + + const handleFieldsSubmit = useCallback(async (): Promise => { + if (!fields || success || isSubmitting) return; + + const missingFields = fields.filter( + (field) => field.required && !fieldValues[field.name]?.trim() + ); + + if (missingFields.length > 0) { + console.warn('Missing required fields:', missingFields.map(f => f.name)); + return; + } + + const fieldsWithValues = fields.map((field) => ({ + ...field, + value: fieldValues[field.name] || '' + })); + + setIsSubmitting(true); + + try { + if (!disablePaymentId && to) { + const effectiveAmount = convertedCryptoAmount ?? thisCurrencyObject?.float; + const newPaymentId = await createPayment(effectiveAmount, to, apiBaseUrl, fieldsWithValues); + console.log({ newPaymentId, setPaymentId }); + if (setPaymentId && newPaymentId) { + setPaymentId(newPaymentId); + } + } + + if (onFieldsSubmit) { + onFieldsSubmit(fieldValues); + } + setFieldsSubmitted(true); + setTimeout(() => { + setFieldsSubmitted(false); + }, 2000); + } catch (error) { + console.error('Error submitting fields:', error); + } finally { + setIsSubmitting(false); + } + }, [fields, fieldValues, success, isSubmitting, onFieldsSubmit, disablePaymentId, to, convertedCryptoAmount, thisCurrencyObject, apiBaseUrl, setPaymentId]) const resolveUrl = useCallback((currency: string, amount?: number) => { if (disabled || !to) return; @@ -1305,6 +1359,13 @@ export const Widget: React.FunctionComponent = props => { size="small" fullWidth disabled={success} + value={fieldValues[field.name] || ''} + onChange={(e) => { + setFieldValues(prev => ({ + ...prev, + [field.name]: e.target.value + })); + }} sx={{ '& .MuiOutlinedInput-root': { backgroundColor: isDarkMode ? '#1a1a1a' : '#fff', @@ -1327,6 +1388,55 @@ export const Widget: React.FunctionComponent = props => { }} /> ))} + + {isSubmitting ? ( + <> + + Submitting... + + ) : fieldsSubmitted ? ( + 'Confirmed ✓' + ) : ( + 'Confirm' + )} + ) : null} diff --git a/react/lib/util/api-client.ts b/react/lib/util/api-client.ts index ab271df0..819cab85 100644 --- a/react/lib/util/api-client.ts +++ b/react/lib/util/api-client.ts @@ -8,6 +8,7 @@ import { PriceData, TransactionDetails, Currency, + Field, } from './types'; import { isFiat } from './currency'; import { CURRENCY_TYPES_MAP, DECIMALS } from './constants'; @@ -94,6 +95,7 @@ export const createPayment = async ( amount: string | number | undefined, address: string, rootUrl = config.apiBaseUrl, + fields?: Field[], ): Promise => { const prefix = getAddressPrefix(address) const decimals = DECIMALS[CURRENCY_TYPES_MAP[prefix]] @@ -102,7 +104,7 @@ export const createPayment = async ( : undefined const { data, status } = await axios.post( `${rootUrl}/api/payments/paymentId`, - { amount: safeAmount, address } + { amount: safeAmount, address, fields } ); if (status === 200) { diff --git a/react/lib/util/types.ts b/react/lib/util/types.ts index 8ade811c..5483cec6 100644 --- a/react/lib/util/types.ts +++ b/react/lib/util/types.ts @@ -162,5 +162,7 @@ export type Field = { name: string text: string type: string + value?: string + required?: boolean } From 403dc69327d78611c929facee8815c1404ee1390 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Wed, 14 Jan 2026 19:07:25 -0300 Subject: [PATCH 4/6] feat: move field inputs to bottom --- react/lib/components/Widget/Widget.tsx | 197 +++++++++++++------------ 1 file changed, 100 insertions(+), 97 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 0dfe2bd5..f9ba1882 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -870,9 +870,6 @@ export const Widget: React.FunctionComponent = props => { useEffect(() => { try { - console.log({ - opReturn: props.opReturn, - }) setOpReturn( encodeOpReturnProps({ opReturn: props.opReturn, @@ -1020,6 +1017,13 @@ export const Widget: React.FunctionComponent = props => { const newPaymentId = await createPayment(effectiveAmount, to, apiBaseUrl, fieldsWithValues); console.log({ newPaymentId, setPaymentId }); if (setPaymentId && newPaymentId) { + setOpReturn( + encodeOpReturnProps({ + opReturn: props.opReturn, + paymentId: newPaymentId, + disablePaymentId: disablePaymentId ?? false, + }), + ) setPaymentId(newPaymentId); } } @@ -1036,7 +1040,7 @@ export const Widget: React.FunctionComponent = props => { } finally { setIsSubmitting(false); } - }, [fields, fieldValues, success, isSubmitting, onFieldsSubmit, disablePaymentId, to, convertedCryptoAmount, thisCurrencyObject, apiBaseUrl, setPaymentId]) + }, [fields, fieldValues, success, isSubmitting, onFieldsSubmit, disablePaymentId, to, convertedCryptoAmount, thisCurrencyObject, apiBaseUrl, setPaymentId, paymentId, props.opReturn]); const resolveUrl = useCallback((currency: string, amount?: number) => { if (disabled || !to) return; @@ -1348,102 +1352,9 @@ export const Widget: React.FunctionComponent = props => { ) : null} - {fields && Array.isArray(fields) && fields.length > 0 ? ( - - {fields.map((field, index) => ( - { - setFieldValues(prev => ({ - ...prev, - [field.name]: e.target.value - })); - }} - sx={{ - '& .MuiOutlinedInput-root': { - backgroundColor: isDarkMode ? '#1a1a1a' : '#fff', - '& fieldset': { - borderColor: isDarkMode ? '#333' : '#ddd', - }, - '&:hover fieldset': { - borderColor: theme.palette.primary, - }, - '&.Mui-focused fieldset': { - borderColor: theme.palette.primary, - }, - }, - '& .MuiInputLabel-root': { - color: isDarkMode ? '#b0b0b0' : '#666', - }, - '& .MuiInputBase-input': { - color: isDarkMode ? '#e0e0e0' : '#333', - }, - }} - /> - ))} - - {isSubmitting ? ( - <> - - Submitting... - - ) : fieldsSubmitted ? ( - 'Confirmed ✓' - ) : ( - 'Confirm' - )} - - - ) : null} - {success ? null : ( { - // Use createElement to avoid JSX element-type incompatibility from duplicate React types React.createElement(ButtonEl, { text: widgetButtonText, hoverText, @@ -1488,6 +1399,98 @@ export const Widget: React.FunctionComponent = props => { ) : null} + {fields && Array.isArray(fields) && fields.length > 0 ? ( + + {fields.map((field, index) => ( + { + setFieldValues(prev => ({ + ...prev, + [field.name]: e.target.value + })); + }} + sx={{ + '& .MuiOutlinedInput-root': { + backgroundColor: isDarkMode ? '#1a1a1a' : '#fff', + '& fieldset': { + borderColor: isDarkMode ? '#333' : '#ddd', + }, + '&:hover fieldset': { + borderColor: theme.palette.primary, + }, + '&.Mui-focused fieldset': { + borderColor: theme.palette.primary, + }, + }, + '& .MuiInputLabel-root': { + color: isDarkMode ? '#b0b0b0' : '#666', + }, + '& .MuiInputBase-input': { + color: isDarkMode ? '#e0e0e0' : '#333', + }, + }} + /> + ))} + + {isSubmitting ? ( + <> + + Submitting... + + ) : fieldsSubmitted ? ( + 'Confirmed ✓' + ) : ( + 'Confirm' + )} + + + ) : null} + Powered by PayButton.org From db4d10114d6a7572148a350398afd004c51e63af Mon Sep 17 00:00:00 2001 From: lissavxo Date: Wed, 14 Jan 2026 19:08:04 -0300 Subject: [PATCH 5/6] feat: fields in demo page --- paybutton/dev/demo/index.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 3ee3f628..6e06499a 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -30,6 +30,14 @@

+
+
From 9cab1869047f6c808c159cb7eaa7f4ce33421afe Mon Sep 17 00:00:00 2001 From: lissavxo Date: Wed, 14 Jan 2026 19:08:25 -0300 Subject: [PATCH 6/6] docs: fields --- docs/README.md | 34 ++++++++++++ docs/zh-cn/README.md | 34 ++++++++++++ docs/zh-tw/README.md | 71 ++++++++++++++++++++++++++ react/lib/components/Widget/Widget.tsx | 1 - 4 files changed, 139 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index e0cf1965..f58d51ba 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1127,6 +1127,40 @@ donationRate = 10 ``` +## fields + +> **The 'fields' parameter specifies custom input fields to collect additional information from users before payment.** + +?> This parameter is optional. Default value is empty. Possible values are an array of field objects or a JSON string. Each field object can have the following properties: `name` (required, unique identifier), `text` (required, label displayed to user), `type` (input type like 'text', 'email', 'number'), `value` (default value), and `required` (boolean). + +**Example:** + + +#### ** HTML ** + +```html +fields='[{"name": "email", "text": "Email Address", "type": "email", "required": true}, {"name": "note", "text": "Note", "type": "text"}]' +``` + +#### ** JavaScript ** + +```javascript +fields: [ + { name: 'email', text: 'Email Address', type: 'email', required: true }, + { name: 'note', text: 'Note', type: 'text' } +] +``` + +#### ** React ** + +```react +fields = {[ + { name: 'email', text: 'Email Address', type: 'email', required: true }, + { name: 'note', text: 'Note', type: 'text' } +]} +``` + + # Contribute PayButton is a community-driven open-source initiative. Contributions from the community are _crucial_ to the success of the project. diff --git a/docs/zh-cn/README.md b/docs/zh-cn/README.md index 2e5addaa..ca3afd36 100644 --- a/docs/zh-cn/README.md +++ b/docs/zh-cn/README.md @@ -1126,6 +1126,40 @@ donationRate = 10 ``` +## fields + +> **「fields」参数用于指定自定义输入字段,以便在付款前收集用户的额外信息。** + +?> 此参数是可选的。默认值为空。可接受的值是字段对象数组或 JSON 字符串。每个字段对象可以包含以下属性:`name`(必填,唯一标识符)、`text`(必填,显示给用户的标签)、`type`(输入类型,如 'text'、'email'、'number')、`value`(默认值)和 `required`(布尔值)。 + +**Example:** + + +#### ** HTML ** + +```html +fields='[{"name": "email", "text": "电子邮件地址", "type": "email", "required": true}, {"name": "note", "text": "备注", "type": "text"}]' +``` + +#### ** JavaScript ** + +```javascript +fields: [ + { name: 'email', text: '电子邮件地址', type: 'email', required: true }, + { name: 'note', text: '备注', type: 'text' } +] +``` + +#### ** React ** + +```react +fields = {[ + { name: 'email', text: '电子邮件地址', type: 'email', required: true }, + { name: 'note', text: '备注', type: 'text' } +]} +``` + + # 贡献 diff --git a/docs/zh-tw/README.md b/docs/zh-tw/README.md index c93271f1..32790e32 100644 --- a/docs/zh-tw/README.md +++ b/docs/zh-tw/README.md @@ -1122,6 +1122,77 @@ donationRate = 10 ``` +## fields + +> **「fields」參數用於指定自訂輸入欄位,以便在付款前收集用戶的額外資訊。** + +?> 此參數是可選的。預設值為空。可接受的值是欄位物件陣列或 JSON 字串。每個欄位物件可以包含以下屬性:`name`(必填,唯一識別碼)、`text`(必填,顯示給用戶的標籤)、`type`(輸入類型,如 'text'、'email'、'number')、`value`(預設值)和 `required`(布林值)。 + +**Example:** + + +#### ** HTML ** + +```html +fields='[{"name": "email", "text": "電子郵件地址", "type": "email", "required": true}, {"name": "note", "text": "備註", "type": "text"}]' +``` + +#### ** JavaScript ** + +```javascript +fields: [ + { name: 'email', text: '電子郵件地址', type: 'email', required: true }, + { name: 'note', text: '備註', type: 'text' } +] +``` + +#### ** React ** + +```react +fields = {[ + { name: 'email', text: '電子郵件地址', type: 'email', required: true }, + { name: 'note', text: '備註', type: 'text' } +]} +``` + + +## on-fields-submit + +> **「on-fields-submit」參數用於指定當用戶提交自訂欄位時執行的回呼函數。** + +?> 此參數是可選的。預設值為空。可接受的值是任何已定義的函數。 + +#### *callback* 參數 + +- **fieldValues** (`object`): 包含以欄位名稱為鍵的欄位值物件 + +**Example:** + + +#### ** HTML ** + +```html +on-fields-submit="fieldsSubmitCallback" +``` + +#### ** JavaScript ** + +```javascript +onFieldsSubmit: (fieldValues) => { + console.log('Fields submitted:', fieldValues); + // fieldValues 示例: { email: 'user@example.com', note: '我的付款備註' } +} +``` + +#### ** React ** + +```react +onFieldsSubmit = {(fieldValues) => { + console.log('Fields submitted:', fieldValues); +}} +``` + + # 貢獻 diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index f9ba1882..a51cae7a 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -1015,7 +1015,6 @@ export const Widget: React.FunctionComponent = props => { if (!disablePaymentId && to) { const effectiveAmount = convertedCryptoAmount ?? thisCurrencyObject?.float; const newPaymentId = await createPayment(effectiveAmount, to, apiBaseUrl, fieldsWithValues); - console.log({ newPaymentId, setPaymentId }); if (setPaymentId && newPaymentId) { setOpReturn( encodeOpReturnProps({