| 1 |
import { useState } from '@wordpress/element'; |
| 2 |
import { getCurrencySymbol, normalizeIconClass, titleToSnake } from '../../helpers/helpers'; |
| 3 |
|
| 4 |
const Layout1 = ({ attributes, setAttributes, serialized, pageId }) => { |
| 5 |
const { |
| 6 |
formFields, |
| 7 |
showSidebar, |
| 8 |
currencyAlign, |
| 9 |
showAmountList, |
| 10 |
amountList, |
| 11 |
transactionTitle, |
| 12 |
transactionSubTitle, |
| 13 |
amountText, |
| 14 |
paypalButtonText, |
| 15 |
stripeButtonText, |
| 16 |
paystackButtonText, |
| 17 |
paypalEnabled, |
| 18 |
stripeEnabled, |
| 19 |
paystackEnabled, |
| 20 |
currency, |
| 21 |
paymentSource, |
| 22 |
blockId, |
| 23 |
productName, |
| 24 |
productPrice, |
| 25 |
productPermalink, |
| 26 |
transactionDetailsProductTitle, |
| 27 |
} = attributes; |
| 28 |
|
| 29 |
// --- Computed layout variables (mirrors layout-vars.php) --- |
| 30 |
const currencySymbol = currency ? getCurrencySymbol(currency) : ''; |
| 31 |
const currencyAlignment = currencyAlign || 'left'; |
| 32 |
const currencyLeft = currencyAlignment === 'left' ? currencySymbol : ''; |
| 33 |
const currencyRight = currencyAlignment === 'right' ? currencySymbol : ''; |
| 34 |
|
| 35 |
const isPaymentTypeWoocommerce = paymentSource === 'woocommerce'; |
| 36 |
const isPaymentTypeFluentcart = paymentSource === 'fluentcart'; |
| 37 |
const isPaymentTypeStripe = paymentSource === 'stripe'; |
| 38 |
const layoutPutAmountFieldHideShow = (isPaymentTypeWoocommerce || isPaymentTypeFluentcart || isPaymentTypeStripe) ? 'is-hidden' : ''; |
| 39 |
const layoutDynamicPaymentHideShow = (isPaymentTypeWoocommerce || isPaymentTypeFluentcart || isPaymentTypeStripe) ? '' : 'is-hidden'; |
| 40 |
// Show product title only for WooCommerce and FluentCart (not Stripe) |
| 41 |
const showProductTitle = isPaymentTypeWoocommerce || isPaymentTypeFluentcart; |
| 42 |
|
| 43 |
// Three-items-enabled class (when all 3 payment methods are on) |
| 44 |
const threeItemsClass = (paypalEnabled && stripeEnabled && paystackEnabled) ? 'bp-three-items-enabled' : ''; |
| 45 |
|
| 46 |
// Count enabled payment methods for single-item logic |
| 47 |
const enabledCount = [paypalEnabled, stripeEnabled, paystackEnabled].filter(Boolean).length; |
| 48 |
|
| 49 |
const assetsUrl = window?.betterPaymentBlockData?.assetsUrl || ''; |
| 50 |
|
| 51 |
// Return null if all payment methods are disabled (mirrors Elementor widget behavior) |
| 52 |
if (!paypalEnabled && !stripeEnabled && !paystackEnabled) { |
| 53 |
return null; |
| 54 |
} |
| 55 |
|
| 56 |
// Track amount entered in the payment amount input field |
| 57 |
const [enteredAmount, setEnteredAmount] = useState(''); |
| 58 |
|
| 59 |
// Track selected payment method for dynamic button display |
| 60 |
const getDefaultPaymentMethod = () => { |
| 61 |
if (paypalEnabled) return 'paypal'; |
| 62 |
if (stripeEnabled) return 'stripe'; |
| 63 |
if (paystackEnabled) return 'paystack'; |
| 64 |
return 'paypal'; |
| 65 |
}; |
| 66 |
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(getDefaultPaymentMethod()); |
| 67 |
|
| 68 |
// Track selected amount from amount list |
| 69 |
const [selectedAmountIndex, setSelectedAmountIndex] = useState(-1); |
| 70 |
|
| 71 |
// Determine display amount: product price takes precedence when available |
| 72 |
const hasProductPrice = (isPaymentTypeWoocommerce || isPaymentTypeFluentcart || isPaymentTypeStripe) && productPrice !== ''; |
| 73 |
const displayAmount = hasProductPrice ? parseFloat(productPrice) : (enteredAmount || ''); |
| 74 |
|
| 75 |
// Handle amount list item click |
| 76 |
const handleAmountSelect = (amountValue, index) => { |
| 77 |
setSelectedAmountIndex(index); |
| 78 |
setEnteredAmount(String(amountValue)); |
| 79 |
}; |
| 80 |
|
| 81 |
// Handle payment method change |
| 82 |
const handlePaymentMethodChange = (method) => { |
| 83 |
setSelectedPaymentMethod(method); |
| 84 |
}; |
| 85 |
|
| 86 |
// --- Render helper: icon for a form field (mirrors layout-repeater-vars.php icon logic) --- |
| 87 |
const renderFieldIcon = (item, isPaymentAmountField) => { |
| 88 |
// Determine the default icon based on field type (mirrors layout-repeater-vars.php line 26) |
| 89 |
const fieldType = item.type || 'text'; |
| 90 |
const defaultIcon = fieldType === 'email' ? 'bp-icon bp-envelope' : 'bp-icon bp-user'; |
| 91 |
const icon = normalizeIconClass(item.icon || defaultIcon); |
| 92 |
|
| 93 |
const isImage = icon.startsWith('http://') || icon.startsWith('https://') || icon.startsWith('/'); |
| 94 |
|
| 95 |
// For payment amount field, always show currency symbol |
| 96 |
if (isPaymentAmountField) { |
| 97 |
return ( |
| 98 |
<span className="icon is-medium is-left"> |
| 99 |
<span className="icon is-medium is-left"> |
| 100 |
<span className="bp-currency-symbol">{currencySymbol}</span> |
| 101 |
</span> |
| 102 |
</span> |
| 103 |
); |
| 104 |
} |
| 105 |
|
| 106 |
if (isImage) { |
| 107 |
return ( |
| 108 |
<span className="icon is-medium is-left"> |
| 109 |
<img src={icon} alt="Icon" width="20" /> |
| 110 |
</span> |
| 111 |
); |
| 112 |
} |
| 113 |
|
| 114 |
// FontAwesome icons start with 'fas', 'far', 'fab' etc. |
| 115 |
// bp-icon icons start with 'bp-icon' |
| 116 |
return ( |
| 117 |
<span className="icon is-medium is-left"> |
| 118 |
<i className={icon}></i> |
| 119 |
</span> |
| 120 |
); |
| 121 |
}; |
| 122 |
|
| 123 |
// --- Render amount list (mirrors render_amount_element) --- |
| 124 |
const renderAmountList = () => { |
| 125 |
if (!showAmountList || !amountList || amountList.length === 0) { |
| 126 |
return null; |
| 127 |
} |
| 128 |
return ( |
| 129 |
<> |
| 130 |
{amountList.map((amountItem, idx) => { |
| 131 |
const amountValue = parseFloat(amountItem.label) || 0; |
| 132 |
const uniqueId = `bp_payment_amount_${blockId}_${idx}`; |
| 133 |
return ( |
| 134 |
<div key={idx} className="bp-form__group pt-5"> |
| 135 |
<input |
| 136 |
type="radio" |
| 137 |
value={amountValue} |
| 138 |
id={uniqueId} |
| 139 |
className="bp-form__control bp-form_pay-radio" |
| 140 |
name="primary_payment_amount_radio" |
| 141 |
checked={selectedAmountIndex === idx} |
| 142 |
onChange={() => handleAmountSelect(amountValue, idx)} |
| 143 |
/> |
| 144 |
<label htmlFor={uniqueId}>{currencyLeft}{amountValue}{currencyRight}</label> |
| 145 |
</div> |
| 146 |
); |
| 147 |
})} |
| 148 |
</> |
| 149 |
); |
| 150 |
}; |
| 151 |
|
| 152 |
return ( |
| 153 |
<div className={`better-payment ${blockId ? 'bp-' + blockId : ''}`}> |
| 154 |
<div className="better-payment--wrapper"> |
| 155 |
<div className="better-payment--container"> |
| 156 |
<form |
| 157 |
name={`better-payment-form-${blockId}`} |
| 158 |
data-better-payment={serialized} |
| 159 |
className="better-payment-form" |
| 160 |
id={`better-payment-form-${blockId}`} |
| 161 |
method="post" |
| 162 |
> |
| 163 |
<input type="hidden" name="better_payment_page_id" value={pageId} /> |
| 164 |
<input type="hidden" name="better_payment_widget_id" value={blockId} /> |
| 165 |
|
| 166 |
<div className="payment-form-layout-1 payment-form-layout"> |
| 167 |
<div className="tile is-ancestor m-0"> |
| 168 |
{showSidebar && ( |
| 169 |
<div className="tile is-parent dynamic-amount-section"> |
| 170 |
<div className="tile is-child"> |
| 171 |
<div className="dynamic-amount-section-inner has-text-white p-6"> |
| 172 |
<div className="pt-3 pb-3"> |
| 173 |
<h4 className="is-size-2-fix bp-dynamic-amount-section-amount-summary"> |
| 174 |
{currencyLeft} |
| 175 |
<span className="bp-transaction-details-amount-text">{displayAmount}</span> |
| 176 |
{currencyRight} |
| 177 |
</h4> |
| 178 |
<h4 className="is-size-5-fix pt-5 bp-dynamic-amount-section-title"> |
| 179 |
{transactionTitle} |
| 180 |
</h4> |
| 181 |
<p className="is-size-6-fix pt-3 bp-dynamic-amount-section-sub-title"> |
| 182 |
{transactionSubTitle} |
| 183 |
</p> |
| 184 |
<p className="is-size-6-fix pt-2 bp-dynamic-amount-section-amount"> |
| 185 |
{showProductTitle && ( |
| 186 |
<> |
| 187 |
{transactionDetailsProductTitle}{' '} |
| 188 |
<a className="has-text-white" href={productPermalink || '#'} style={{ textDecoration: 'none', pointerEvents: 'none' }}> |
| 189 |
{productName} |
| 190 |
</a> |
| 191 |
<br /> |
| 192 |
</> |
| 193 |
)} |
| 194 |
{amountText}{' '} |
| 195 |
<span className="bp-transaction-details-amount-text">{displayAmount}</span> |
| 196 |
</p> |
| 197 |
</div> |
| 198 |
</div> |
| 199 |
</div> |
| 200 |
</div> |
| 201 |
)} |
| 202 |
|
| 203 |
<div className={`tile is-parent has-background-white-fix form-content-section ${showSidebar ? 'is-8' : 'is-12'}`}> |
| 204 |
<div className="tile is-child"> |
| 205 |
<div className="form-content-section-inner p-6"> |
| 206 |
<div className="pt-3 pb-3"> |
| 207 |
<div className="form-content-section-fields"> |
| 208 |
{/* Payment method radio buttons */} |
| 209 |
<div className={`field-payment_method ${threeItemsClass ? threeItemsClass + '-wrap' : ''}`}> |
| 210 |
<div className="control bp-radio-box"> |
| 211 |
{paypalEnabled && ( |
| 212 |
(enabledCount > 1) ? ( |
| 213 |
<label className={`radio pr-6 payment-method-checkbox payment-method-checkbox-paypal ${threeItemsClass}`}> |
| 214 |
<input type="radio" name="payment_method" className="layout-payment-method-paypal" checked={selectedPaymentMethod === 'paypal'} onChange={() => handlePaymentMethodChange('paypal')} /> |
| 215 |
{'PayPal'} |
| 216 |
</label> |
| 217 |
) : ( |
| 218 |
<label className={`radio pr-6 mb-5 payment-method-checkbox payment-method-checkbox-paypal single-item active column has-text-centered ${threeItemsClass}`}> |
| 219 |
<input type="radio" name="payment_method" className="layout-payment-method-paypal" checked={selectedPaymentMethod === 'paypal'} onChange={() => handlePaymentMethodChange('paypal')} /> |
| 220 |
<img src={`${assetsUrl}/img/paypal.png`} alt="PayPal" /> |
| 221 |
</label> |
| 222 |
) |
| 223 |
)} |
| 224 |
|
| 225 |
{stripeEnabled && ( |
| 226 |
(enabledCount > 1) ? ( |
| 227 |
<label className={`radio pr-6 payment-method-checkbox payment-method-checkbox-stripe ${threeItemsClass}`}> |
| 228 |
<input type="radio" name="payment_method" className="layout-payment-method-stripe" checked={selectedPaymentMethod === 'stripe'} onChange={() => handlePaymentMethodChange('stripe')} /> |
| 229 |
{'Stripe'} |
| 230 |
</label> |
| 231 |
) : ( |
| 232 |
<label className={`radio pr-6 mb-5 payment-method-checkbox payment-method-checkbox-stripe single-item active column has-text-centered ${threeItemsClass}`}> |
| 233 |
<input type="radio" name="payment_method" className="layout-payment-method-stripe" checked={selectedPaymentMethod === 'stripe'} onChange={() => handlePaymentMethodChange('stripe')} /> |
| 234 |
<img src={`${assetsUrl}/img/stripe.svg`} alt="Stripe" /> |
| 235 |
</label> |
| 236 |
) |
| 237 |
)} |
| 238 |
|
| 239 |
{paystackEnabled && ( |
| 240 |
(enabledCount > 1) ? ( |
| 241 |
<label className={`radio payment-method-checkbox payment-method-checkbox-paystack ${threeItemsClass}`}> |
| 242 |
<input type="radio" name="payment_method" className="layout-payment-method-paystack" checked={selectedPaymentMethod === 'paystack'} onChange={() => handlePaymentMethodChange('paystack')} /> |
| 243 |
{'Paystack'} |
| 244 |
</label> |
| 245 |
) : ( |
| 246 |
<label className={`radio mb-5 payment-method-checkbox payment-method-checkbox-paystack single-item active column has-text-centered ${threeItemsClass}`}> |
| 247 |
<input type="radio" name="payment_method" className="layout-payment-method-paystack" checked={selectedPaymentMethod === 'paystack'} onChange={() => handlePaymentMethodChange('paystack')} /> |
| 248 |
<img src={`${assetsUrl}/img/paystack.svg`} alt="Paystack" /> |
| 249 |
</label> |
| 250 |
) |
| 251 |
)} |
| 252 |
</div> |
| 253 |
</div> |
| 254 |
|
| 255 |
{/* Form fields */} |
| 256 |
{formFields.map((item, index) => { |
| 257 |
const { displayInline, fieldWidth, label, placeholder, primaryFieldType, required, show, type } = item; |
| 258 |
|
| 259 |
// --- Repeater vars (mirrors layout-repeater-vars.php) --- |
| 260 |
const isPaymentAmountField = primaryFieldType === 'primary_payment_amount'; |
| 261 |
const isPrimaryEmailField = primaryFieldType === 'primary_email'; |
| 262 |
const isPrimaryField = [ |
| 263 |
'primary_first_name', 'primary_last_name', 'primary_email', |
| 264 |
'primary_payment_amount', 'primary_coupon_code', 'primary_reference_number' |
| 265 |
].includes(primaryFieldType); |
| 266 |
|
| 267 |
// Field name: use primaryFieldType for known types, else convert label to snake_case |
| 268 |
const fieldName = isPrimaryField ? primaryFieldType : titleToSnake(label); |
| 269 |
|
| 270 |
// Required logic |
| 271 |
const isRequired = required || isPaymentAmountField || isPrimaryEmailField; |
| 272 |
const requiredClass = isRequired ? ' required' : ''; |
| 273 |
const requiredPlaceholder = isRequired ? ' *' : ''; |
| 274 |
|
| 275 |
// Visibility |
| 276 |
const visibleClass = show ? '' : ' is-hidden'; |
| 277 |
|
| 278 |
// Field type overrides |
| 279 |
let fieldType = type || 'text'; |
| 280 |
if (isPaymentAmountField) fieldType = 'number'; |
| 281 |
if (isPrimaryEmailField) fieldType = 'email'; |
| 282 |
|
| 283 |
// Placeholder |
| 284 |
const fieldPlaceholder = (placeholder || '') + requiredPlaceholder; |
| 285 |
|
| 286 |
// Payment amount field class |
| 287 |
const paymentAmountFieldClass = isPaymentAmountField ? 'bp-custom-payment-amount' : ''; |
| 288 |
|
| 289 |
// Inline display |
| 290 |
const inlineClass = displayInline ? ' field-display-inline' : ''; |
| 291 |
const inlineStyle = displayInline && fieldWidth ? { width: `${fieldWidth}%` } : {}; |
| 292 |
|
| 293 |
return ( |
| 294 |
<div key={index}> |
| 295 |
{/* Amount list before payment amount field */} |
| 296 |
{isPaymentAmountField && ( |
| 297 |
<div className={`bp-payment-amount-wrap ${layoutPutAmountFieldHideShow}`}> |
| 298 |
{renderAmountList()} |
| 299 |
</div> |
| 300 |
)} |
| 301 |
|
| 302 |
<div |
| 303 |
className={`better-payment-field-advanced-layout field-${fieldName} pt-5 elementor-repeater-item-${index}${inlineClass} ${paymentAmountFieldClass}${visibleClass}${isPaymentAmountField ? ' ' + layoutPutAmountFieldHideShow : ''}`} |
| 304 |
style={inlineStyle} |
| 305 |
> |
| 306 |
<div className="control has-icons-left"> |
| 307 |
<input |
| 308 |
className={`input is-medium${requiredClass}${visibleClass}`} |
| 309 |
type={fieldType} |
| 310 |
placeholder={fieldPlaceholder} |
| 311 |
name={fieldName} |
| 312 |
{...(isRequired ? { required: 'required' } : {})} |
| 313 |
{...(isPaymentAmountField ? { step: 'any', min: '1' } : {})} |
| 314 |
{...(isPaymentAmountField ? { |
| 315 |
value: hasProductPrice ? String(parseFloat(productPrice)) : enteredAmount, |
| 316 |
onChange: (e) => setEnteredAmount(e.target.value), |
| 317 |
...(hasProductPrice ? { readOnly: true } : {}), |
| 318 |
} : {})} |
| 319 |
/> |
| 320 |
{renderFieldIcon(item, isPaymentAmountField)} |
| 321 |
</div> |
| 322 |
</div> |
| 323 |
</div> |
| 324 |
); |
| 325 |
})} |
| 326 |
|
| 327 |
<div className="pb-5"></div> |
| 328 |
|
| 329 |
{/* Payment buttons */} |
| 330 |
<div className="field-process_payment_button pt-3"> |
| 331 |
<div className="control"> |
| 332 |
<div className="payment__option"> |
| 333 |
{paypalEnabled && ( |
| 334 |
<button type="button" className="button is-medium is-fullwidth better-payment-paypal-bt bp-payment-btn" style={{ display: selectedPaymentMethod === 'paypal' ? 'block' : 'none' }}> |
| 335 |
{paypalButtonText || 'Proceed to Payment'} |
| 336 |
</button> |
| 337 |
)} |
| 338 |
{stripeEnabled && ( |
| 339 |
<button type="button" className="button is-medium is-fullwidth better-payment-stripe-bt bp-payment-btn" style={{ display: selectedPaymentMethod === 'stripe' ? 'block' : 'none' }}> |
| 340 |
{stripeButtonText || 'Proceed to Payment'} |
| 341 |
</button> |
| 342 |
)} |
| 343 |
{paystackEnabled && ( |
| 344 |
<button type="button" className="button is-medium is-fullwidth better-payment-paystack-bt bp-payment-btn" style={{ display: selectedPaymentMethod === 'paystack' ? 'block' : 'none' }}> |
| 345 |
{paystackButtonText || 'Proceed to Payment'} |
| 346 |
</button> |
| 347 |
)} |
| 348 |
</div> |
| 349 |
</div> |
| 350 |
</div> |
| 351 |
</div> |
| 352 |
</div> |
| 353 |
</div> |
| 354 |
</div> |
| 355 |
</div> |
| 356 |
</div> |
| 357 |
</div> |
| 358 |
</form> |
| 359 |
</div> |
| 360 |
</div> |
| 361 |
</div> |
| 362 |
); |
| 363 |
}; |
| 364 |
|
| 365 |
export default Layout1; |
| 366 |
|