Skip to content

Commit

Permalink
Customizing BNPL messaging with Appearance API (#8421)
Browse files Browse the repository at this point in the history
  • Loading branch information
gpressutto5 authored Mar 22, 2024
1 parent ee235db commit 294756f
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 52 deletions.
4 changes: 4 additions & 0 deletions changelog/appearance-api-bnp-message
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Customizing BNPL messaging with Appearance API
6 changes: 3 additions & 3 deletions client/checkout/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,13 +410,13 @@ export default class WCPayAPI {
* Saves the calculated UPE appearance values in a transient.
*
* @param {Object} appearance The UPE appearance object with style values
* @param {string} isBlocksCheckout 'true' if save request is for Blocks Checkout. Default 'false'.
* @param {string} elementsLocation The location of the elements.
*
* @return {Promise} The final promise for the request to the server.
*/
saveUPEAppearance( appearance, isBlocksCheckout = 'false' ) {
saveUPEAppearance( appearance, elementsLocation ) {
return this.request( getConfig( 'ajaxUrl' ), {
is_blocks_checkout: isBlocksCheckout,
elements_location: elementsLocation,
appearance: JSON.stringify( appearance ),
action: 'save_upe_appearance',
// eslint-disable-next-line camelcase
Expand Down
4 changes: 2 additions & 2 deletions client/checkout/blocks/payment-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ const PaymentElements = ( { api, ...props } ) => {
useEffect( () => {
async function generateUPEAppearance() {
// Generate UPE input styles.
let upeAppearance = getAppearance( true );
let upeAppearance = getAppearance( 'blocks_checkout' );
upeAppearance = await api.saveUPEAppearance(
upeAppearance,
'true'
'blocks_checkout'
);
setAppearance( upeAppearance );
}
Expand Down
5 changes: 4 additions & 1 deletion client/checkout/classic/payment-processing.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ async function initializeAppearance( api ) {
return Promise.resolve( appearance );
}

return await api.saveUPEAppearance( getAppearance() );
return await api.saveUPEAppearance(
getAppearance( 'shortcode_checkout' ),
'shortcode_checkout'
);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion client/checkout/classic/test/payment-processing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ describe( 'Stripe Payment Element mounting', () => {

expect( getAppearance ).toHaveBeenCalled();
expect( apiMock.saveUPEAppearance ).toHaveBeenCalledWith(
appearanceMock
appearanceMock,
'shortcode_checkout'
);
expect( dispatchMock ).toHaveBeenCalled();
} );
Expand Down
50 changes: 36 additions & 14 deletions client/checkout/upe-styles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ const appearanceSelectors = {
'body',
],
},
bnplProductPage: {
appendTarget: '.product .cart .quantity',
upeThemeInputSelector: '.product .cart .quantity .qty',
upeThemeLabelSelector: '.product .cart .quantity label',
rowElement: 'div',
validClasses: [ 'input-text' ],
invalidClasses: [ 'input-text', 'has-error' ],
backgroundSelectors: [
'#payment-method-message',
'#main > .product > div.summary.entry-summary',
'#main > .product',
'#main',
'body',
],
},

/**
* Update selectors to use alternate if not present on DOM.
Expand Down Expand Up @@ -88,21 +103,28 @@ const appearanceSelectors = {
/**
* Returns selectors based on checkout type.
*
* @param {boolean} isBlocksCheckout True ff block checkout. Default false.
* @param {boolean} elementsLocation The location of the elements.
*
* @return {Object} Selectors for checkout type specified.
*/
getSelectors: function ( isBlocksCheckout = false ) {
if ( isBlocksCheckout ) {
return {
...this.default,
...this.updateSelectors( this.blocksCheckout ),
};
getSelectors: function ( elementsLocation ) {
let appearanceSelector = this.blocksCheckout;

switch ( elementsLocation ) {
case 'blocks_checkout':
appearanceSelector = this.blocksCheckout;
break;
case 'classic_checkout':
appearanceSelector = this.classicCheckout;
break;
case 'bnpl_product_page':
appearanceSelector = this.bnplProductPage;
break;
}

return {
...this.default,
...this.updateSelectors( this.classicCheckout ),
...this.updateSelectors( appearanceSelector ),
};
},
};
Expand Down Expand Up @@ -180,10 +202,10 @@ const hiddenElementsForUPE = {
/**
* Initialize hidden fields to generate UPE styles.
*
* @param {boolean} isBlocksCheckout True if Blocks Checkout. Default false.
* @param {boolean} elementsLocation The location of the elements.
*/
init: function ( isBlocksCheckout = false ) {
const selectors = appearanceSelectors.getSelectors( isBlocksCheckout ),
init: function ( elementsLocation ) {
const selectors = appearanceSelectors.getSelectors( elementsLocation ),
appendTarget = document.querySelector( selectors.appendTarget ),
elementToClone = document.querySelector(
selectors.upeThemeInputSelector
Expand Down Expand Up @@ -342,11 +364,11 @@ export const getFontRulesFromPage = () => {
return fontRules;
};

export const getAppearance = ( isBlocksCheckout = false ) => {
const selectors = appearanceSelectors.getSelectors( isBlocksCheckout );
export const getAppearance = ( elementsLocation ) => {
const selectors = appearanceSelectors.getSelectors( elementsLocation );

// Add hidden fields to DOM for generating styles.
hiddenElementsForUPE.init( isBlocksCheckout );
hiddenElementsForUPE.init( elementsLocation );

const inputRules = getFieldStyles( selectors.hiddenInput, '.Input' );
const inputInvalidRules = getFieldStyles(
Expand Down
2 changes: 1 addition & 1 deletion client/checkout/upe-styles/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe( 'Getting styles for automated theming', () => {
return mockCSStyleDeclaration;
} );

const appearance = upeStyles.getAppearance();
const appearance = upeStyles.getAppearance( 'shortcode_checkout' );
expect( appearance ).toEqual( {
variables: {
colorBackground: '#ffffff',
Expand Down
33 changes: 30 additions & 3 deletions client/product-details/bnpl-site-messaging/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,31 @@
*/
import './style.scss';
import WCPayAPI from 'wcpay/checkout/api';
import { getAppearance, getFontRulesFromPage } from 'wcpay/checkout/upe-styles';
import { getUPEConfig } from 'wcpay/utils/checkout';
import apiRequest from 'wcpay/checkout/utils/request';

export const initializeBnplSiteMessaging = () => {
/**
* Initializes the appearance of the payment element by retrieving the UPE configuration
* from the API and saving the appearance if it doesn't exist. If the appearance already exists,
* it is simply returned.
*
* @param {Object} api The API object used to save the UPE configuration.
* @return {Promise<Object>} The appearance object for the UPE.
*/
async function initializeAppearance( api ) {
const appearance = getUPEConfig( 'upeBnplProductPageAppearance' );
if ( appearance ) {
return Promise.resolve( appearance );
}

return await api.saveUPEAppearance(
getAppearance( 'bnpl_product_page' ),
'bnpl_product_page'
);
}

export const initializeBnplSiteMessaging = async () => {
const {
productVariations,
country,
Expand All @@ -21,17 +44,21 @@ export const initializeBnplSiteMessaging = () => {
accountId: accountId,
locale: locale,
},
null
apiRequest
);
const options = {
amount: parseInt( productVariations.base_product.amount, 10 ) || 0,
currency: productVariations.base_product.currency || 'USD',
paymentMethodTypes: paymentMethods || [],
countryCode: country, // Customer's country or base country of the store.
};
const elementsOptions = {
appearance: await initializeAppearance( api ),
fonts: getFontRulesFromPage(),
};
const paymentMessageElement = api
.getStripe()
.elements()
.elements( elementsOptions )
.create( 'paymentMethodMessaging', options );
paymentMessageElement.mount( '#payment-method-message' );

Expand Down
4 changes: 2 additions & 2 deletions client/product-details/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { initializeBnplSiteMessaging } from './bnpl-site-messaging';

jQuery( function ( $ ) {
jQuery( async function ( $ ) {
/**
* Check for the existence of the `wcpayStripeSiteMessaging` variable on the window object.
* This variable holds the configuration for Stripe site messaging and contains the following keys:
Expand Down Expand Up @@ -34,7 +34,7 @@ jQuery( function ( $ ) {
const VARIATION_ID_SELECTOR = 'input[name="variation_id"]';

const quantityInput = $( QUANTITY_INPUT_SELECTOR );
const bnplPaymentMessageElement = initializeBnplSiteMessaging();
const bnplPaymentMessageElement = await initializeBnplSiteMessaging();
const hasVariations = Object.keys( productVariations ).length > 1;

/**
Expand Down
56 changes: 44 additions & 12 deletions includes/class-wc-payment-gateway-wcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,13 @@ class WC_Payment_Gateway_WCPay extends WC_Payment_Gateway_CC {

const USER_FORMATTED_TOKENS_LIMIT = 100;

const PROCESS_REDIRECT_ORDER_MISMATCH_ERROR_CODE = 'upe_process_redirect_order_id_mismatched';
const UPE_APPEARANCE_TRANSIENT = 'wcpay_upe_appearance';
const WC_BLOCKS_UPE_APPEARANCE_TRANSIENT = 'wcpay_wc_blocks_upe_appearance';
const UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_appearance_theme';
const WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_wc_blocks_upe_appearance_theme';
const PROCESS_REDIRECT_ORDER_MISMATCH_ERROR_CODE = 'upe_process_redirect_order_id_mismatched';
const UPE_APPEARANCE_TRANSIENT = 'wcpay_upe_appearance';
const WC_BLOCKS_UPE_APPEARANCE_TRANSIENT = 'wcpay_wc_blocks_upe_appearance';
const UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_product_page_appearance';
const UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_appearance_theme';
const WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_wc_blocks_upe_appearance_theme';
const UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_product_page_appearance_theme';

/**
* Client for making requests to the WooCommerce Payments API
Expand Down Expand Up @@ -3882,19 +3884,47 @@ public function save_upe_appearance_ajax() {
);
}

$is_blocks_checkout = isset( $_POST['is_blocks_checkout'] ) ? rest_sanitize_boolean( wc_clean( wp_unslash( $_POST['is_blocks_checkout'] ) ) ) : false;
$appearance = isset( $_POST['appearance'] ) ? json_decode( wc_clean( wp_unslash( $_POST['appearance'] ) ) ) : null;
$elements_location = isset( $_POST['elements_location'] ) ? wc_clean( wp_unslash( $_POST['elements_location'] ) ) : null;
$appearance = isset( $_POST['appearance'] ) ? json_decode( wc_clean( wp_unslash( $_POST['appearance'] ) ) ) : null;

$valid_locations = [ 'blocks_checkout', 'shortcode_checkout', 'bnpl_product_page' ];
if ( ! $elements_location || ! in_array( $elements_location, $valid_locations, true ) ) {
throw new Exception(
__( 'Unable to update UPE appearance values at this time.', 'woocommerce-payments' )
);
}

if ( in_array( $elements_location, [ 'blocks_checkout', 'shortcode_checkout' ], true ) ) {
$is_blocks_checkout = 'blocks_checkout' === $elements_location;
/**
* This filter is only called on "save" of the appearance, to avoid calling it on every page load.
* If you apply changes through this filter, you'll need to clear the transient data to see them at checkout.
*
* @deprecated 7.4.0 Use {@see 'wcpay_elements_appearance'} instead.
* @since 7.3.0
*/
$appearance = apply_filters_deprecated( 'wcpay_upe_appearance', [ $appearance, $is_blocks_checkout ], '7.4.0', 'wcpay_elements_appearance' );
}

/**
* This filter is only called on "save" of the appearance, to avoid calling it on every page load.
* If you apply changes through this filter, you'll need to clear the transient data to see them at checkout.
* $elements_location can be 'blocks_checkout', 'shortcode_checkout', or 'bnpl_product_page'.
*
* @since 7.3.0
* @since 7.4.0
*/
$appearance = apply_filters( 'wcpay_upe_appearance', $appearance, $is_blocks_checkout );

$appearance_transient = $is_blocks_checkout ? self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT : self::UPE_APPEARANCE_TRANSIENT;
$appearance_theme_transient = $is_blocks_checkout ? self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT : self::UPE_APPEARANCE_THEME_TRANSIENT;
$appearance = apply_filters( 'wcpay_elements_appearance', $appearance, $elements_location );

$appearance_transient = [
'shortcode_checkout' => self::UPE_APPEARANCE_TRANSIENT,
'blocks_checkout' => self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT,
'bnpl_product_page' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT,
][ $elements_location ];
$appearance_theme_transient = [
'shortcode_checkout' => self::UPE_APPEARANCE_THEME_TRANSIENT,
'blocks_checkout' => self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT,
'bnpl_product_page' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT,
][ $elements_location ];

if ( null !== $appearance ) {
set_transient( $appearance_transient, $appearance, DAY_IN_SECONDS );
Expand All @@ -3921,8 +3951,10 @@ public function save_upe_appearance_ajax() {
public function clear_upe_appearance_transient() {
delete_transient( self::UPE_APPEARANCE_TRANSIENT );
delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT );
delete_transient( self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT );
delete_transient( self::UPE_APPEARANCE_THEME_TRANSIENT );
delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT );
delete_transient( self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT );
}

/**
Expand Down
28 changes: 15 additions & 13 deletions includes/class-wc-payments-checkout.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,19 +212,21 @@ public function get_payment_fields_js_config() {
*/
$payment_fields = apply_filters( 'wcpay_payment_fields_js_config', $js_config );

$payment_fields['accountDescriptor'] = $this->gateway->get_account_statement_descriptor();
$payment_fields['addPaymentReturnURL'] = wc_get_account_endpoint_url( 'payment-methods' );
$payment_fields['gatewayId'] = WC_Payment_Gateway_WCPay::GATEWAY_ID;
$payment_fields['isCheckout'] = is_checkout();
$payment_fields['paymentMethodsConfig'] = $this->get_enabled_payment_method_config();
$payment_fields['testMode'] = WC_Payments::mode()->is_test();
$payment_fields['upeAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_APPEARANCE_TRANSIENT );
$payment_fields['wcBlocksUPEAppearance'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT );
$payment_fields['wcBlocksUPEAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT );
$payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart();
$payment_fields['currency'] = get_woocommerce_currency();
$cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 );
$payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() );
$payment_fields['accountDescriptor'] = $this->gateway->get_account_statement_descriptor();
$payment_fields['addPaymentReturnURL'] = wc_get_account_endpoint_url( 'payment-methods' );
$payment_fields['gatewayId'] = WC_Payment_Gateway_WCPay::GATEWAY_ID;
$payment_fields['isCheckout'] = is_checkout();
$payment_fields['paymentMethodsConfig'] = $this->get_enabled_payment_method_config();
$payment_fields['testMode'] = WC_Payments::mode()->is_test();
$payment_fields['upeAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_APPEARANCE_TRANSIENT );
$payment_fields['upeBnplProductPageAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT );
$payment_fields['upeBnplProductPageAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT );
$payment_fields['wcBlocksUPEAppearance'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT );
$payment_fields['wcBlocksUPEAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT );
$payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart();
$payment_fields['currency'] = get_woocommerce_currency();
$cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 );
$payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() );

$enabled_billing_fields = [];
foreach ( WC()->checkout()->get_checkout_fields( 'billing' ) as $billing_field => $billing_field_options ) {
Expand Down
10 changes: 10 additions & 0 deletions includes/class-wc-payments-payment-method-messaging-element.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ public function init(): string {
]
);

// Ensure wcpayConfig is available in the page.
$wcpay_config = rawurlencode( wp_json_encode( WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ) );
wp_add_inline_script(
'WCPAY_PRODUCT_DETAILS',
"
var wcpayConfig = wcpayConfig || JSON.parse( decodeURIComponent( '" . esc_js( $wcpay_config ) . "' ) );
",
'before'
);

return '<div id="payment-method-message"></div>';
}
}

0 comments on commit 294756f

Please sign in to comment.