Merge branch 'PCP-3649-fastlane-backend-logic-for-blocks' of github.com:woocommerce/woocommerce-paypal-payments into PCP-3380-prepare-fastlane-integration-on-block-checkout

This commit is contained in:
Daniel Dudzic 2024-09-14 00:21:27 +02:00
commit 75c808216c
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
8 changed files with 250 additions and 79 deletions

View file

@ -0,0 +1,42 @@
import { useCallback } from '@wordpress/element';
const useHandlePaymentSetup = (
emitResponse,
card,
paymentComponent,
tokenizedCustomerData
) => {
return useCallback( async () => {
const isRyanFlow = !! card?.id;
let cardToken = card?.id;
if ( ! cardToken && paymentComponent ) {
cardToken = await paymentComponent
.getPaymentToken( tokenizedCustomerData )
.then( ( response ) => response.id );
}
if ( ! cardToken ) {
return {
type: emitResponse.responseTypes.ERROR,
message: 'Could not process the payment (tokenization error)',
};
}
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
fastlane_member: isRyanFlow,
axo_nonce: cardToken,
},
},
};
}, [
card,
paymentComponent,
tokenizedCustomerData,
] );
};
export default useHandlePaymentSetup;

View file

@ -0,0 +1,24 @@
import { useEffect, useCallback } from '@wordpress/element';
const usePaymentSetupEffect = ( onPaymentSetup, handlePaymentSetup ) => {
/**
* `onPaymentSetup()` fires when we enter the "PROCESSING" state in the checkout flow.
* It pre-processes the payment details and returns data for server-side processing.
*/
useEffect( () => {
const unsubscribe = onPaymentSetup( handlePaymentSetup );
return () => {
unsubscribe();
};
}, [ onPaymentSetup, handlePaymentSetup ] );
const handlePaymentLoad = useCallback( ( component ) => {
// We'll return this function instead of calling setPaymentComponent directly
return component;
}, [] );
return { handlePaymentLoad };
};
export default usePaymentSetupEffect;

View file

