diff --git a/paypal/controllers/FrmPayPalLiteActionsController.php b/paypal/controllers/FrmPayPalLiteActionsController.php index d1e2030cf2..ff96362408 100644 --- a/paypal/controllers/FrmPayPalLiteActionsController.php +++ b/paypal/controllers/FrmPayPalLiteActionsController.php @@ -1009,7 +1009,7 @@ function ( $settings_for_action, $payment_action ) use ( &$payment_action_by_id 'intent' => $intent, 'currency' => strtoupper( $action->post_content['currency'] ?? 'USD' ), 'merchant-id' => FrmPayPalLiteConnectHelper::get_merchant_id(), - 'enable-funding' => 'venmo', + 'enable-funding' => 'venmo,applepay', ); if ( 'subscription' === $intent ) { @@ -1051,6 +1051,7 @@ function ( $settings_for_action, $payment_action ) use ( &$payment_action_by_id if ( $include_buttons ) { $components[] = 'buttons'; $components[] = 'googlepay'; + $components[] = 'applepay'; } if ( $include_card_fields ) { @@ -1084,6 +1085,7 @@ function ( $settings_for_action, $payment_action ) use ( &$payment_action_by_id $sdk_url = add_query_arg( $query_args, 'https://www.paypal.com/sdk/js' ); wp_register_script( 'paypal-sdk', $sdk_url, array(), null, false ); + wp_register_script( 'apple-pay-sdk', 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js', array(), null, false ); $has_break = FrmAppHelper::pro_is_installed() && (bool) FrmField::get_all_types_in_form( $form_id, 'break' ); @@ -1097,15 +1099,14 @@ function ( $settings_for_action, $payment_action ) use ( &$payment_action_by_id */ function ( $tag, $handle ) use ( $has_break ) { if ( 'paypal-sdk' === $handle ) { - $attributes = ' data-partner-attribution-id="' . esc_attr( FrmPayPalLiteConnectHelper::get_bn_code() ) . '"'; - - if ( $has_break ) { - $attributes .= ' async'; - } - + $attributes = ' async data-partner-attribution-id="' . esc_attr( FrmPayPalLiteConnectHelper::get_bn_code() ) . '"'; return str_replace( ' src=', $attributes . ' src=', $tag ); } + if ( in_array( $handle, array( 'apple-pay-sdk', 'google-pay' ), true ) ) { + return str_replace( ' src=', ' async src=', $tag ); + } + if ( $has_break && 'formidable-paypal' === $handle ) { return str_replace( ' src=', ' async src=', $tag ); } @@ -1123,7 +1124,7 @@ function ( $tag, $handle ) use ( $has_break ) { FrmAppHelper::plugin_version() ); - $dependencies = array( 'paypal-sdk', 'formidable' ); + $dependencies = array( 'paypal-sdk', 'apple-pay-sdk', 'formidable' ); $script_url = FrmPayPalLiteAppHelper::plugin_url() . 'js/frontend.js'; wp_enqueue_script( diff --git a/paypal/controllers/FrmPayPalLiteAppController.php b/paypal/controllers/FrmPayPalLiteAppController.php index 204d0f48f5..8292048765 100644 --- a/paypal/controllers/FrmPayPalLiteAppController.php +++ b/paypal/controllers/FrmPayPalLiteAppController.php @@ -272,17 +272,19 @@ private static function get_valid_payment_sources() { $sources = array( 'card', 'paypal', - 'mybank', + 'apple_pay', 'bancontact', 'blik', 'eps', + 'giropay', + 'ideal', + 'mybank', 'p24', - 'trustly', - 'satispay', 'sepa', - 'ideal', - 'paylater', + 'sofort', + 'trustly', 'venmo', + 'paylater', 'google_pay', ); diff --git a/paypal/css/frontend.css b/paypal/css/frontend.css index bf1f1b3d3a..702dc51748 100644 --- a/paypal/css/frontend.css +++ b/paypal/css/frontend.css @@ -110,6 +110,15 @@ width: auto; } +.frm-payment-method-apple-pay-icon { + height: 24px; +} + +.frm-payment-method-apple-pay-icon svg { + height: 24px; + width: auto; +} + .frm-payment-method-paylater-wrap { border-bottom: 1px solid #e0e0e0; background: #fff; diff --git a/paypal/images/apple-pay.svg b/paypal/images/apple-pay.svg new file mode 100644 index 0000000000..5733474aa1 --- /dev/null +++ b/paypal/images/apple-pay.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/paypal/js/frontend.js b/paypal/js/frontend.js index 743728b1c8..020086b864 100644 --- a/paypal/js/frontend.js +++ b/paypal/js/frontend.js @@ -37,6 +37,9 @@ /** Cached Google Pay config from paypal.Googlepay().config(). */ let googlePayConfig = null; + /** Cached Apple Pay config from paypal.Applepay().config(). */ + let applePayConfig = null; + // ---- Constants ---- /** @@ -48,6 +51,7 @@ venmo: 'Venmo', paylater: 'Pay Later', google_pay: 'Google Pay', + apple_pay: 'Apple Pay', bancontact: 'Bancontact', blik: 'BLIK', eps: 'EPS', @@ -255,6 +259,19 @@ } ); } } + + // --- Apple Pay --- + if ( buttonsAreEnabled && ! isRecurring ) { + const applePayEligibilityResult = await checkApplePayEligibility(); + if ( applePayEligibilityResult === '' ) { + registerMethod( 'apple_pay', { + eligible: true, + render: renderApplePayButton + } ); + } else { + console.log( 'Apple Pay not available:', applePayEligibilityResult ); + } + } } /** @@ -342,11 +359,18 @@ } else if ( key === 'google_pay' ) { markWrap.classList.add( 'frm-payment-method-google-pay-icon' ); const img = document.createElement( 'img' ); - const baseUrl = frmPayPalVars.imagesUrl || ''; img.src = `${ baseUrl }gpay.svg`; img.alt = 'Google Pay'; img.height = 24; markWrap.append( img ); + } else if ( key === 'apple_pay' ) { + markWrap.classList.add( 'frm-payment-method-apple-pay-icon' ); + const img = document.createElement( 'img' ); + img.src = `${ baseUrl }apple-pay.svg`; + img.alt = 'Apple Pay'; + img.height = 24; + img.style.width = 'auto'; + markWrap.append( img ); } label.append( radio ); @@ -854,6 +878,145 @@ } } + // ---- Apple Pay ---- + + /** + * Check if Apple Pay is eligible (without rendering). + * + * @return {Promise} An empty string if Apple Pay is supported and ready to accept payments in the current environment, or a string with the reason for ineligibility. + */ + async function checkApplePayEligibility() { + if ( 'function' !== typeof paypal.Applepay ) { + return 'PayPal Apple Pay SDK not loaded'; + } + + if ( ! window.ApplePaySession ) { + return 'Not on Apple device'; + } + + if ( ! ApplePaySession.canMakePayments() ) { + return 'Apple Pay not configured on device'; + } + + // Use paypal.Applepay().config() as the definitive eligibility check (per PayPal multiparty docs). + try { + applePayConfig = await paypal.Applepay().config(); + + if ( ! applePayConfig || ! applePayConfig.isEligible ) { + return 'PayPal reports Apple Pay is not eligible for this merchant/domain'; + } + } catch ( err ) { + return 'Apple Pay config check failed: ' + err.message; + } + + return ''; + } + + /** + * Render the Apple Pay button into its method container. + */ + async function renderApplePayButton() { + const method = paymentMethods.get( 'apple_pay' ); + if ( ! method ) { + return; + } + + const container = method.containerEl; + container.innerHTML = ''; + + const btn = document.createElement( 'apple-pay-button' ); + btn.setAttribute( 'buttonstyle', 'black' ); + btn.setAttribute( 'type', 'buy' ); + btn.setAttribute( 'locale', 'en' ); + btn.style.width = '100%'; + btn.style.height = '40px'; + + btn.addEventListener( 'click', onApplePayButtonClick ); + container.appendChild( btn ); + } + + /** + * Handle click on the Apple Pay button. + * Creates an ApplePaySession synchronously (required by Apple) and processes the payment via PayPal. + */ + function onApplePayButtonClick() { + if ( ! applePayConfig ) { + console.error( 'Apple Pay config not available' ); + return; + } + + const paymentRequest = { + countryCode: applePayConfig.countryCode, + merchantCapabilities: applePayConfig.merchantCapabilities, + supportedNetworks: applePayConfig.supportedNetworks, + currencyCode: applePayConfig.currencyCode || 'USD', + total: { + label: document.title || 'Payment', + type: 'final', + amount: getFormTotal(), + }, + }; + + // ApplePaySession MUST be created synchronously inside the click handler. + const session = new ApplePaySession( 4, paymentRequest ); + const applepay = paypal.Applepay(); + + session.onvalidatemerchant = ( event ) => { + applepay.validateMerchant( { + validationUrl: event.validationURL, + displayName: document.title || 'Payment' + } ) + .then( ( validateResult ) => { + session.completeMerchantValidation( validateResult.merchantSession ); + } ) + .catch( ( validateError ) => { + console.error( 'Apple Pay merchant validation failed', validateError ); + session.abort(); + } ); + }; + + session.onpaymentauthorized = ( event ) => { + createOrderForApplePay() + .then( ( orderId ) => { + return applepay.confirmOrder( { + orderId: orderId, + token: event.payment.token, + billingContact: event.payment.billingContact + } ) + .then( () => { + session.completePayment( ApplePaySession.STATUS_SUCCESS ); + onApprove( { + orderID: orderId, + paymentSource: 'apple_pay' + } ); + } ); + } ) + .catch( ( err ) => { + console.error( 'Apple Pay payment failed', err ); + session.completePayment( ApplePaySession.STATUS_FAILURE ); + } ); + }; + + session.oncancel = () => { + onCancel(); + }; + + session.begin(); + } + + /** + * Get the form total amount as a string. + * + * @return {string} The total amount. + */ + function getFormTotal() { + const totalField = thisForm.querySelector( '[data-frmtotal]' ); + if ( totalField && totalField.value ) { + return parseFloat( totalField.value ).toFixed( 2 ); + } + return '0.00'; + } + // ---- AJAX / Order Creation ---- /** @@ -964,6 +1127,39 @@ return orderData.data.orderID; } + /** + * Create a PayPal order specifically for Apple Pay. + * + * @return {Promise} The PayPal order ID. + */ + async function createOrderForApplePay() { + const formData = new FormData( thisForm ); + formData.append( 'action', 'frm_paypal_create_order' ); + formData.append( 'nonce', frmPayPalVars.nonce ); + formData.append( 'payment_source', 'apple_pay' ); + + formData.delete( 'frm_action' ); + formData.delete( 'form_key' ); + formData.delete( 'item_key' ); + + const response = await fetch( frmPayPalVars.ajax, { + method: 'POST', + body: formData + } ); + + if ( ! response.ok ) { + throw new Error( 'Failed to create PayPal order for Apple Pay' ); + } + + const orderData = await response.json(); + + if ( ! orderData.success || ! orderData.data.orderID ) { + throw new Error( orderData.data || 'Failed to create PayPal order for Apple Pay' ); + } + + return orderData.data.orderID; + } + async function createVaultSetupToken() { const formData = new FormData( thisForm ); formData.append( 'action', 'frm_paypal_create_vault_setup_token' );