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' );