Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customizing BNPL messaging with Appearance API #8421

Merged
merged 5 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -211,19 +211,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>';
}
}
Loading