* Add woocommerce.com data source poller

* Change data source back to .com

* Fix namespace

* Make use of the sub_title prop to display payment cards

* Fix some styling issue and add TS types

* Rename admin script to keep it more generic

* Add br tag as allowed, and update expected data format

* Update styling

* Address PR feedback
This commit is contained in:
louwie17 2021-09-09 09:25:13 -03:00 committed by GitHub
parent 4c4161ac3c
commit 7f38f6fd0a
11 changed files with 448 additions and 178 deletions

View File

@ -0,0 +1,46 @@
/**
* External dependencies
*/
import { render } from '@wordpress/element';
/**
* Internal dependencies
*/
import { PaymentPromotionRow } from './payment-promotion-row';
const PAYMENT_METHOD_PROMOTIONS = [
{
gatewayId: 'pre_install_woocommerce_payments_promotion',
pluginSlug: 'woocommerce-payments',
link: 'https://woocommerce.com/payments/?utm_medium=product',
},
];
PAYMENT_METHOD_PROMOTIONS.forEach( ( paymentMethod ) => {
const container = document.querySelector(
`[data-gateway_id="${ paymentMethod.gatewayId }"]`
);
if ( container ) {
const sortColumn = container.children[ 0 ].innerHTML;
const descriptionColumn = container.children[ 3 ].innerHTML;
const title = container.getElementsByClassName(
'wc-payment-gateway-method-title'
);
const subTitle = container.getElementsByClassName( 'gateway-subtitle' );
render(
<PaymentPromotionRow
pluginSlug={ paymentMethod.pluginSlug }
sortColumnContent={ sortColumn }
descriptionColumnContent={ descriptionColumn }
title={ title.length === 1 ? title[ 0 ].innerHTML : undefined }
titleLink={ paymentMethod.link }
subTitleContent={
subTitle.length === 1 ? subTitle[ 0 ].innerHTML : undefined
}
/>,
container
);
}
} );

View File

