PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
← All changes | paypal/js/frontend.js +153 -95 6.32 → trunk View file →
@@ -154,9 +154,13 @@
154 154 return;
155 155 }
156 156
157 157 // 2. Build the radio selector UI, then render marks after it's in the DOM.
158 + // Hide the radio group if there's only one payment method available.
158 159 const radioGroup = buildRadioGroup();
160 + if ( paymentMethods.size === 1 ) {
161 + radioGroup.style.display = 'none';
162 + }
159 163 cardElement.append( radioGroup );
160 164 renderMarks();
161 165
162 166 // 3. Build a container area for each method's UI (buttons / card fields).
@@ -218,9 +222,10 @@
218 222 async function discoverPaymentMethods( opts ) {
219 223 const { cardFieldsAreSupported, buttonsAreEnabled, isRecurring } = opts;
220 224
221 225 // --- Card Fields ---
222 - if ( cardFieldsAreSupported ) {
226 + // Card fields are not supported for recurring payments
227 + if ( cardFieldsAreSupported && ! isRecurring ) {
223 228 const cardFields = createCardFieldsSDKInstance();
224 229 if ( cardFields?.isEligible() ) {
225 230 cardFieldsInstance = cardFields;
226 231 registerMethod( 'card', {
@@ -297,11 +302,11 @@
297 302 if ( ! buttonsAreEnabled || isRecurring ) {
298 303 return;
299 304 }
300 305
301 - // Skip (and avoid the SDK wait) when Google Pay / Apple Pay were not enqueued
302 - // server-side, e.g. via the frm_include_google_pay_apple_pay filter or non-SSL.
303 - if ( ! frmPayPalVars.includeGooglePayApplePay ) {
306 + // Skip (and avoid the SDK wait) when neither wallet was enqueued server-side,
307 + // e.g. via the frm_include_google_pay_apple_pay filter or non-SSL.
308 + if ( ! frmPayPalVars.includeGooglePay && ! frmPayPalVars.includeApplePay ) {
304 309 return;
305 310 }
306 311
307 312 // Resolve both eligibility checks in parallel so the combined wait is bounded by
@@ -306,12 +311,14 @@
306 311
307 312 // Resolve both eligibility checks in parallel so the combined wait is bounded by
308 313 // the slower of the two, then register them in a fixed order (Google Pay, then
309 314 // Apple Pay). Registration happens before the selector is built, so they render
310 - // together with the other methods.
315 + // together with the other methods. A wallet turned off server-side, e.g. via
316 + // frm_paypal_commerce_include_google_pay or frm_paypal_commerce_include_apple_pay,
317 + // skips its check so it never waits on an SDK that was never enqueued.
311 318 const [ googlePayEligible, applePayEligible ] = await Promise.all( [
312 - resolveGooglePayEligibility(),
313 - resolveApplePayEligibility()
319 + frmPayPalVars.includeGooglePay ? resolveGooglePayEligibility() : false,
320 + frmPayPalVars.includeApplePay ? resolveApplePayEligibility() : false
314 321 ] );
315 322
316 323 if ( googlePayEligible ) {
317 324 registerMethod( 'google_pay', {
@@ -702,8 +709,13 @@
702 709 *
703 710 * @return {Object|null} The card fields instance.
704 711 */
705 712 function createCardFieldsSDKInstance() {
713 + if ( isRecurring ) {
714 + // Credit cards are only supported for one time payments.
715 + return null;
716 + }
717 +
706 718 try {
707 719 const config = {
708 720 onError,
709 721 style: frmPayPalVars.style,
@@ -711,15 +723,10 @@
711 723 onChange: onCardFieldsChange
712 724 }
713 725 };
714 726
715 - if ( isRecurring ) {
716 - config.createVaultSetupToken = createVaultSetupToken;
717 - config.onApprove = onVaultApprove;
718 - } else {
719 - config.createOrder = createOrder;
720 - config.onApprove = onApprove;
721 - }
727 + config.createOrder = createOrder;
728 + config.onApprove = onApprove;
722 729
723 730 return window.paypal.CardFields( config );
724 731 } catch ( err ) {
725 732 console.error( 'Failed to create CardFields instance', err );
@@ -914,9 +921,9 @@
914 921 * @return {google.payments.api.PaymentsClient} The payments client instance.
915 922 */
916 923 function getGooglePaymentsClient() {
917 924 return new google.payments.api.PaymentsClient( {
918 - environment: 'TEST',
925 + environment: frmPayPalVars.mode === 'test' ? 'TEST' : 'PRODUCTION',
919 926 paymentDataCallbacks: {
920 927 onPaymentAuthorized
921 928 }
922 929 } );
@@ -1191,8 +1198,14 @@
1191 1198 if ( approvalStatus === 'PAYER_ACTION_REQUIRED' ) {
1192 1199 await applePayInstance.initiatePayerAction( { orderId } );
1193 1200 }
1194 1201
1202 + if ( approvalStatus !== 'APPROVED' && approvalStatus !== 'COMPLETED' ) {
1203 + session.completePayment( ApplePaySession.STATUS_FAILURE );
1204 + sessionCompleted = true;
1205 + return;
1206 + }
1207 +
1195 1208 session.completePayment( ApplePaySession.STATUS_SUCCESS );
1196 1209 sessionCompleted = true;
1197 1210
1198 1211 onApprove( {
@@ -1353,73 +1366,11 @@
1353 1366
1354 1367 return orderData.data.orderID;
1355 1368 }
1356 1369
1357 - async function createVaultSetupToken() {
1358 - const formData = new FormData( thisForm );
1359 - formData.append( 'action', 'frm_paypal_create_vault_setup_token' );
1360 - formData.append( 'nonce', frmPayPalVars.nonce );
1361 - formData.append( 'payment_source', 'card' );
1362 -
1363 - formData.delete( 'frm_action' );
1364 - formData.delete( 'form_key' );
1365 - formData.delete( 'item_key' );
1366 -
1367 - const response = await fetch( frmPayPalVars.ajax, {
1368 - method: 'POST',
1369 - body: formData
1370 - } );
1371 -
1372 - if ( ! response.ok ) {
1373 - throw new Error( 'Failed to create PayPal vault setup token' );
1374 - }
1375 -
1376 - const tokenData = await response.json();
1377 -
1378 - if ( ! tokenData.success || ! tokenData.data.token ) {
1379 - console.error( 'Vault setup token response:', tokenData );
1380 - throwServerError( tokenData.data, 'Failed to create PayPal vault setup token', 'create_vault_token' );
1381 - }
1382 -
1383 - return tokenData.data.token;
1384 - }
1385 -
1386 1370 // ---- Payment Callbacks ----
1387 1371
1388 1372 /**
1389 - * Handle vault approval for card field subscriptions.
1390 - * Receives the vaultSetupToken, sends it to the server to create
1391 - * a payment token and subscription, then submits the form.
1392 - *
1393 - * @param {Object} data The approval data containing vaultSetupToken.
1394 - */
1395 - async function onVaultApprove( data ) {
1396 - if ( 'NO' === data.liabilityShift || 'UNKNOWN' === data.liabilityShift ) {
1397 - onError( new Error( 'This payment was flagged as possible fraud and has been rejected.' ) );
1398 - return;
1399 - }
1400 -
1401 - try {
1402 - let vaultInput = thisForm.querySelector( 'input[name="vault_setup_token"]' );
1403 - if ( ! vaultInput ) {
1404 - vaultInput = document.createElement( 'input' );
1405 - vaultInput.type = 'hidden';
1406 - vaultInput.name = 'vault_setup_token';
1407 - thisForm.append( vaultInput );
1408 - }
1409 - vaultInput.value = data.vaultSetupToken;
1410 -
1411 - const subscriptionID = await createSubscription( data );
1412 - await onApprove( {
1413 - subscriptionID,
1414 - paymentSource: 'card'
1415 - } );
1416 - } catch ( err ) {
1417 - onError( err );
1418 - }
1419 - }
1420 -
1421 - /**
1422 1373 * Handle approved payment.
1423 1374 *
1424 1375 * @param {Object} data The approval data containing orderID.
1425 1376 */
@@ -1453,18 +1404,15 @@
1453 1404 paymentSourceInput.value = data.paymentSource || 'card';
1454 1405
1455 1406 thisForm.append( paymentSourceInput );
1456 1407
1408 + // If using the PayPal buttons to submit, there will not be a submitEvent.
1457 1409 if ( ! submitEvent ) {
1458 1410 submitEvent = new Event( 'submit', { cancelable: true, bubbles: true } );
1459 1411 submitEvent.target = thisForm;
1460 1412 }
1461 1413
1462 - if ( typeof frmFrontForm.submitFormManual === 'function' ) {
1463 - frmFrontForm.submitFormManual( submitEvent, thisForm );
1464 - } else {
1465 - thisForm.submit();
1466 - }
1414 + frmFrontForm.submitFormManual( submitEvent, thisForm );
1467 1415 }
1468 1416
1469 1417 /**
1470 1418 * Handle payment errors.
@@ -1621,9 +1569,13 @@
1621 1569 return fallback;
1622 1570 }
1623 1571
1624 1572 if ( 'string' === typeof err ) {
1625 - return parsePayPalErrorString( err ) || err;
1573 + const parsed = parsePayPalErrorString( err );
1574 + if ( parsed ) {
1575 + return parsed;
1576 + }
1577 + return mapPayPalErrorCode( err ) || err;
1626 1578 }
1627 1579
1628 1580 // PayPal SDK sometimes nests the payload under `err.data` or `err.response`.
1629 1581 const payloads = [ err, err.data, err.response ].filter( Boolean );
@@ -1634,9 +1586,13 @@
1634 1586 }
1635 1587 }
1636 1588
1637 1589 if ( err.message ) {
1638 - return parsePayPalErrorString( err.message ) || err.message;
1590 + const parsed = parsePayPalErrorString( err.message );
1591 + if ( parsed ) {
1592 + return parsed;
1593 + }
1594 + return mapPayPalErrorCode( err.message ) || err.message;
1639 1595 }
1640 1596
1641 1597 return fallback;
1642 1598 }
@@ -1641,8 +1597,25 @@
1641 1597 return fallback;
1642 1598 }
1643 1599
1644 1600 /**
1601 + * Map PayPal error codes to user-friendly messages.
1602 + *
1603 + * @param {string} code The PayPal error code (e.g. INVALID_CVV).
1604 + * @return {string} The user-friendly message, or empty string if not mapped.
1605 + */
1606 + function mapPayPalErrorCode( code ) {
1607 + const codeMap = {
1608 + INVALID_CVV: 'Please enter a valid CVV code.',
1609 + INVALID_CARD_NUMBER: 'Please enter a valid card number.',
1610 + INVALID_EXPIRY: 'Please enter a valid expiry date.',
1611 + };
1612 +
1613 + const upperCode = code.toUpperCase();
1614 + return codeMap[ upperCode ] || '';
1615 + }
1616 +
1617 + /**
1645 1618 * Extract the first `description` from a PayPal `details` array.
1646 1619 *
1647 1620 * @param {Array} details The PayPal error details array.
1648 1621 * @return {string} The description, or an empty string if none.
@@ -1806,21 +1779,13 @@
1806 1779 if ( meta.name ) {
1807 1780 submitArgs.cardholderName = meta.name;
1808 1781 }
1809 1782
1810 - /*
1811 - TODO Add the billing address here as well.
1812 - Stripe calls a window.frmProForm.addAddressMeta function.
1813 - That's included in frmstrp.js though, so we need to add a script in Pro for PayPal as well.
1783 + const billingAddress = getBillingAddress();
1814 1784
1815 - billingAddress: {
1816 - addressLine1: '555 Billing Ave',
1817 - adminArea1: 'NY',
1818 - adminArea2: 'New York',
1819 - postalCode: '10001',
1820 - countryCode: 'US'
1785 + if ( billingAddress ) {
1786 + submitArgs.billingAddress = billingAddress;
1821 1787 }
1822 - */
1823 1788
1824 1789 try {
1825 1790 await cardFieldsInstance.submit( submitArgs );
1826 1791 } catch ( err ) {
@@ -1830,8 +1795,101 @@
1830 1795 enableSubmit();
1831 1796 }
1832 1797 reportErrorToServer( err, 'card_submit' );
1833 1798 }
1799 + }
1800 +
1801 + /**
1802 + * Build the billing address for the card fields from the address field mapped in the payment action.
1803 + *
1804 + * @since 6.34
1805 + *
1806 + * @return {Object|null} Billing address for the card fields submit args, or null when no address is available.
1807 + */
1808 + function getBillingAddress() {
1809 + let addressID = '';
1810 +
1811 + getPayPalSettings().forEach( function( setting ) {
1812 + if ( setting.address ) {
1813 + addressID = setting.address;
1814 + }
1815 + } );
1816 +
1817 + if ( '' === addressID ) {
1818 + return null;
1819 + }
1820 +
1821 + let prefix = '';
1822 + let addressContainer = document.querySelector( `#frm_field_${ addressID }_container, .frm_field_${ addressID }_container` );
1823 +
1824 + if ( ! addressContainer ) {
1825 + const line1Input = document.querySelector( `input[name="item_meta[${ addressID }][line1]"]` );
1826 + if ( line1Input ) {
1827 + prefix = `${ addressID }][`;
1828 + addressContainer = line1Input.parentNode;
1829 + }
1830 + }
1831 +
1832 + if ( ! addressContainer ) {
1833 + return null;
1834 + }
1835 +
1836 + const getSubFieldValue = function( name ) {
1837 + const input = addressContainer.querySelector( `input[name$="[${ prefix }${ name }]"], select[name$="[${ prefix }${ name }]"]` );
1838 + return input?.value ? input.value : '';
1839 + };
1840 +
1841 + const subFieldMapping = {
1842 + line1: 'addressLine1',
1843 + line2: 'addressLine2',
1844 + city: 'adminArea2',
1845 + state: 'adminArea1',
1846 + zip: 'postalCode'
1847 + };
1848 +
1849 + const billingAddress = {};
1850 +
1851 + Object.keys( subFieldMapping ).forEach( function( name ) {
1852 + const value = getSubFieldValue( name );
1853 + if ( value ) {
1854 + billingAddress[ subFieldMapping[ name ] ] = value;
1855 + }
1856 + } );
1857 +
1858 + if ( ! billingAddress.addressLine1 ) {
1859 + return null;
1860 + }
1861 +
1862 + const countryCode = getCountryCode( addressContainer, prefix );
1863 +
1864 + if ( countryCode ) {
1865 + billingAddress.countryCode = countryCode;
1866 + }
1867 +
1868 + return billingAddress;
1869 + }
1870 +
1871 + /**
1872 + * Get the two letter country code for the filled address.
1873 + * The country dropdown holds the code in a data-code attribute.
1874 + * US type address fields have a state dropdown and no country field, so US is assumed for them.
1875 + *
1876 + * @since 6.34
1877 + *
1878 + * @param {Element} addressContainer
1879 + * @param {string} prefix
1880 + * @return {string} Country code, or an empty string when the country is unknown.
1881 + */
1882 + function getCountryCode( addressContainer, prefix ) {
1883 + const countryDropdown = addressContainer.querySelector( `select[name$="[${ prefix }country]"]` );
1884 +
1885 + if ( countryDropdown ) {
1886 + const countryOption = countryDropdown.querySelector( `option[value="${ countryDropdown.value }"]` );
1887 + return countryOption?.getAttribute( 'data-code' ) || '';
1888 + }
1889 +
1890 + const stateDropdown = addressContainer.querySelector( `select[name$="[${ prefix }state]"]` );
1891 + return stateDropdown ? 'US' : '';
1834 1892 }
1835 1893
1836 1894 // ---- Price / Pay Later ----
1837 1895