+ { __( 'Buy now, pay later is here', 'woocommerce-payments' ) }
+
+
+ { __(
+ // eslint-disable-next-line max-len
+ 'Boost conversions and give your shoppers additional buying power, with buy now, pay later — now available in your WooPayments dashboard.*',
+ 'woocommerce-payments'
+ ) }
+
+
+ { __(
+ '*Subject to country availability',
+ 'woocommerce-payments'
+ ) }
+
+
+ );
+};
+
+const container = document.getElementById( 'wpwrap' );
+if ( container ) {
+ const dialogWrapper = document.createElement( 'div' );
+ container.appendChild( dialogWrapper );
+
+ ReactDOM.createRoot( dialogWrapper ).render( );
+}
diff --git a/client/bnpl-announcement/style.scss b/client/bnpl-announcement/style.scss
new file mode 100644
index 00000000000..4a6ca64e4cf
--- /dev/null
+++ b/client/bnpl-announcement/style.scss
@@ -0,0 +1,68 @@
+.wcpay-bnpl-announcement {
+ &.wcpay-confirmation-modal.wcpay-confirmation-modal {
+ margin-top: auto;
+ height: auto;
+
+ @media screen and ( min-width: 600px ) {
+ max-width: 400px;
+ }
+
+ .components-modal__header {
+ padding: 0;
+
+ .components-button.has-icon {
+ position: absolute;
+ top: 18px;
+ left: auto;
+ right: 18px;
+ }
+ }
+
+ .components-modal__content {
+ padding: 0 20px 100px;
+ margin-top: 60px;
+
+ @media screen and ( min-width: 600px ) {
+ padding: 0 35px 24px;
+ }
+ }
+
+ .wcpay-confirmation-modal__separator {
+ opacity: 0;
+ }
+ }
+
+ &__payment-icons {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ gap: 17px;
+ margin-bottom: 20px;
+
+ .payment-method__icon {
+ margin-right: 0;
+ max-height: 35px;
+ outline: none;
+
+ &[alt='Affirm'] {
+ max-height: 30px;
+ }
+ }
+ }
+
+ h1 {
+ text-align: left;
+ width: 100%;
+ }
+
+ p {
+ text-align: left;
+ }
+
+ .components-external-link {
+ padding: 6px 12px;
+ align-items: center;
+ display: flex;
+ }
+}
diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx
index 1696db52b2d..7cb4fdf9a15 100644
--- a/client/connect-account-page/index.tsx
+++ b/client/connect-account-page/index.tsx
@@ -31,6 +31,12 @@ import strings from './strings';
import './style.scss';
import InlineNotice from 'components/inline-notice';
+const SandboxModeNotice = () => (
+
+ { strings.sandboxModeNotice }
+
+);
+
const ConnectAccountPage: React.FC = () => {
const firstName = wcSettings.admin?.currentUserData?.first_name;
const incentive = wcpaySettings.connectIncentive;
@@ -50,12 +56,6 @@ const ConnectAccountPage: React.FC = () => {
const isCountrySupported = !! availableCountries[ country ];
- const SandboxModeNotice = () => (
-
- { strings.sandboxModeNotice }
-
- );
-
useEffect( () => {
recordEvent( 'page_view', {
path: 'payments_connect_v2',
diff --git a/includes/admin/class-wc-payments-bnpl-announcement.php b/includes/admin/class-wc-payments-bnpl-announcement.php
new file mode 100644
index 00000000000..8fca5c3d561
--- /dev/null
+++ b/includes/admin/class-wc-payments-bnpl-announcement.php
@@ -0,0 +1,209 @@
+gateway = $gateway;
+ $this->account = $account;
+ $this->current_time = $current_time;
+ }
+
+ /**
+ * Initializes this class's WP hooks.
+ *
+ * @return void
+ */
+ public function init_hooks() {
+ add_action( 'current_screen', [ $this, 'maybe_enqueue_scripts' ] );
+ }
+
+ /**
+ * Needs to run after `current_screen`, to determine which page we're currently on.
+ *
+ * @return void
+ */
+ public function maybe_enqueue_scripts() {
+ if ( ! is_admin() ) {
+ return;
+ }
+
+ // Only shown once to each Administrator and Shop Manager users.
+ if ( ! current_user_can( 'manage_woocommerce' ) ) {
+ return;
+ }
+
+ // Time boxed - Campaign expires after 90 days.
+ if ( $this->current_time > strtotime( '2024-07-15 00:00:00' ) ) {
+ return;
+ }
+
+ if ( get_user_meta( get_current_user_id(), '_wcpay_bnpl_april15_viewed', true ) === '1' ) {
+ return;
+ }
+
+ // Only displayed to BNPL eligible countries - AU, NZ, US, AT, BE, CA, CZ, DK, FI, FR, DE, GR, IE, IT, NO, PL, PT, ES, SE, CH, NL, UK, US.
+ if ( ! in_array(
+ $this->account->get_account_country(),
+ [
+ \WCPay\Constants\Country_Code::AUSTRALIA,
+ \WCPay\Constants\Country_Code::AUSTRIA,
+ \WCPay\Constants\Country_Code::NEW_ZEALAND,
+ \WCPay\Constants\Country_Code::UNITED_STATES,
+ \WCPay\Constants\Country_Code::BELGIUM,
+ \WCPay\Constants\Country_Code::CANADA,
+ \WCPay\Constants\Country_Code::CZECHIA,
+ \WCPay\Constants\Country_Code::DENMARK,
+ \WCPay\Constants\Country_Code::FINLAND,
+ \WCPay\Constants\Country_Code::FRANCE,
+ \WCPay\Constants\Country_Code::GERMANY,
+ \WCPay\Constants\Country_Code::GREECE,
+ \WCPay\Constants\Country_Code::IRELAND,
+ \WCPay\Constants\Country_Code::ITALY,
+ \WCPay\Constants\Country_Code::NORWAY,
+ \WCPay\Constants\Country_Code::POLAND,
+ \WCPay\Constants\Country_Code::PORTUGAL,
+ \WCPay\Constants\Country_Code::SPAIN,
+ \WCPay\Constants\Country_Code::SWEDEN,
+ \WCPay\Constants\Country_Code::SWITZERLAND,
+ \WCPay\Constants\Country_Code::NETHERLANDS,
+ \WCPay\Constants\Country_Code::UNITED_KINGDOM,
+ ],
+ true
+ ) ) {
+ return;
+ }
+
+ // just to be safe for older versions.
+ if ( ! class_exists( '\Automattic\WooCommerce\Admin\PageController' ) ) {
+ return;
+ }
+
+ // Target page to be displayed on - Any WooPayments page except disputes.
+ $current_page = \Automattic\WooCommerce\Admin\PageController::get_instance()->get_current_page();
+ if ( ! WC_Payments_Utils::is_payments_settings_page() && ( empty( $current_page ) || ! in_array(
+ $current_page['id'],
+ [
+ 'wc-payments',
+ 'wc-payments-deposits',
+ 'wc-payments-transactions',
+ 'wc-payments-deposit-details',
+ 'wc-payments-transaction-details',
+ 'wc-payments-multi-currency-setup',
+ ],
+ true
+ ) ) ) {
+ return;
+ }
+
+ // at least 3 purchases (on any payment method).
+ $woopayments_successful_orders_count = $this->get_woopayments_successful_orders_count();
+ if ( $woopayments_successful_orders_count < 3 ) {
+ return;
+ }
+
+ // don't display the promo if the merchant already has BNPL methods enabled.
+ $enabled_bnpl_payment_methods = array_intersect(
+ \WCPay\Constants\Payment_Method::BNPL_PAYMENT_METHODS,
+ $this->gateway->get_upe_enabled_payment_method_ids()
+ );
+ if ( ! empty( $enabled_bnpl_payment_methods ) ) {
+ return;
+ }
+
+ add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
+
+ add_user_meta( get_current_user_id(), '_wcpay_bnpl_april15_viewed', '1' );
+ }
+
+ /**
+ * Enqueues the script & styles for the BNPL announcement dialog.
+ *
+ * @return void
+ */
+ public function enqueue_scripts() {
+ WC_Payments::register_script_with_dependencies( 'WCPAY_BNPL_ANNOUNCEMENT', 'dist/bnpl-announcement' );
+ wp_set_script_translations( 'WCPAY_BNPL_ANNOUNCEMENT', 'woocommerce-payments' );
+ WC_Payments_Utils::register_style(
+ 'WCPAY_BNPL_ANNOUNCEMENT',
+ plugins_url( 'dist/bnpl-announcement.css', WCPAY_PLUGIN_FILE ),
+ [ 'wc-components' ],
+ WC_Payments::get_file_version( 'dist/bnpl-announcement.css' ),
+ 'all'
+ );
+ // conditionally show afterpay/clearpay based on account country.
+ $wcpay_bnpl_announcement = rawurlencode( wp_json_encode( [ 'accountCountry' => $this->account->get_account_country() ] ) );
+ wp_add_inline_script(
+ 'WCPAY_BNPL_ANNOUNCEMENT',
+ "
+ var wcpayBnplAnnouncement = wcpayBnplAnnouncement || JSON.parse( decodeURIComponent( '" . esc_js( $wcpay_bnpl_announcement ) . "' ) );
+ ",
+ 'before'
+ );
+
+ wp_enqueue_script( 'WCPAY_BNPL_ANNOUNCEMENT' );
+ wp_enqueue_style( 'WCPAY_BNPL_ANNOUNCEMENT' );
+ }
+
+ /**
+ * Returns the number of successful orders paid with any WooPayments payment method.
+ *
+ * @return int
+ */
+ private function get_woopayments_successful_orders_count() {
+ // using a transient to store the value of a previous calculation, since it can be expensive on each page load.
+ $wcpay_successful_orders_count = get_transient( 'wcpay_bnpl_april15_successful_purchases_count' );
+ if ( false !== $wcpay_successful_orders_count ) {
+ return intval( $wcpay_successful_orders_count );
+ }
+
+ $orders = wc_get_orders(
+ [
+ // we don't need them all, just more than 3.
+ 'limit' => 5,
+ 'status' => [ 'completed', 'processing' ],
+ ]
+ );
+ $orders_count = count( $orders );
+
+ // storing the transient for a couple of days is probably sufficient, in case the value is too low (less than 3).
+ set_transient( 'wcpay_bnpl_april15_successful_purchases_count', $orders_count, 2 * DAY_IN_SECONDS );
+
+ return $orders_count;
+ }
+}
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 3fc25562912..22c21a3c4f4 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -635,6 +635,10 @@ public static function init() {
$admin_settings = new WC_Payments_Admin_Settings( self::get_gateway() );
$admin_settings->init_hooks();
+ include_once WCPAY_ABSPATH . 'includes/admin/class-wc-payments-bnpl-announcement.php';
+ $bnpl_announcement = new WC_Payments_Bnpl_Announcement( self::get_gateway(), self::get_account_service(), time() );
+ $bnpl_announcement->init_hooks();
+
// Use tracks loader only in admin screens because it relies on WC_Tracks loaded by WC_Admin.
include_once WCPAY_ABSPATH . 'includes/admin/tracks/tracks-loader.php';
diff --git a/tests/unit/admin/test-class-wc-payments-bnpl-announcement.php b/tests/unit/admin/test-class-wc-payments-bnpl-announcement.php
new file mode 100644
index 00000000000..4bbcbfa5a96
--- /dev/null
+++ b/tests/unit/admin/test-class-wc-payments-bnpl-announcement.php
@@ -0,0 +1,136 @@
+gateway_mock = $this->getMockBuilder( WC_Payment_Gateway_WCPay::class )
+ ->disableOriginalConstructor()
+ ->setMethods( [ 'get_upe_enabled_payment_method_ids' ] )
+ ->getMock();
+
+ $this->account_service_mock = $this->getMockBuilder( WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'get_account_country' ] )->getMock();
+
+ $this->bnpl_announcement = new WC_Payments_Bnpl_Announcement( $this->gateway_mock, $this->account_service_mock, strtotime( '2024-06-06' ) );
+ }
+
+ protected function tearDown(): void {
+ parent::tearDown();
+
+ wp_deregister_script( 'WCPAY_BNPL_ANNOUNCEMENT' );
+ }
+
+ public function test_it_enqueues_scripts_for_eligible_users() {
+ global $current_section, $current_tab, $wp_actions;
+
+ // mocking the settings page URL.
+ $current_section = 'woocommerce_payments';
+ $current_tab = 'checkout';
+ $this->set_is_admin( true );
+
+ // mocking the "did action" for 'current_screen'.
+ $wp_actions['current_screen'] = true; // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ wp_set_current_user( self::factory()->user->create( [ 'role' => 'administrator' ] ) );
+ WC_Payments::mode()->live();
+ $this->set_current_user_can( true );
+ $this->account_service_mock->method( 'get_account_country' )->willReturn( 'US' );
+ $this->gateway_mock->method( 'get_upe_enabled_payment_method_ids' )->willReturn( [ 'card' ] );
+
+ $this->bnpl_announcement->maybe_enqueue_scripts();
+
+ do_action( 'admin_enqueue_scripts' );
+
+ // ensuring the dialog has been marked as "viewed".
+ $this->assertEquals( '1', get_user_meta( get_current_user_id(), '_wcpay_bnpl_april15_viewed', true ) );
+ $this->assertTrue( wp_script_is( 'WCPAY_BNPL_ANNOUNCEMENT', 'registered' ) );
+ }
+
+ public function test_it_does_not_enqueues_scripts_for_users_that_have_already_seen_the_message() {
+ global $current_section, $current_tab, $wp_actions;
+
+ // mocking the settings page URL.
+ $current_section = 'woocommerce_payments';
+ $current_tab = 'checkout';
+ $this->set_is_admin( true );
+
+ // mocking the "did action" for 'current_screen'.
+ $wp_actions['current_screen'] = true; // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ wp_set_current_user( self::factory()->user->create( [ 'role' => 'administrator' ] ) );
+ WC_Payments::mode()->live();
+ $this->set_current_user_can( true );
+ $this->account_service_mock->method( 'get_account_country' )->willReturn( 'US' );
+ $this->gateway_mock->method( 'get_upe_enabled_payment_method_ids' )->willReturn( [ 'card' ] );
+
+ // marking it as "already viewed" for the current user.
+ add_user_meta( get_current_user_id(), '_wcpay_bnpl_april15_viewed', '1' );
+
+ $this->bnpl_announcement->maybe_enqueue_scripts();
+
+ do_action( 'admin_enqueue_scripts' );
+
+ $this->assertFalse( wp_script_is( 'WCPAY_BNPL_ANNOUNCEMENT', 'registered' ) );
+ }
+
+ private function set_current_user_can( bool $can ) {
+ global $current_user_can;
+
+ // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
+ $current_user_can = $this->getMockBuilder( \stdClass::class )
+ ->addMethods( [ 'current_user_can' ] )
+ ->getMock();
+
+ $current_user_can->method( 'current_user_can' )->willReturn( $can );
+ }
+
+ /**
+ * @param bool $is_admin
+ */
+ private function set_is_admin( bool $is_admin ) {
+ global $current_screen;
+
+ if ( ! $is_admin ) {
+ $current_screen = null; // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ return;
+ }
+
+ // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
+ $current_screen = $this->getMockBuilder( \stdClass::class )
+ ->setMethods( [ 'in_admin' ] )
+ ->getMock();
+
+ $current_screen->method( 'in_admin' )->willReturn( $is_admin );
+ $current_screen->id = 'wc-payments-deposits';
+ $current_screen->action = null;
+ }
+}
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
index e5afd62b0e5..a73e0120e4a 100755
--- a/tests/unit/bootstrap.php
+++ b/tests/unit/bootstrap.php
@@ -96,6 +96,7 @@ function () {
require_once $_plugin_dir . 'includes/class-woopay-tracker.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-customer-controller.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-refunds-controller.php';
+ require_once $_plugin_dir . 'includes/admin/class-wc-payments-bnpl-announcement.php';
// Load currency helper class early to ensure its implementation is used over the one resolved during further test initialization.
require_once __DIR__ . '/helpers/class-wc-helper-site-currency.php';
diff --git a/webpack/shared.js b/webpack/shared.js
index 63bff86aa23..63a71ca649a 100644
--- a/webpack/shared.js
+++ b/webpack/shared.js
@@ -10,6 +10,7 @@ module.exports = {
entry: mapValues(
{
index: './client/index.js',
+ 'bnpl-announcement': './client/bnpl-announcement/index.js',
settings: './client/settings/index.js',
'blocks-checkout': './client/checkout/blocks/index.js',
woopay: './client/checkout/woopay/index.js',
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index eaa03a07f8a..13540148710 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -55,6 +55,8 @@ function wcpay_activated() {
function wcpay_deactivated() {
require_once WCPAY_ABSPATH . '/includes/class-wc-payments.php';
WC_Payments::remove_woo_admin_notes();
+ delete_user_meta( get_current_user_id(), '_wcpay_bnpl_april15_viewed' );
+ delete_transient( 'wcpay_bnpl_april15_successful_purchases_count' );
}
register_activation_hook( __FILE__, 'wcpay_activated' );