@ -14,13 +14,26 @@
display: none;
}
}
}
.pre-install-wcpay_name {
.wc_gateways {
.name {
.wc-payment-gateway-method-name {
display: inline-flex;
img {
height: 24px;
width: auto;
margin-left: 4px;
}
}
}
.wc-payment-gateway-method_name {
display: flex;
.pre-install-wcpay_accepted {
.pre-install-payment-gateway_subtitle {
margin-left: $gap;
> svg {
> img {
height: 24px;
width: auto;
margin-left: 4px;

View File

@ -0,0 +1,154 @@
/**
* External dependencies
*/
import { Link } from '@woocommerce/components';
import { Button } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
import {
PLUGINS_STORE_NAME,
PAYMENT_GATEWAYS_STORE_NAME,
WCDataSelector,
PluginsStoreActions,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { useDispatch, useSelect } from '@wordpress/data';
import { sanitize } from 'dompurify';
import { __ } from '@wordpress/i18n';
import _ from 'lodash';
/**
* Internal dependencies
*/
import './payment-promotion-row.scss';
function sanitizeHTML( html: string ) {
return {
__html: sanitize( html, {
ALLOWED_TAGS: [ 'a', 'img', 'br' ],
ALLOWED_ATTR: [ 'href', 'src', 'class', 'alt', 'target' ],
} ),
};
}
type PaymentPromotionRowProps = {
pluginSlug: string;
sortColumnContent: string;
descriptionColumnContent: string;
titleLink: string;
title: string;
subTitleContent?: string;
};
export const PaymentPromotionRow: React.FC< PaymentPromotionRowProps > = ( {
pluginSlug,
sortColumnContent,
descriptionColumnContent,
title,
titleLink,
subTitleContent,
} ) => {
const [ installing, setInstalling ] = useState( false );
const { installAndActivatePlugins }: PluginsStoreActions = useDispatch(
PLUGINS_STORE_NAME
);
const { createNotice } = useDispatch( 'core/notices' );
const { gatewayIsActive, paymentGateway } = useSelect(
( select: WCDataSelector ) => {
const { getPaymentGateway } = select( PAYMENT_GATEWAYS_STORE_NAME );
const activePlugins: string[] = select(
PLUGINS_STORE_NAME
).getActivePlugins();
const isActive =
activePlugins && activePlugins.includes( pluginSlug );
let paymentGatewayData;
if ( isActive ) {
paymentGatewayData = getPaymentGateway(
pluginSlug.replace( /\-/g, '_' )
);
}
return {
gatewayIsActive: isActive,
paymentGateway: paymentGatewayData,
};
}
);
useEffect( () => {
if (
gatewayIsActive &&
paymentGateway &&
paymentGateway.settings_url
) {
window.location.href = paymentGateway.settings_url;
}
}, [ gatewayIsActive, paymentGateway ] );
const installPaymentGateway = () => {
if ( installing ) {
return;
}
setInstalling( true );
recordEvent( 'settings_payments_recommendations_setup', {
extension_selected: pluginSlug,
} );
installAndActivatePlugins( [ pluginSlug ] ).catch(
( response: { message?: string } ) => {
if ( response.message ) {
createNotice( 'error', response.message );
}
setInstalling( false );
}
);
};
return (
<>
<td
className="sort ui-sortable-handle"
width="1%"
dangerouslySetInnerHTML={ {
__html: sortColumnContent,
} }
></td>
<td className="name">
<div className="wc-payment-gateway-method_name">
<Link
target="_blank"
type="external"
rel="noreferrer"
href={ titleLink }
>
{ title }
</Link>
{ subTitleContent ? (
<div
className="pre-install-payment-gateway_subtitle"
dangerouslySetInnerHTML={ sanitizeHTML(
subTitleContent
) }
></div>
) : null }
</div>
</td>
<td className="pre-install-payment-gateway_status"></td>
<td
className="description"
dangerouslySetInnerHTML={ sanitizeHTML(
descriptionColumnContent
) }
></td>
<td className="action">
<Button
className="button alignright"
onClick={ () => installPaymentGateway() }
isSecondary
isBusy={ installing }
aria-disabled={ installing }
>
{ __( 'Install', 'woocommerce-admin' ) }
</Button>
</td>
</>
);
};

View File

@ -1,25 +0,0 @@
/**
* External dependencies
*/
import { render } from '@wordpress/element';
/**
* Internal dependencies
*/
import { WCPaymentsRow } from './wc-payments-row';
const container = document.querySelector(
'[data-gateway_id="pre_install_woocommerce_payments_promotion"]'
);
if ( container ) {
const sortColumn = container.children[ 0 ].innerHTML;
const descriptionColumn = container.children[ 3 ].innerHTML;
render(
<WCPaymentsRow
sortColumnContent={ sortColumn }
descriptionColumnContent={ descriptionColumn }
/>,
container
);
}

View File

@ -1,141 +0,0 @@
/**
* External dependencies
*/
import { Link } from '@woocommerce/components';
import { Button } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
import {
PLUGINS_STORE_NAME,
PAYMENT_GATEWAYS_STORE_NAME,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { useDispatch, useSelect } from '@wordpress/data';
import {
Visa,
MasterCard,
Amex,
ApplePay,
GooglePay,
} from '@woocommerce/onboarding';
import { __ } from '@wordpress/i18n';
import _ from 'lodash';
/**
* Internal dependencies
*/
import './wc-payments-row.scss';
type WCPaymentsRowProps = {
sortColumnContent: string;
descriptionColumnContent: string;
};
const WC_PAY_SLUG = 'woocommerce-payments';
export const WCPaymentsRow: React.FC< WCPaymentsRowProps > = ( {
sortColumnContent,
descriptionColumnContent,
} ) => {
const [ installing, setInstalling ] = useState( false );
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
const { createNotice } = useDispatch( 'core/notices' );
const wcPayInstallationInfo = useSelect( ( select ) => {
const { getPaymentGateway } = select( PAYMENT_GATEWAYS_STORE_NAME );
const activePlugins: string[] = select(
PLUGINS_STORE_NAME
).getActivePlugins();
const isWCPayActive =
activePlugins && activePlugins.includes( WC_PAY_SLUG );
let wcPayGateway;
if ( isWCPayActive ) {
wcPayGateway = getPaymentGateway(
WC_PAY_SLUG.replace( /\-/g, '_' )
);
}
return {
isWCPayActive,
wcPayGateway,
};
} );
useEffect( () => {
if (
wcPayInstallationInfo.isWCPayActive &&
wcPayInstallationInfo.wcPayGateway &&
wcPayInstallationInfo.wcPayGateway.settings_url
) {
window.location.href =
wcPayInstallationInfo.wcPayGateway.settings_url;
}
}, [
wcPayInstallationInfo.isWCPayActive,
wcPayInstallationInfo.wcPayGateway,
] );
const installWCPay = () => {
if ( installing ) {
return;
}
setInstalling( true );
recordEvent( 'settings_payments_recommendations_setup', {
extension_selected: WC_PAY_SLUG,
} );
installAndActivatePlugins( [ WC_PAY_SLUG ] ).catch(
( response: { message?: string } ) => {
if ( response.message ) {
createNotice( 'error', response.message );
}
setInstalling( false );
}
);
};
return (
<>
<td
className="sort ui-sortable-handle"
width="1%"
dangerouslySetInnerHTML={ {
__html: sortColumnContent,
} }
></td>
<td className="name">
<div className="pre-install-wcpay_name">
<Link
target="_blank"
type="external"
rel="noreferrer"
href="https://woocommerce.com/payments/?utm_medium=product"
>
{ __( 'WooCommerce Payments', 'woocommerce-admin' ) }
</Link>
<div className="pre-install-wcpay_accepted">
<Visa />
<MasterCard />
<Amex />
<GooglePay />
<ApplePay />
</div>
</div>
</td>
<td className="pre-install-wcpay_status"></td>
<td
className="description"
dangerouslySetInnerHTML={ {
__html: descriptionColumnContent,
} }
></td>
<td className="action">
<Button
className="button alignright"
onClick={ () => installWCPay() }
isSecondary
isBusy={ installing }
aria-disabled={ installing }
>
{ __( 'Install', 'woocommerce-admin' ) }
</Button>
</td>
</>
);
};

View File

@ -27,6 +27,7 @@ export { useSettings } from './settings/use-settings';
export { PLUGINS_STORE_NAME } from './plugins';
export type { Plugin } from './plugins/types';
export { ActionDispatchers as PluginsStoreActions } from './plugins/actions';
export { pluginNames } from './plugins/constants';
export { withPluginsHydration } from './plugins/with-plugins-hydration';

View File

@ -357,3 +357,9 @@ export type Actions =
| ReturnType< typeof updateJetpackConnectUrl >
| ReturnType< typeof setPaypalOnboardingStatus >
| ReturnType< typeof setRecommendedPlugins >;
// Types
export type ActionDispatchers = {
installJetpackAndConnect: typeof installJetpackAndConnect;
installAndActivatePlugins: typeof installAndActivatePlugins;
};

View File

@ -0,0 +1,131 @@
<?php
/**
* Handles polling and storage of specs
*/
namespace Automattic\WooCommerce\Admin\Features\WcPayPromotion;
defined( 'ABSPATH' ) || exit;
/**
* Specs data source poller class.
* This handles polling specs from JSON endpoints.
*/
class DataSourcePoller {
/**
* Name of data sources filter.
*/
const FILTER_NAME = 'woocommerce_admin_payment_method_promotions_data_sources';
/**
* Default data sources array.
*/
const DATA_SOURCES = array(
'https://woocommerce.com/wp-json/wccom/payment-gateway-suggestions/1.0/payment-method/promotions.json',
);
/**
* The logger instance.
*
* @var WC_Logger|null
*/
protected static $logger = null;
/**
* Get the logger instance.
*
* @return WC_Logger
*/
private static function get_logger() {
if ( is_null( self::$logger ) ) {
self::$logger = wc_get_logger();
}
return self::$logger;
}
/**
* Reads the data sources for specs and persists those specs.
*
* @return bool Whether any specs were read.
*/
public static function read_specs_from_data_sources() {
$specs = array();
$data_sources = apply_filters( self::FILTER_NAME, self::DATA_SOURCES );
// Note that this merges the specs from the data sources based on the
// product - last one wins.
foreach ( $data_sources as $url ) {
$specs_from_data_source = self::read_data_source( $url );
self::merge_specs( $specs_from_data_source, $specs, $url );
}
return $specs;
}
/**
* Read a single data source and return the read specs
*
* @param string $url The URL to read the specs from.
*
* @return array The specs that have been read from the data source.
*/
private static function read_data_source( $url ) {
$logger_context = array( 'source' => $url );
$logger = self::get_logger();
$response = wp_remote_get(
add_query_arg(
'_locale',
get_user_locale(),
$url
)
);
if ( is_wp_error( $response ) || ! isset( $response['body'] ) ) {
$logger->error(
'Error getting remote payment method data feed',
$logger_context
);
// phpcs:ignore
$logger->error( print_r( $response, true ), $logger_context );
return [];
}
$body = $response['body'];
$specs = json_decode( $body );
if ( null === $specs ) {
$logger->error(
'Empty response in remote payment method data feed',
$logger_context
);
return [];
}
if ( ! is_array( $specs ) ) {
$logger->error(
'Remote payment method data feed is not an array',
$logger_context
);
return [];
}
return $specs;
}
/**
* Merge the specs.
*
* @param Array $specs_to_merge_in The specs to merge in to $specs.
* @param Array $specs The list of specs being merged into.
*/
private static function merge_specs( $specs_to_merge_in, &$specs ) {
foreach ( $specs_to_merge_in as $spec ) {
$id = $spec->product;
$specs[ $id ] = $spec;
}
}
}

View File

@ -9,17 +9,21 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\Loader;
use Automattic\WooCommerce\Admin\PaymentPlugins;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\EvaluateSuggestion;
/**
* WC Pay Promotion engine.
*/
class Init {
const SPECS_TRANSIENT_NAME = 'woocommerce_admin_payment_method_promotion_specs';
/**
* Constructor.
*/
public function __construct() {
include_once __DIR__ . '/WCPaymentGatewayPreInstallWCPayPromotion.php';
add_action( 'change_locale', array( __CLASS__, 'delete_specs_transient' ) );
add_filter( PaymentPlugins::FILTER_NAME, array( __CLASS__, 'possibly_filter_recommended_payment_gateways' ) );
if ( ! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] || ! isset( $_GET['tab'] ) || 'checkout' !== $_GET['tab'] ) { // phpcs:ignore WordPress.Security.NonceVerification
@ -33,18 +37,18 @@ class Init {
$rtl = is_rtl() ? '.rtl' : '';
wp_enqueue_style(
'wc-admin-wc-pay-payments-promotion',
Loader::get_url( "wc-pay-payments-promotion/style{$rtl}", 'css' ),
'wc-admin-payment-method-promotions',
Loader::get_url( "payment-method-promotions/style{$rtl}", 'css' ),
array( 'wp-components' ),
Loader::get_file_version( 'css' )
);
$script_assets_filename = Loader::get_script_asset_filename( 'wp-admin-scripts', 'wc-pay-payments-promotion' );
$script_assets_filename = Loader::get_script_asset_filename( 'wp-admin-scripts', 'payment-method-promotions' );
$script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . 'wp-admin-scripts/' . $script_assets_filename;
wp_enqueue_script(
'wc-admin-wc-pay-payments-promotion',
Loader::get_url( 'wp-admin-scripts/wc-pay-payments-promotion', 'js' ),
'wc-admin-payment-method-promotions',
Loader::get_url( 'wp-admin-scripts/payment-method-promotions', 'js' ),
array_merge( array( WC_ADMIN_APP ), $script_assets ['dependencies'] ),
Loader::get_file_version( 'js' ),
true
@ -95,6 +99,11 @@ class Init {
if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
return false;
}
$wc_pay_spec = self::get_wc_pay_promotion_spec();
if ( ! $wc_pay_spec ) {
return false;
}
return true;
}
@ -116,5 +125,77 @@ class Init {
}
return $ordering;
}
/**
* Get WC Pay promotion spec.
*/
public static function get_wc_pay_promotion_spec() {
$promotions = self::get_promotions();
$wc_pay_promotion_spec = array_values(
array_filter(
$promotions,
function( $promotion ) {
return isset( $promotion->plugins ) && in_array( 'woocommerce-payments', $promotion->plugins, true );
}
)
);
return current( $wc_pay_promotion_spec );
}
/**
* Go through the specs and run them.
*/
public static function get_promotions() {
$suggestions = array();
$specs = self::get_specs();
foreach ( $specs as $spec ) {
$suggestion = EvaluateSuggestion::evaluate( $spec );
$suggestions[] = $suggestion;
}
return array_values(
array_filter(
$suggestions,
function( $suggestion ) {
return ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible;
}
)
);
}
/**
* Delete the specs transient.
*/
public static function delete_specs_transient() {
delete_transient( self::SPECS_TRANSIENT_NAME );
}
/**
* Get specs or fetch remotely if they don't exist.
*/
public static function get_specs() {
$specs = get_transient( self::SPECS_TRANSIENT_NAME );
// Fetch specs if they don't yet exist.
if ( false === $specs || ! is_array( $specs ) || 0 === count( $specs ) ) {
if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
return array();
}
$specs = DataSourcePoller::read_specs_from_data_sources();
// Fall back to default specs if polling failed.
if ( ! $specs ) {
return array();
}
set_transient( self::SPECS_TRANSIENT_NAME, $specs, 7 * DAY_IN_SECONDS );
}
return $specs;
}
}

View File

@ -24,9 +24,13 @@ class WCPaymentGatewayPreInstallWCPayPromotion extends \WC_Payment_Gateway {
* Constructor
*/
public function __construct() {
$this->id = static::GATEWAY_ID;
$this->title = __( 'WooCommerce Payments', 'woocommerce-admin' );
$this->method_description = ''; // will be replaced by wc.com data.
$wc_pay_spec = Init::get_wc_pay_promotion_spec();
$this->id = static::GATEWAY_ID;
$this->method_title = $wc_pay_spec->title;
if ( property_exists( $wc_pay_spec, 'sub_title' ) ) {
$this->title = sprintf( '<span class="gateway-subtitle" >%s</span>', $wc_pay_spec->sub_title );
}
$this->method_description = $wc_pay_spec->content;
$this->has_fields = false;
// Get setting values.

View File

@ -52,7 +52,7 @@ const wpAdminScripts = [
'onboarding-tax-notice',
'print-shipping-label-banner',
'beta-features-tracking-modal',
'wc-pay-payments-promotion',
'payment-method-promotions',
];
wpAdminScripts.forEach( ( name ) => {
entryPoints[ name ] = `./client/wp-admin-scripts/${ name }`;