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/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 @@
+
+
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 && (
) => void;
}
interface StyleProps {
@@ -176,7 +179,30 @@ export const Widget: React.FunctionComponent = props => {
donationRate = DEFAULT_DONATION_RATE,
setConvertedCurrencyObj = () => {},
setPaymentId,
+ 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;
+ 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)
@@ -638,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);
@@ -839,7 +866,7 @@ 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 {
@@ -957,9 +984,62 @@ export const Widget: React.FunctionComponent = props => {
const handleQrCodeClick = useCallback((): void => {
if (disabled || to === undefined || qrLoading) return
if (!url || !copyToClipboard(url)) return
+ console.log({
+ 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);
+ if (setPaymentId && newPaymentId) {
+ setOpReturn(
+ encodeOpReturnProps({
+ opReturn: props.opReturn,
+ paymentId: newPaymentId,
+ disablePaymentId: disablePaymentId ?? false,
+ }),
+ )
+ 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, paymentId, props.opReturn]);
const resolveUrl = useCallback((currency: string, amount?: number) => {
if (disabled || !to) return;
@@ -1274,7 +1354,6 @@ export const Widget: React.FunctionComponent = props => {
{success ? null : (
{
- // Use createElement to avoid JSX element-type incompatibility from duplicate React types
React.createElement(ButtonEl, {
text: widgetButtonText,
hoverText,
@@ -1319,6 +1398,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
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/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 9ca41d6c..5483cec6 100644
--- a/react/lib/util/types.ts
+++ b/react/lib/util/types.ts
@@ -157,3 +157,12 @@ export interface CheckSuccessInfo {
currencyObj?: CurrencyObject,
donationRate?: number
}
+
+export type Field = {
+ name: string
+ text: string
+ type: string
+ value?: string
+ required?: boolean
+}
+