diff --git a/.github/actions/e2e/run-log-tests/action.yml b/.github/actions/e2e/run-log-tests/action.yml index 71e8d883865..142e085d0fe 100644 --- a/.github/actions/e2e/run-log-tests/action.yml +++ b/.github/actions/e2e/run-log-tests/action.yml @@ -39,6 +39,7 @@ runs: with: name: wp(${{ env.E2E_WP_VERSION }})-wc(${{ env.E2E_WC_VERSION }})-${{ env.E2E_GROUP }}-${{ env.E2E_BRANCH }} path: | + screenshots tests/e2e/screenshots tests/e2e/docker/wordpress/wp-content/debug.log ${{ env.E2E_RESULT_FILEPATH }} diff --git a/assets/images/payment-methods/afterpay.svg b/assets/images/payment-methods/afterpay.svg index a769af42cdd..3795553025f 100644 --- a/assets/images/payment-methods/afterpay.svg +++ b/assets/images/payment-methods/afterpay.svg @@ -1,9 +1,5 @@ - - - - @@ -12,5 +8,4 @@ - diff --git a/assets/images/payment-methods/clearpay.svg b/assets/images/payment-methods/clearpay.svg new file mode 100644 index 00000000000..bce4db33418 --- /dev/null +++ b/assets/images/payment-methods/clearpay.svg @@ -0,0 +1,4 @@ + + + + diff --git a/changelog.txt b/changelog.txt index 598679d0f3c..79d2e23d29e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,47 @@ *** WooPayments Changelog *** += 7.2.0 - 2024-02-14 = +* Add - Add compatibility data to onboarding init payload. +* Add - Add WooPay direct checkout flow behind a feature flag. +* Add - Apply localization to CSV exports for transactions, deposits, and disputes sent via email. +* Add - Displaying Clearpay instead of Afterpay for UK based stores +* Add - Enhance WooPay session validation +* Add - Filtering APMs by billing country +* Add - Show a notice to the merchant when the available balance is below the minimum deposit amount. +* Add - Show charge id on payments details page, so merchants can grab it to fill out the dispute evidence form when needed. +* Add - Showing "started" event in transaction timeline +* Add - Support Stripe Link payments with 3DS cards. +* Fix - Adjust WordPress locale code to match the languages supported by the server. +* Fix - Displaying the correct method name in Order Edit page for HPOS +* Fix - Don't instantiate `Fraud_Prevention_Service` in checkout if processing an authorized WooPay request. +* Fix - fix: help text alignment with Gutenberg plugin enabled +* Fix - fix: pay-for-order compatibility with other gateways +* Fix - Fixed a bug where the 'deposits paused while balance is negative' notice was erroneously shown after an instant deposit. +* Fix - Fixes Pay for Order checkout using non-card payment methods. +* Fix - Fix losing cart contents during the login at checkout. +* Fix - Merge duplicated Payment Request and WooPay button functionality . +* Fix - Prevent coupon usage increase in a WooPay preflight check. +* Fix - Prevent WooPay webhook creation when account is suspended +* Update - Add source to the onboarding flow page and track it +* Update - Refactor the WooPay checkout flow UX +* Update - Some minor update to tracking parameters to pass additional data like Woo store ID. +* Update - Stop relying on Woo core for loading plugin translations. +* Dev - Added ENUM class for currency codes +* Dev - Bump WC tested up to version to 8.5.2. +* Dev - chore: removed deprecated functions since 5.0.0 +* Dev - chore: remove unused checkout API methods +* Dev - chore: remove unused gateway class methods +* Dev - chore: remove unused isOrderPage return value from confirmIntent +* Dev - chore: update colors on documentation pages +* Dev - Comment: Bump qit-cli dependency to version 0.4.0. +* Dev - E2E test - Merchant facing multi-currency on-boarding screen. +* Dev - Fix for E2E shopper tests around 3DS and UPE settings +* Dev - Refactoring the tracking logic +* Dev - Refactor to how tracking events are defined for better readability. +* Dev - Remove unnecessary tracks events for dispute accept success/error. +* Dev - Update REST API documentation for deposits endpoints with changes to estimated and instant deposits +* Dev - Update Tracks conditions + = 7.1.0 - 2024-01-25 = * Add - Add active plugins array to compatibility data. * Add - Add post_types and their counts as an array to compatibility data. diff --git a/client/card-readers/settings/file-upload.tsx b/client/card-readers/settings/file-upload.tsx index 4d47f63c010..63fcf597e96 100644 --- a/client/card-readers/settings/file-upload.tsx +++ b/client/card-readers/settings/file-upload.tsx @@ -3,7 +3,7 @@ * External dependencies */ import React from 'react'; -import wcpayTracks from 'tracks'; +import { recordEvent } from 'tracks'; import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; @@ -71,12 +71,9 @@ const BrandingFileUpload: React.FunctionComponent< CardReaderFileUploadProps > = setLoading( true ); - wcpayTracks.recordEvent( - 'wcpay_merchant_settings_file_upload_started', - { - type: key, - } - ); + recordEvent( 'wcpay_merchant_settings_file_upload_started', { + type: key, + } ); const body = new FormData(); body.append( 'file', file ); @@ -99,14 +96,11 @@ const BrandingFileUpload: React.FunctionComponent< CardReaderFileUploadProps > = setLoading( false ); setUploadError( false ); - wcpayTracks.recordEvent( - 'wcpay_merchant_settings_file_upload_success', - { - type: key, - } - ); + recordEvent( 'wcpay_merchant_settings_file_upload_success', { + type: key, + } ); } catch ( { err } ) { - wcpayTracks.recordEvent( 'wcpay_merchant_settings_upload_failed', { + recordEvent( 'wcpay_merchant_settings_file_upload_success', { message: ( err as Error ).message, } ); diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index 1c42111e6e0..3ccaa9026a8 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -207,7 +207,7 @@ export default class WCPayAPI { * * @param {string} redirectUrl The redirect URL, returned from the server. * @param {string} paymentMethodToSave The ID of a Payment Method if it should be saved (optional). - * @return {mixed} A redirect URL on success, or `true` if no confirmation is needed. + * @return {Promise|boolean} A redirect URL on success, or `true` if no confirmation is needed. */ confirmIntent( redirectUrl, paymentMethodToSave ) { const partials = redirectUrl.match( @@ -252,9 +252,9 @@ export default class WCPayAPI { // If this is a setup intent we're not processing a woopay payment so we can // use the regular getStripe function. if ( isSetupIntent ) { - return this.getStripe().confirmCardSetup( - decryptClientSecret( clientSecret ) - ); + return this.getStripe().handleNextAction( { + clientSecret: decryptClientSecret( clientSecret ), + } ); } // For woopay we need the capability to switch up the account ID specifically for @@ -274,66 +274,61 @@ export default class WCPayAPI { // When not dealing with a setup intent or woopay we need to force an account // specific request in Stripe. - return this.getStripe( true ).confirmCardPayment( - decryptClientSecret( clientSecret ) - ); + return this.getStripe( true ).handleNextAction( { + clientSecret: decryptClientSecret( clientSecret ), + } ); }; - const confirmAction = confirmPaymentOrSetup(); - - const request = confirmAction - // ToDo: Switch to an async function once it works with webpack. - .then( ( result ) => { - const intentId = - ( result.paymentIntent && result.paymentIntent.id ) || - ( result.setupIntent && result.setupIntent.id ) || - ( result.error && - result.error.payment_intent && - result.error.payment_intent.id ) || - ( result.error.setup_intent && - result.error.setup_intent.id ); - - // In case this is being called via payment request button from a product page, - // the getConfig function won't work, so fallback to getPaymentRequestData. - const ajaxUrl = - getPaymentRequestData( 'ajax_url' ) ?? - getConfig( 'ajaxUrl' ); - - const ajaxCall = this.request( ajaxUrl, { - action: 'update_order_status', - order_id: orderId, - // Update the current order status nonce with the new one to ensure that the update - // order status call works when a guest user creates an account during checkout. - _ajax_nonce: nonce, - intent_id: intentId, - payment_method_id: paymentMethodToSave || null, - } ); - - return [ ajaxCall, result.error ]; - } ) - .then( ( [ verificationCall, originalError ] ) => { - if ( originalError ) { - throw originalError; - } - - return verificationCall.then( ( response ) => { - const result = - typeof response === 'string' - ? JSON.parse( response ) - : response; + return ( + confirmPaymentOrSetup() + // ToDo: Switch to an async function once it works with webpack. + .then( ( result ) => { + const intentId = + ( result.paymentIntent && result.paymentIntent.id ) || + ( result.setupIntent && result.setupIntent.id ) || + ( result.error && + result.error.payment_intent && + result.error.payment_intent.id ) || + ( result.error.setup_intent && + result.error.setup_intent.id ); + + // In case this is being called via payment request button from a product page, + // the getConfig function won't work, so fallback to getPaymentRequestData. + const ajaxUrl = + getPaymentRequestData( 'ajax_url' ) ?? + getConfig( 'ajaxUrl' ); + + const ajaxCall = this.request( ajaxUrl, { + action: 'update_order_status', + order_id: orderId, + // Update the current order status nonce with the new one to ensure that the update + // order status call works when a guest user creates an account during checkout. + _ajax_nonce: nonce, + intent_id: intentId, + payment_method_id: paymentMethodToSave || null, + } ); - if ( result.error ) { - throw result.error; + return [ ajaxCall, result.error ]; + } ) + .then( ( [ verificationCall, originalError ] ) => { + if ( originalError ) { + throw originalError; } - return result.return_url; - } ); - } ); + return verificationCall.then( ( response ) => { + const result = + typeof response === 'string' + ? JSON.parse( response ) + : response; - return { - request, - isOrderPage, - }; + if ( result.error ) { + throw result.error; + } + + return result.return_url; + } ); + } ) + ); } /** @@ -565,49 +560,4 @@ export default class WCPayAPI { ...paymentData, } ); } - - /** - * Log Payment Errors via Ajax. - * - * @param {string} chargeId Stripe Charge ID - * @return {boolean} Returns true irrespective of result. - */ - logPaymentError( chargeId ) { - return this.request( - buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'log_payment_error' ), - { - charge_id: chargeId, - _ajax_nonce: getConfig( 'logPaymentErrorNonce' ), - } - ).then( () => { - // There is not any action to take or harm caused by a failed update, so just returning true. - return true; - } ); - } - - /** - * Redirect to the order-received page for duplicate payments. - * - * @param {Object} response Response data to check if doing the redirect. - * @return {boolean} Returns true if doing the redirection. - */ - handleDuplicatePayments( { - wcpay_upe_paid_for_previous_order: previouslyPaid, - wcpay_upe_previous_successful_intent: previousSuccessfulIntent, - redirect, - } ) { - if ( redirect ) { - // Another order has the same cart content and was paid. - if ( previouslyPaid ) { - return ( window.location = redirect ); - } - - // Another intent has the equivalent successful status for the order. - if ( previousSuccessfulIntent ) { - return ( window.location = redirect ); - } - } - - return false; - } } diff --git a/client/checkout/blocks/confirm-card-payment.js b/client/checkout/blocks/confirm-card-payment.js index f5235427fc6..92089853376 100644 --- a/client/checkout/blocks/confirm-card-payment.js +++ b/client/checkout/blocks/confirm-card-payment.js @@ -16,26 +16,22 @@ export default async function confirmCardPayment( const { redirect, payment_method: paymentMethod } = paymentDetails; try { - const confirmation = api.confirmIntent( + const confirmationRequest = api.confirmIntent( redirect, shouldSavePayment ? paymentMethod : null ); // `true` means there is no intent to confirm. - if ( confirmation === true ) { + if ( confirmationRequest === true ) { return { type: 'success', redirectUrl: redirect, }; } - // `confirmIntent` also returns `isOrderPage`, but that's not supported in blocks yet. - const { request } = confirmation; - - const finalRedirect = await request; return { type: 'success', - redirectUrl: finalRedirect, + redirectUrl: await confirmationRequest, }; } catch ( error ) { return { diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index 0ce235a0086..f501c169f5c 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -34,7 +34,7 @@ import { } from '../constants.js'; import { getDeferredIntentCreationUPEFields } from './payment-elements'; import { handleWooPayEmailInput } from '../woopay/email-input-iframe'; -import wcpayTracks from 'tracks'; +import { recordUserEvent } from 'tracks'; import wooPayExpressCheckoutPaymentMethod from '../woopay/express-button/woopay-express-checkout-payment-method'; import { isPreviewing } from '../preview'; @@ -127,7 +127,7 @@ const addCheckoutTracking = () => { return; } - wcpayTracks.recordUserEvent( wcpayTracks.events.PLACE_ORDER_CLICK ); + recordUserEvent( 'checkout_place_order_button_click' ); } ); } }; diff --git a/client/checkout/classic/3ds-flow-handling.js b/client/checkout/classic/3ds-flow-handling.js index 305db950ca4..72a28c815c8 100644 --- a/client/checkout/classic/3ds-flow-handling.js +++ b/client/checkout/classic/3ds-flow-handling.js @@ -25,20 +25,19 @@ export const showAuthenticationModalIfRequired = ( api ) => { const paymentMethodId = document.querySelector( '#wcpay-payment-method' ) ?.value; - const confirmation = api.confirmIntent( + const confirmationRequest = api.confirmIntent( window.location.href, shouldSavePaymentPaymentMethod() ? paymentMethodId : null ); // Boolean `true` means that there is nothing to confirm. - if ( confirmation === true ) { + if ( confirmationRequest === true ) { return Promise.resolve(); } - const { request } = confirmation; cleanupURL(); - return request + return confirmationRequest .then( ( redirectUrl ) => { window.location = redirectUrl; } ) diff --git a/client/checkout/classic/event-handlers.js b/client/checkout/classic/event-handlers.js index b6daa42875a..4bbacc9beeb 100644 --- a/client/checkout/classic/event-handlers.js +++ b/client/checkout/classic/event-handlers.js @@ -28,7 +28,7 @@ import WCPayAPI from 'wcpay/checkout/api'; import apiRequest from '../utils/request'; import { handleWooPayEmailInput } from 'wcpay/checkout/woopay/email-input-iframe'; import { isPreviewing } from 'wcpay/checkout/preview'; -import wcpayTracks from 'tracks'; +import { recordUserEvent } from 'tracks'; jQuery( function ( $ ) { enqueueFraudScripts( getUPEConfig( 'fraudServices' ) ); @@ -83,7 +83,7 @@ jQuery( function ( $ ) { return; } - wcpayTracks.recordUserEvent( wcpayTracks.events.PLACE_ORDER_CLICK ); + recordUserEvent( 'checkout_place_order_button_click' ); } ); window.addEventListener( 'hashchange', () => { @@ -130,6 +130,10 @@ jQuery( function ( $ ) { } ); $payForOrderForm.on( 'submit', function () { + if ( getSelectedUPEGatewayPaymentMethod() === null ) { + return; + } + return processPaymentIfNotUsingSavedMethod( $payForOrderForm ); } ); diff --git a/client/checkout/classic/test/3ds-flow-handling.test.js b/client/checkout/classic/test/3ds-flow-handling.test.js index 9ca751c896c..5833d7aa3e7 100644 --- a/client/checkout/classic/test/3ds-flow-handling.test.js +++ b/client/checkout/classic/test/3ds-flow-handling.test.js @@ -27,11 +27,7 @@ describe( 'showAuthenticationModalIfRequired', () => { const mockedRequest = Promise.resolve( 'https://example.com/checkout' ); const apiMock = { - confirmIntent: jest.fn( () => { - return { - request: mockedRequest, - }; - } ), + confirmIntent: jest.fn( () => mockedRequest ), }; showAuthenticationModalIfRequired( apiMock ); diff --git a/client/checkout/woopay/direct-checkout/index.js b/client/checkout/woopay/direct-checkout/index.js new file mode 100644 index 00000000000..6d6162a2898 --- /dev/null +++ b/client/checkout/woopay/direct-checkout/index.js @@ -0,0 +1,24 @@ +/** + * Internal dependencies + */ +import WooPayDirectCheckout from 'wcpay/checkout/woopay/direct-checkout/woopay-direct-checkout'; + +window.addEventListener( 'load', async () => { + if ( ! WooPayDirectCheckout.isWooPayEnabled() ) { + return; + } + + WooPayDirectCheckout.init(); + + const checkoutElements = WooPayDirectCheckout.getCheckoutRedirectElements(); + const isThirdPartyCookieEnabled = await WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled(); + if ( isThirdPartyCookieEnabled ) { + if ( await WooPayDirectCheckout.isUserLoggedIn() ) { + WooPayDirectCheckout.redirectToWooPaySession( checkoutElements ); + } + + return; + } + + WooPayDirectCheckout.redirectToWooPay( checkoutElements ); +} ); diff --git a/client/checkout/woopay/direct-checkout/woopay-connect-iframe.js b/client/checkout/woopay/direct-checkout/woopay-connect-iframe.js new file mode 100644 index 00000000000..5674a0fbddf --- /dev/null +++ b/client/checkout/woopay/direct-checkout/woopay-connect-iframe.js @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import React, { useEffect, useRef } from 'react'; + +/** + * Internal dependencies + */ +import { getConfig } from 'wcpay/utils/checkout'; + +export const WooPayConnectIframe = ( { listeners, actionCallback } ) => { + const iframeRef = useRef(); + + const getWoopayConnectUrl = () => { + const tracksUserId = JSON.stringify( + getConfig( 'tracksUserIdentity' ) + ); + + const urlParams = new URLSearchParams(); + urlParams.append( 'testMode', getConfig( 'testMode' ) ); + urlParams.append( 'source_url', window.location.href ); + urlParams.append( 'tracksUserIdentity', tracksUserId ); + + return getConfig( 'woopayHost' ) + '/connect/?' + urlParams.toString(); + }; + + useEffect( () => { + if ( ! iframeRef.current ) { + return; + } + + const iframe = iframeRef.current; + iframe.addEventListener( 'load', () => { + listeners.setIframePostMessage( ( value ) => { + iframe.contentWindow.postMessage( + value, + getConfig( 'woopayHost' ) + ); + } ); + } ); + + const onMessage = ( event ) => { + const isFromWoopayHost = getConfig( 'woopayHost' ).startsWith( + event.origin + ); + + if ( ! isFromWoopayHost ) { + return; + } + + if ( event.data.action in actionCallback ) { + const callback = actionCallback[ event.data.action ]; + listeners[ callback ]( event.data.value ); + } + }; + + window.addEventListener( 'message', onMessage ); + + return () => { + window.removeEventListener( 'message', onMessage ); + }; + }, [ actionCallback, listeners ] ); + + return ( +