@ -0,0 +1,49 @@
import { useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
export const useTokenizeCustomerData = () => {
const customerData = useSelect( ( select ) =>
select( 'wc/store/cart' ).getCustomerData()
);
const isValidAddress = ( address ) => {
// At least one name must be present.
if ( ! address.first_name && ! address.last_name ) {
return false;
}
// Street, city, postcode, country are mandatory; state is optional.
return (
address.address_1 &&
address.city &&
address.postcode &&
address.country
);
};
// Memoize the customer data to avoid unnecessary re-renders (and potential infinite loops).
return useMemo( () => {
const { billingAddress, shippingAddress } = customerData;
// Prefer billing address, but fallback to shipping address if billing address is not valid.
const mainAddress = isValidAddress( billingAddress )
? billingAddress
: shippingAddress;
return {
cardholderName: {
fullName: `${ mainAddress.first_name } ${ mainAddress.last_name }`,
},
billingAddress: {
addressLine1: mainAddress.address_1,
addressLine2: mainAddress.address_2,
adminArea1: mainAddress.state,
adminArea2: mainAddress.city,
postalCode: mainAddress.postcode,
countryCode: mainAddress.country,
},
};
}, [ customerData ] );
};
export default useTokenizeCustomerData;

View file

@ -1,15 +1,18 @@
import { useCallback, useState } from '@wordpress/element';
import { useState } from '@wordpress/element';
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
// Hooks
import useFastlaneSdk from './hooks/useFastlaneSdk';
import useTokenizeCustomerData from './hooks/useTokenizeCustomerData';
import useCardChange from './hooks/useCardChange';
import useAxoSetup from './hooks/useAxoSetup';
import usePaymentSetup from './hooks/usePaymentSetup';
import useAxoCleanup from './hooks/useAxoCleanup';
import useHandlePaymentSetup from './hooks/useHandlePaymentSetup';
// Components
import { Payment } from './components/Payment/Payment';
import usePaymentSetupEffect from './hooks/usePaymentSetupEffect';
const gatewayHandle = 'ppcp-axo-gateway';
const ppcpConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` );
@ -25,8 +28,17 @@ const Axo = ( props ) => {
const { onPaymentSetup } = eventRegistration;
const [ shippingAddress, setShippingAddress ] = useState( null );
const [ card, setCard ] = useState( null );
const [ paymentComponent, setPaymentComponent ] = useState( null );
const fastlaneSdk = useFastlaneSdk( axoConfig, ppcpConfig );
const tokenizedCustomerData = useTokenizeCustomerData();
const onChangeCardButtonClick = useCardChange( fastlaneSdk, setCard );
const handlePaymentSetup = useHandlePaymentSetup(
emitResponse,
card,
paymentComponent,
tokenizedCustomerData
);
useAxoSetup(
ppcpConfig,
@ -36,11 +48,13 @@ const Axo = ( props ) => {
setCard
);
usePaymentSetup( onPaymentSetup, emitResponse, card );
useAxoCleanup();
const handlePaymentLoad = useCallback( ( paymentComponent ) => {
console.log( 'Payment component loaded', paymentComponent );
}, [] );
const { handlePaymentLoad } = usePaymentSetupEffect(
onPaymentSetup,
handlePaymentSetup
);
useAxoCleanup();
const handleCardChange = ( selectedCard ) => {
console.log( 'Card selection changed', selectedCard );

View file

@ -75,6 +75,7 @@ return array(
$container->get( 'wcgateway.settings.render' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.url' ),
$container->get( 'session.handler' ),
$container->get( 'wcgateway.order-processor' ),
$container->get( 'axo.card_icons' ),
$container->get( 'axo.card_icons.axo' ),

View file

@ -28,6 +28,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WC_Payment_Gateways;
/**
* Class AxoModule
*/
@ -130,6 +132,23 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
}
);
// Enforce Fastlane to always be the first payment method in the list.
add_action(
'wc_payment_gateways_initialized',
function ( WC_Payment_Gateways $gateways ) {
if ( is_admin() ) {
return;
}
foreach ( $gateways->payment_gateways as $key => $gateway ) {
if ( $gateway->id === AxoGateway::ID ) {
unset( $gateways->payment_gateways[ $key ] );
array_unshift( $gateways->payment_gateways, $gateway );
break;
}
}
}
);
// Force 'cart-block' and 'cart' Smart Button locations in the settings.
add_action(
'admin_init',

View file

@ -10,15 +10,15 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo\Gateway;
use Psr\Log\LoggerInterface;
use Exception;
use WC_Order;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewaySettingsRendererTrait;
@ -26,12 +26,15 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
/**
* Class AXOGateway.
*/
class AxoGateway extends WC_Payment_Gateway {
use OrderMetaTrait, GatewaySettingsRendererTrait;
use OrderMetaTrait, GatewaySettingsRendererTrait, ProcessPaymentTrait;
const ID = 'ppcp-axo-gateway';
@ -119,18 +122,26 @@ class AxoGateway extends WC_Payment_Gateway {
*/
protected $logger;
/**
* The Session Handler.
*
* @var SessionHandler
*/
protected $session_handler;
/**
* AXOGateway constructor.
*
* @param SettingsRenderer $settings_renderer The settings renderer.
* @param ContainerInterface $ppcp_settings The settings.
* @param string $wcgateway_module_url The WcGateway module URL.
* @param SessionHandler $session_handler The session handler.
* @param OrderProcessor $order_processor The Order processor.
* @param array $card_icons The card icons.
* @param array $card_icons_axo The card icons.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param ShippingPreferenceFactory $shipping_preference_factory The shipping preference factory.
* @param ShippingPreferenceFactory $shipping_preference_factory Shipping preference factory.
* @param TransactionUrlProvider $transaction_url_provider The transaction url provider.
* @param Environment $environment The environment.
* @param LoggerInterface $logger The logger.
@ -139,6 +150,7 @@ class AxoGateway extends WC_Payment_Gateway {
SettingsRenderer $settings_renderer,
ContainerInterface $ppcp_settings,
string $wcgateway_module_url,
SessionHandler $session_handler,
OrderProcessor $order_processor,
array $card_icons,
array $card_icons_axo,
@ -154,6 +166,7 @@ class AxoGateway extends WC_Payment_Gateway {
$this->settings_renderer = $settings_renderer;
$this->ppcp_settings = $ppcp_settings;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->session_handler = $session_handler;
$this->order_processor = $order_processor;
$this->card_icons = $card_icons;
$this->card_icons_axo = $card_icons_axo;
@ -213,11 +226,20 @@ class AxoGateway extends WC_Payment_Gateway {
* Processes the order.
*
* @param int $order_id The WC order ID.
*
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return $this->handle_payment_failure(
null,
new GatewayGenericException( new Exception( 'WC order was not found.' ) )
);
}
try {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$fastlane_member = wc_clean( wp_unslash( $_POST['fastlane_member'] ?? '' ) );
if ( $fastlane_member ) {
@ -226,26 +248,51 @@ class AxoGateway extends WC_Payment_Gateway {
$wc_order->save();
}
// The `axo_nonce` is not a WP nonce, but a card-token generated by the JS SDK.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$token = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
$order = $this->create_paypal_order( $wc_order, $token );
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
} catch ( Exception $exception ) {
return $this->handle_payment_failure( $wc_order, $exception );
}
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
}
/**
* Create a new PayPal order from the existing WC_Order instance.
*
* @param WC_Order $wc_order The WooCommerce order to use as a base.
* @param string $payment_token The payment token, generated by the JS SDK.
*
* @return Order The PayPal order.
*/
protected function create_paypal_order( WC_Order $wc_order, string $payment_token ) : Order {
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$nonce = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
try {
$shipping_preference = $this->shipping_preference_factory->from_state(
$purchase_unit,
'checkout'
);
$payment_source_properties = new \stdClass();
$payment_source_properties->single_use_token = $nonce;
$payment_source_properties = (object) array(
'single_use_token' => $payment_token,
);
$payment_source = new PaymentSource(
'card',
$payment_source_properties
);
$order = $this->order_endpoint->create(
return $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
null,
@ -256,37 +303,6 @@ class AxoGateway extends WC_Payment_Gateway {
array(),
$payment_source
);
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$this->logger->error( $error );
wc_add_notice( $error, 'error' );
$wc_order->update_status(
'failed',
$error
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
WC()->cart->empty_cart();
$result = array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
return $result;
}
/**

View file

@ -13,6 +13,7 @@ use Exception;
use Throwable;
use WC_Order;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
/**
* Trait ProcessPaymentTrait
@ -75,7 +76,12 @@ trait ProcessPaymentTrait {
* @return string
*/
protected function format_exception( Throwable $exception ) : string {
$output = $exception->getMessage() . ' ' . basename( $exception->getFile() ) . ':' . $exception->getLine();
$message = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$message = $exception->get_details( $message );
}
$output = $message . ' ' . basename( $exception->getFile() ) . ':' . $exception->getLine();
$prev = $exception->getPrevious();
if ( ! $prev ) {
return $output;