-
Notifications
You must be signed in to change notification settings - Fork 225
Add adaptive pricing in classic checkout #5006
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
Changes from 20 commits
8aaf2e8
5ca030a
db550e8
18d8230
10bb628
5077aee
2f210d9
c088e36
afeda2c
4f39d36
5ab6773
128c7d1
9c8dfeb
b6eaf7f
a8c057f
1a8f2f0
e896712
3d28eae
9260764
c213532
ce58091
d906064
2ce1a79
7fe6067
98c1fea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,8 @@ import { handleDisplayOfSavingCheckbox } from 'wcstripe/optimized-checkout/handl | |
|
|
||
| const gatewayUPEComponents = {}; | ||
| const paymentMethodsConfig = getStripeServerData()?.paymentMethodsConfig; | ||
| const isAdaptivePricingSupported = | ||
| getStripeServerData()?.isAdaptivePricingSupported; | ||
|
|
||
| /** | ||
| * Initialize the UPE components for each payment method type. | ||
|
|
@@ -81,24 +83,21 @@ export function validateElements( elements ) { | |
|
|
||
| /** | ||
| * Updates the payment element's default values. | ||
| * | ||
| * @param {boolean} forCheckoutSession Whether the default values are for a Checkout Session. | ||
| */ | ||
| function updatePaymentElementDefaultValues() { | ||
| function updatePaymentElementDefaultValues( forCheckoutSession = false ) { | ||
| if ( ! gatewayUPEComponents?.card?.upeElement ) { | ||
| return; | ||
| } | ||
|
|
||
| const paymentElement = gatewayUPEComponents.card.upeElement; | ||
| paymentElement.update( getDefaultValues() ); | ||
| paymentElement.update( getDefaultValues( forCheckoutSession ) ); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Creates a Stripe payment element with the specified payment method type and options. | ||
| * | ||
| * If the payment method doesn't support deferred intent, the intent must be created first. | ||
| * Then, the payment element is created with the intent's client secret. | ||
| * | ||
| * Finally, the payment element is mounted and attached to the gatewayUPEComponents object. | ||
| * | ||
| * @param {Object} api The API object used to create the Stripe payment element. | ||
| * @param {string} paymentMethodType The type of Stripe payment method to create. | ||
| * @return {Object} A promise that resolves with the created Stripe payment element. | ||
|
|
@@ -190,19 +189,65 @@ async function createStripePaymentElement( api, paymentMethodType ) { | |
| } | ||
| } | ||
|
|
||
| const elements = api.getStripe().elements( options ); | ||
| let elements; | ||
| let shouldLoadStripeElements = ! isAdaptivePricingSupported; | ||
| // If Adaptive Pricing is enabled, use the Checkout Session API to load the elements. | ||
| if ( isAdaptivePricingSupported ) { | ||
| try { | ||
| const response = await api.checkoutSessionsCreateSession(); | ||
| const clientSecret = response.data?.client_secret; | ||
|
|
||
Mayisha marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if ( ! clientSecret ) { | ||
| throw new Error( | ||
| __( | ||
| 'Failed to load payment method due to missing client secret.', | ||
| 'woocommerce-gateway-stripe' | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| const attachDefaultValuesUpdateEvent = ( element ) => { | ||
| elements = await api.getStripe().initCheckout( { | ||
| clientSecret, | ||
| elementsOptions: { | ||
| appearance: options.appearance, | ||
| fonts: options.fonts, | ||
| }, | ||
| adaptivePricing: { | ||
| allowed: true, | ||
| }, | ||
| ...getDefaultValues( true ), | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } ); | ||
|
|
||
| if ( elements.error ) { | ||
| throw elements.error; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| shouldLoadStripeElements = false; | ||
| } catch ( error ) { | ||
| // eslint-disable-next-line no-console | ||
| console.error( error ); | ||
| shouldLoadStripeElements = true; | ||
| } | ||
| } | ||
Mayisha marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // If Adaptive Pricing is not enabled, or if there was an error loading the AP elements, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice idea! |
||
| // load the Stripe elements as fallback. | ||
| if ( shouldLoadStripeElements ) { | ||
| elements = api.getStripe().elements( options ); | ||
| } | ||
Mayisha marked this conversation as resolved.
Show resolved
Hide resolved
Mayisha marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const attachDefaultValuesUpdateEvent = ( | ||
| element, | ||
| forCheckoutSession = false | ||
| ) => { | ||
| if ( document.getElementById( element ) ) { | ||
| document.getElementById( element ).onblur = function () { | ||
| updatePaymentElementDefaultValues(); | ||
| updatePaymentElementDefaultValues( forCheckoutSession ); | ||
| }; | ||
| } | ||
| }; | ||
|
|
||
| let paymentElementOptions = { | ||
| ...getUpeSettings(), | ||
| ...getDefaultValues(), | ||
| wallets: { | ||
| applePay: 'never', | ||
| googlePay: 'never', | ||
|
|
@@ -224,10 +269,24 @@ async function createStripePaymentElement( api, paymentMethodType ) { | |
| }; | ||
| } | ||
|
|
||
| const createdStripePaymentElement = elements.create( | ||
| 'payment', | ||
| paymentElementOptions | ||
| ); | ||
| let createdStripePaymentElement = null; | ||
|
|
||
| if ( shouldLoadStripeElements ) { | ||
| paymentElementOptions = { | ||
| ...paymentElementOptions, | ||
| ...getDefaultValues(), | ||
| ...getUpeSettings(), | ||
| }; | ||
| createdStripePaymentElement = elements.create( | ||
| 'payment', | ||
| paymentElementOptions | ||
| ); | ||
| } else { | ||
| createdStripePaymentElement = elements.createPaymentElement( | ||
| paymentElementOptions | ||
| ); | ||
| mountCurrencySelectorElement( elements ); | ||
| } | ||
Mayisha marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| gatewayUPEComponents[ paymentMethodType ].elements = elements; | ||
| gatewayUPEComponents[ paymentMethodType ].upeElement = | ||
|
|
@@ -240,13 +299,35 @@ async function createStripePaymentElement( api, paymentMethodType ) { | |
| isLinkEnabled() && | ||
| paymentMethodType === PAYMENT_METHOD_CARD | ||
| ) { | ||
| attachDefaultValuesUpdateEvent( 'billing_email' ); | ||
| attachDefaultValuesUpdateEvent( 'billing_phone' ); | ||
| attachDefaultValuesUpdateEvent( | ||
| 'billing_email', | ||
| ! shouldLoadStripeElements | ||
| ); | ||
| attachDefaultValuesUpdateEvent( | ||
| 'billing_phone', | ||
| ! shouldLoadStripeElements | ||
| ); | ||
| } | ||
|
|
||
| return createdStripePaymentElement; | ||
| } | ||
|
|
||
| /** | ||
| * Mounts the currency selector element to the DOM element. | ||
| * | ||
| * @param {Object} elements The Stripe elements object. | ||
| */ | ||
| function mountCurrencySelectorElement( elements ) { | ||
| const currencySelectorContainer = document.getElementById( | ||
| 'wc-stripe-currency-selector' | ||
| ); | ||
| if ( ! currencySelectorContainer ) { | ||
| return; | ||
| } | ||
| const currencySelector = elements.createCurrencySelectorElement(); | ||
| currencySelector.mount( currencySelectorContainer ); | ||
| } | ||
|
|
||
| /** | ||
| * Submits the provided jQuery form and removes the 'processing' class from it. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -459,9 +459,10 @@ export const getUpeSettings = () => { | |
| * On order pay and change payment method pages, also preloads all billing details | ||
| * from the customer billing data passed from the server. | ||
| * | ||
| * @param {boolean} forCheckoutSession Whether the default values are for a Checkout Session. | ||
| * @return {Object} The defaultValues object for the Payment Element. | ||
| */ | ||
| export const getDefaultValues = () => { | ||
| export const getDefaultValues = ( forCheckoutSession = false ) => { | ||
| const stripeServerData = getStripeServerData(); | ||
| const isOrderPay = stripeServerData?.isOrderPay; | ||
| const isChangingPayment = stripeServerData?.isChangingPayment; | ||
|
|
@@ -500,6 +501,20 @@ export const getDefaultValues = () => { | |
| address.postal_code = postalCode; | ||
| } | ||
|
|
||
| if ( forCheckoutSession ) { | ||
| return { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Personally, I prefer to keep the return statements clean and extract this object into a variable, but that's mostly a stylistic preference and not a blocker. |
||
| defaultValues: { | ||
| billingAddress: { | ||
| name: billingData.name?.trim() || undefined, | ||
| ...( Object.keys( address ).length > 0 | ||
| ? { address } | ||
| : {} ), | ||
| }, | ||
| phoneNumber: billingData.phone?.trim() || undefined, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| defaultValues: { | ||
| billingDetails: { | ||
|
|
@@ -525,6 +540,10 @@ export const getDefaultValues = () => { | |
| document.getElementById( 'billing_phone' )?.value || | ||
| document.getElementById( 'shipping_phone' )?.value; | ||
|
|
||
| if ( forCheckoutSession ) { | ||
| return {}; | ||
| } | ||
|
|
||
| return { | ||
| defaultValues: { | ||
| billingDetails: { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ class WC_Stripe_API { | |
| * Stripe API Endpoint | ||
| */ | ||
| const ENDPOINT = 'https://api.stripe.com/v1/'; | ||
| const STRIPE_API_VERSION = '2024-06-20'; | ||
| const STRIPE_API_VERSION = '2025-09-30.clover'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a significant update that might need its own PR, given that it will affect all payment flows (not just checkout sessions), but e2es seem to pass, so it looks like all is well.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are absolutely correct in calling this out. We have a separate PR for updating the Stripe API and JS versions #4977. I have included the changes here as well for testing. I will merge the other PR first to ensure there are no regressions. |
||
|
|
||
| /** | ||
| * The invalid API key error count cache key. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -282,6 +282,9 @@ public function __construct() { | |
| add_action( 'customize_save_after', [ $this, 'clear_appearance_transients' ] ); | ||
| add_action( 'save_post', [ $this, 'clear_appearance_transients_block_theme' ], 10, 2 ); | ||
|
|
||
| // Attach the currency selector div to the classic checkout page. | ||
| add_action( 'woocommerce_review_order_before_payment', [ $this, 'attach_currency_selector_element' ] ); | ||
|
|
||
Mayisha marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Hide action buttons for pending orders if they take a while to be confirmed. | ||
| add_filter( 'woocommerce_my_account_my_orders_actions', [ $this, 'filter_my_account_my_orders_actions' ], 10, 2 ); | ||
|
|
||
|
|
@@ -415,9 +418,9 @@ public function payment_scripts() { | |
|
|
||
| wp_register_script( | ||
| 'stripe', | ||
| 'https://js.stripe.com/v3/', | ||
| 'https://js.stripe.com/clover/stripe.js', | ||
| [], | ||
| '3.0', | ||
| null, | ||
| true | ||
| ); | ||
|
|
||
|
|
@@ -529,6 +532,9 @@ public function javascript_params() { | |
| $stripe_params['excludedPaymentMethodTypes'] = $this->get_excluded_payment_method_types(); | ||
| } | ||
|
|
||
| // Adaptive Pricing feature flag and setting. | ||
| $stripe_params['isAdaptivePricingSupported'] = WC_Stripe_Feature_Flags::is_checkout_sessions_available() && 'yes' === $this->get_option( 'adaptive_pricing', 'no' ); | ||
|
|
||
Mayisha marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Checking for other BNPL extensions. | ||
| $stripe_params['hasAffirmGatewayPlugin'] = WC_Stripe_Helper::has_gateway_plugin_active( WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_AFFIRM ); | ||
| $stripe_params['hasKlarnaGatewayPlugin'] = WC_Stripe_Helper::has_gateway_plugin_active( WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_KLARNA ); | ||
|
|
@@ -928,6 +934,26 @@ public function payment_fields() { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Attaches the currency selector div to the classic checkout page. | ||
| * This is used to render the currency selector element in the checkout page. | ||
| * | ||
| * @return void | ||
| */ | ||
| public function attach_currency_selector_element() { | ||
| // Bail if checkout sessionsfeature flag is not enabled. | ||
| if ( ! WC_Stripe_Feature_Flags::is_checkout_sessions_available() ) { | ||
| return; | ||
| } | ||
|
|
||
| // Bail if not on the checkout page. | ||
| if ( ! is_checkout() ) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also bail out if so that we don't attach this element when not supported.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call. I will add the check. 👍 |
||
| return; | ||
| } | ||
|
|
||
| echo '<div id="wc-stripe-currency-selector" class="wc-stripe-currency-selector" style="margin: 12px 0;"></div>'; | ||
| } | ||
|
|
||
| /** | ||
| * Process the payment for a given order. | ||
| * | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to double-check, is this removal intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intentional. As we are using
const STRIPE_API_VERSION = '2025-09-30.clover';now, we do not need to pass theapiVersionhere anymore. Also this param is unsupported when we are using thecloverversion.