initial
This commit is contained in:
+108
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 3,
|
||||
"name": "jetpack/simple-payments",
|
||||
"title": "Pay with PayPal",
|
||||
"description": "Add credit and debit card payment buttons with minimal setup. Good for collecting donations or payments for products and services.",
|
||||
"keywords": [
|
||||
"buy",
|
||||
"commerce",
|
||||
"credit card",
|
||||
"debit card",
|
||||
"monetize",
|
||||
"earn",
|
||||
"ecommerce",
|
||||
"money",
|
||||
"paid",
|
||||
"payments",
|
||||
"products",
|
||||
"purchase",
|
||||
"sell",
|
||||
"shop",
|
||||
"square",
|
||||
"payments"
|
||||
],
|
||||
"version": "12.5.0",
|
||||
"textdomain": "jetpack-paypal-payments",
|
||||
"category": "monetize",
|
||||
"icon": "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'><path fill='none' d='M0 0h24v24H0V0z' /><path d='M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z' /></svg>",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"className": false,
|
||||
"customClassName": false,
|
||||
"html": false,
|
||||
"inserter": false,
|
||||
"reusable": false
|
||||
},
|
||||
"attributes": {
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"default": "USD"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"source": "html",
|
||||
"selector": ".jetpack-simple-payments-description p",
|
||||
"default": ""
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"featuredMediaId": {
|
||||
"type": "number",
|
||||
"default": 0
|
||||
},
|
||||
"featuredMediaUrl": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": ".jetpack-simple-payments-image img",
|
||||
"attribute": "src",
|
||||
"default": null
|
||||
},
|
||||
"featuredMediaTitle": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": ".jetpack-simple-payments-image img",
|
||||
"attribute": "alt",
|
||||
"default": null
|
||||
},
|
||||
"multiple": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"postLinkUrl": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": ".jetpack-simple-payments-purchase",
|
||||
"attribute": "href"
|
||||
},
|
||||
"postLinkText": {
|
||||
"type": "string",
|
||||
"source": "html",
|
||||
"selector": ".jetpack-simple-payments-purchase",
|
||||
"default": "Click here to purchase."
|
||||
},
|
||||
"price": {
|
||||
"type": "number"
|
||||
},
|
||||
"productId": {
|
||||
"type": "number"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"source": "html",
|
||||
"selector": ".jetpack-simple-payments-title p",
|
||||
"default": ""
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"attributes": {
|
||||
"price": 25.0,
|
||||
"title": "Jetpack t-shirt",
|
||||
"content": "Take flight in ultimate comfort with this stylish t-shirt featuring the Jetpack logo.",
|
||||
"email": "jetpack@jetpack.com",
|
||||
"featuredMediaUrl": "./simple-payments_example-1.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/**
|
||||
* Pay with PayPal block (aka Simple Payments).
|
||||
*
|
||||
* @since jetpack-9.0.0
|
||||
*
|
||||
* @package automattic/jetpack-paypal-payments
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\PaypalPayments\SimplePayments;
|
||||
|
||||
use Automattic\Jetpack\Assets;
|
||||
use Automattic\Jetpack\Blocks;
|
||||
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
|
||||
use Automattic\Jetpack\Paypal_Payments\Simple_Payments;
|
||||
use Automattic\Jetpack\Status\Request;
|
||||
use WP_Post;
|
||||
|
||||
/**
|
||||
* Register and render the block.
|
||||
*/
|
||||
class Block {
|
||||
/**
|
||||
* The block full slugname.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const BLOCK_NAME = 'jetpack/simple-payments';
|
||||
|
||||
/**
|
||||
* Registers the block for use in Gutenberg
|
||||
* This is done via an action so that we can disable
|
||||
* registration if we need to.
|
||||
*/
|
||||
public static function register_block() {
|
||||
if ( ! Jetpack_Plan::supports( 'simple-payments' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Blocks::jetpack_register_block(
|
||||
__DIR__,
|
||||
array( 'render_callback' => array( __CLASS__, 'render_block' ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pay with PayPal block dynamic rendering.
|
||||
*
|
||||
* @param array $attr Array containing the block attributes.
|
||||
* @param string $content String containing the block content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function render_block( $attr, $content ) {
|
||||
// Do nothing if block content is a `simple-payment` shortcode.
|
||||
if ( preg_match( '/\[simple-payment(.*)]/', $content ) ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Keep content as-is if rendered in other contexts than frontend (i.e. feed, emails, API, etc.).
|
||||
if ( ! Request::is_frontend() ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$simple_payments = Simple_Payments::get_instance();
|
||||
|
||||
if ( ! $simple_payments->is_valid( $attr ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$simple_payments->enqueue_frontend_assets();
|
||||
|
||||
// For AMP requests, make sure the purchase link redirects to the non-AMP post URL.
|
||||
if ( Blocks::is_amp_request() ) {
|
||||
$content = preg_replace(
|
||||
'#(<a class="jetpack-simple-payments-purchase".*)rel="(.*)"(.*>.*</a>)#i',
|
||||
'$1rel="$2 noamphtml"$3',
|
||||
$content
|
||||
);
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Augment block UI with a PayPal button if rendered on the frontend.
|
||||
$product_id = $attr['productId'];
|
||||
$dom_id = wp_unique_id( "jetpack-simple-payments-{$product_id}_" );
|
||||
$is_multiple = get_post_meta( $product_id, 'spay_multiple', true ) || '0';
|
||||
|
||||
$simple_payments->setup_paypal_checkout_button( $product_id, $dom_id, $is_multiple );
|
||||
|
||||
$purchase_box = $simple_payments->output_purchase_box( $dom_id, $is_multiple );
|
||||
$content = preg_replace( '#<a class="jetpack-simple-payments-purchase(.*)</a>#i', $purchase_box, $content );
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load editor styles for the block.
|
||||
* These are loaded via enqueue_block_assets to ensure proper loading in the editor iframe context.
|
||||
*/
|
||||
public static function load_editor_styles() {
|
||||
$handle = 'jp-paypal-payments-blocks';
|
||||
|
||||
Assets::register_script(
|
||||
$handle,
|
||||
'../../dist/block/editor.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'css_path' => '../../dist/block/editor.css',
|
||||
'textdomain' => 'jetpack-paypal-payments',
|
||||
)
|
||||
);
|
||||
wp_enqueue_style( $handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads scripts
|
||||
*/
|
||||
public static function load_editor_scripts() {
|
||||
Assets::register_script(
|
||||
'jp-paypal-payments-blocks',
|
||||
'../../dist/block/editor.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'in_footer' => true,
|
||||
'textdomain' => 'jetpack-paypal-payments',
|
||||
'enqueue' => true,
|
||||
// Editor styles are loaded separately, see load_editor_styles().
|
||||
'css_path' => null,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if AMP should be disabled on posts having "Pay with PayPal" blocks.
|
||||
*
|
||||
* @param bool $skip Skipped.
|
||||
* @param int $post_id Post ID.
|
||||
* @param WP_Post $post Post.
|
||||
*
|
||||
* @return bool Whether to skip the post from AMP.
|
||||
*/
|
||||
public static function amp_skip_post( $skip, $post_id, $post ) {
|
||||
/*
|
||||
* When AMP is on standard mode,
|
||||
* there are no non-AMP posts to link to where
|
||||
* the purchase can be completed,
|
||||
* so let's prevent the post from being available in AMP.
|
||||
*/
|
||||
if (
|
||||
function_exists( 'amp_is_canonical' )
|
||||
&& \amp_is_canonical()
|
||||
&& has_block( self::BLOCK_NAME, $post->post_content )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $skip;
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { RawHTML } from '@wordpress/element';
|
||||
|
||||
export default {
|
||||
attributes: {
|
||||
currency: {
|
||||
type: 'string',
|
||||
default: 'USD',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
featuredMediaId: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
},
|
||||
featuredMediaUrl: {
|
||||
type: 'string',
|
||||
default: null,
|
||||
},
|
||||
featuredMediaTitle: {
|
||||
type: 'string',
|
||||
default: null,
|
||||
},
|
||||
multiple: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
},
|
||||
productId: {
|
||||
type: 'number',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
supports: {
|
||||
align: false,
|
||||
className: false,
|
||||
customClassName: false,
|
||||
html: false,
|
||||
reusable: false,
|
||||
},
|
||||
save: ( { attributes } ) => {
|
||||
const { productId } = attributes;
|
||||
return productId ? <RawHTML>{ `[simple-payment id="${ productId }"]` }</RawHTML> : null;
|
||||
},
|
||||
};
|
||||
+624
@@ -0,0 +1,624 @@
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import {
|
||||
Disabled,
|
||||
PanelBody,
|
||||
SelectControl,
|
||||
TextareaControl,
|
||||
TextControl,
|
||||
ToggleControl,
|
||||
} from '@wordpress/components';
|
||||
import { compose, withInstanceId } from '@wordpress/compose';
|
||||
import { dispatch, withSelect } from '@wordpress/data';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { Link } from '@wordpress/ui';
|
||||
import clsx from 'clsx';
|
||||
import { validate as emailValidatorValidate } from 'email-validator';
|
||||
import { isEmpty, isEqual, pick, trimEnd } from 'lodash';
|
||||
import { SIMPLE_PAYMENTS_PRODUCT_POST_TYPE, SUPPORTED_CURRENCY_LIST } from '../../constants';
|
||||
import FeaturedMedia from '../../featured-media';
|
||||
import HelpMessage from '../../help-message';
|
||||
import ProductPlaceholder from '../../product-placeholder';
|
||||
import { decimalPlaces, formatPriceFallback, getCurrencyDefaults } from '../../utils';
|
||||
|
||||
class SimplePaymentsEdit extends Component {
|
||||
state = {
|
||||
fieldEmailError: null,
|
||||
fieldPriceError: null,
|
||||
fieldTitleError: null,
|
||||
isSavingProduct: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* We'll use this flag to inject attributes one time when the product entity is loaded.
|
||||
*
|
||||
* It is based on the presence of a `productId` attribute.
|
||||
*
|
||||
* If present, initially we are waiting for attributes to be injected.
|
||||
* If absent, we may save the product in the future but do not need to inject attributes based
|
||||
* on the response as they will have come from our product submission.
|
||||
*/
|
||||
shouldInjectPaymentAttributes = !! this.props.attributes.productId;
|
||||
|
||||
componentDidMount() {
|
||||
// Try to get the simplePayment loaded into attributes if possible.
|
||||
this.injectPaymentAttributes();
|
||||
|
||||
const { attributes, hasPublishAction, postLinkUrl, setAttributes } = this.props;
|
||||
const { productId } = attributes;
|
||||
|
||||
// If the user can publish save an empty product so that we have an ID and can save
|
||||
// concurrently with the post that contains the Simple Payment.
|
||||
if ( ! productId && hasPublishAction ) {
|
||||
this.saveProduct();
|
||||
}
|
||||
|
||||
const shouldUpdatePostLinkUrl =
|
||||
postLinkUrl && postLinkUrl !== this.props.attributes.postLinkUrl;
|
||||
const shouldUpdatePostLinkText = ! this.props.attributes.postLinkText;
|
||||
if ( shouldUpdatePostLinkUrl || shouldUpdatePostLinkText ) {
|
||||
setAttributes( {
|
||||
...( shouldUpdatePostLinkUrl && { postLinkUrl } ),
|
||||
...( shouldUpdatePostLinkText && {
|
||||
postLinkText: __( 'Click here to purchase.', 'jetpack-paypal-payments' ),
|
||||
} ),
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
const { hasPublishAction, isSelected, postLinkUrl, setAttributes } = this.props;
|
||||
|
||||
if ( ! isEqual( prevProps.simplePayment, this.props.simplePayment ) ) {
|
||||
this.injectPaymentAttributes();
|
||||
}
|
||||
|
||||
if (
|
||||
! prevProps.isSaving &&
|
||||
this.props.isSaving &&
|
||||
hasPublishAction &&
|
||||
this.validateAttributes()
|
||||
) {
|
||||
// Validate and save product on post save
|
||||
this.saveProduct();
|
||||
} else if ( prevProps.isSelected && ! isSelected ) {
|
||||
// Validate on block deselect
|
||||
this.validateAttributes();
|
||||
}
|
||||
|
||||
const shouldUpdatePostLinkUrl =
|
||||
postLinkUrl && postLinkUrl !== this.props.attributes.postLinkUrl;
|
||||
const shouldUpdatePostLinkText = ! this.props.attributes.postLinkText;
|
||||
if ( shouldUpdatePostLinkUrl || shouldUpdatePostLinkText ) {
|
||||
setAttributes( {
|
||||
...( shouldUpdatePostLinkUrl && { postLinkUrl } ),
|
||||
...( shouldUpdatePostLinkText && {
|
||||
postLinkText: __( 'Click here to purchase.', 'jetpack-paypal-payments' ),
|
||||
} ),
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
injectPaymentAttributes() {
|
||||
/**
|
||||
* Prevent injecting the product attributes when not desired.
|
||||
*
|
||||
* When we first load a product, we should inject its attributes as our initial form state.
|
||||
* When subsequent saves occur, we should avoid injecting attributes so that we do not
|
||||
* overwrite changes that the user has made with stale state from the previous save.
|
||||
*/
|
||||
|
||||
const { simplePayment, featuredMedia } = this.props;
|
||||
if ( ! this.shouldInjectPaymentAttributes || isEmpty( simplePayment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { attributes, setAttributes } = this.props;
|
||||
const {
|
||||
content,
|
||||
currency,
|
||||
email,
|
||||
featuredMediaId,
|
||||
featuredMediaUrl,
|
||||
featuredMediaTitle,
|
||||
multiple,
|
||||
price,
|
||||
title,
|
||||
} = attributes;
|
||||
|
||||
setAttributes( {
|
||||
content: simplePayment?.content?.raw ?? content,
|
||||
currency: simplePayment?.meta?.spay_currency ?? currency,
|
||||
email: simplePayment?.meta?.spay_email ?? email,
|
||||
featuredMediaId: simplePayment?.featured_media ?? featuredMediaId,
|
||||
featuredMediaUrl: featuredMedia?.url ?? featuredMediaUrl,
|
||||
featuredMediaTitle: featuredMedia?.title ?? featuredMediaTitle,
|
||||
multiple: Boolean( simplePayment?.meta?.spay_multiple ?? multiple ),
|
||||
price: simplePayment?.meta?.spay_price ?? ( price || undefined ),
|
||||
title: simplePayment?.title?.raw ?? title,
|
||||
} );
|
||||
|
||||
this.shouldInjectPaymentAttributes = ! this.shouldInjectPaymentAttributes;
|
||||
}
|
||||
|
||||
toApi() {
|
||||
const { attributes } = this.props;
|
||||
const { content, currency, email, featuredMediaId, multiple, price, productId, title } =
|
||||
attributes;
|
||||
|
||||
return {
|
||||
id: productId,
|
||||
content,
|
||||
featured_media: featuredMediaId,
|
||||
meta: {
|
||||
spay_currency: currency,
|
||||
spay_email: email,
|
||||
spay_multiple: multiple,
|
||||
spay_price: price,
|
||||
},
|
||||
status: productId ? 'publish' : 'draft',
|
||||
title,
|
||||
};
|
||||
}
|
||||
|
||||
saveProduct() {
|
||||
if ( this.state.isSavingProduct ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { attributes, setAttributes } = this.props;
|
||||
const { email } = attributes;
|
||||
const { saveEntityRecord } = dispatch( 'core' );
|
||||
|
||||
this.setState( { isSavingProduct: true }, () => {
|
||||
saveEntityRecord( 'postType', SIMPLE_PAYMENTS_PRODUCT_POST_TYPE, this.toApi() )
|
||||
.then( record => {
|
||||
if ( record ) {
|
||||
setAttributes( { productId: record.id } );
|
||||
}
|
||||
|
||||
return record;
|
||||
} )
|
||||
.catch( error => {
|
||||
// Nothing we can do about errors without details at the moment
|
||||
if ( ! error || ! error.data ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
data: { key: apiErrorKey },
|
||||
} = error;
|
||||
|
||||
// @TODO errors in other fields
|
||||
this.setState( {
|
||||
fieldEmailError:
|
||||
apiErrorKey === 'spay_email'
|
||||
? sprintf(
|
||||
/* translators: %s: an email address. */
|
||||
__( '%s is not a valid email address.', 'jetpack-paypal-payments' ),
|
||||
email
|
||||
)
|
||||
: null,
|
||||
fieldPriceError:
|
||||
apiErrorKey === 'spay_price'
|
||||
? __( 'Invalid price.', 'jetpack-paypal-payments' )
|
||||
: null,
|
||||
} );
|
||||
} )
|
||||
.finally( () => {
|
||||
this.setState( {
|
||||
isSavingProduct: false,
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
validateAttributes = () => {
|
||||
const isPriceValid = this.validatePrice();
|
||||
const isTitleValid = this.validateTitle();
|
||||
const isEmailValid = this.validateEmail();
|
||||
const isCurrencyValid = this.validateCurrency();
|
||||
|
||||
return isPriceValid && isTitleValid && isEmailValid && isCurrencyValid;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate currency
|
||||
*
|
||||
* This method does not include validation UI. Currency selection should not allow for invalid
|
||||
* values. It is primarily to ensure that the currency is valid to save.
|
||||
*
|
||||
* @return {boolean} True if currency is valid
|
||||
*/
|
||||
validateCurrency = () => {
|
||||
const { currency } = this.props.attributes;
|
||||
return SUPPORTED_CURRENCY_LIST.includes( currency );
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate price
|
||||
*
|
||||
* Stores error message in state.fieldPriceError
|
||||
*
|
||||
* @return {boolean} True when valid, false when invalid
|
||||
*/
|
||||
validatePrice = () => {
|
||||
const { currency, price } = this.props.attributes;
|
||||
|
||||
if ( ! price || parseFloat( price ) === 0 ) {
|
||||
this.setState( {
|
||||
fieldPriceError: __(
|
||||
'If you’re selling something, you need a price tag. Add yours here.',
|
||||
'jetpack-paypal-payments'
|
||||
),
|
||||
} );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( Number.isNaN( parseFloat( price ) ) ) {
|
||||
this.setState( {
|
||||
fieldPriceError: __( 'Invalid price', 'jetpack-paypal-payments' ),
|
||||
} );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( parseFloat( price ) < 0 ) {
|
||||
this.setState( {
|
||||
fieldPriceError: __(
|
||||
'Your price is negative — enter a positive number so people can pay the right amount.',
|
||||
'jetpack-paypal-payments'
|
||||
),
|
||||
} );
|
||||
return false;
|
||||
}
|
||||
|
||||
const { precision } = getCurrencyDefaults( currency );
|
||||
if ( decimalPlaces( price ) > precision ) {
|
||||
if ( precision === 0 ) {
|
||||
this.setState( {
|
||||
fieldPriceError: __(
|
||||
'We know every penny counts, but prices in this currency can’t contain decimal values.',
|
||||
'jetpack-paypal-payments'
|
||||
),
|
||||
} );
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState( {
|
||||
fieldPriceError: sprintf(
|
||||
/* translators: %d: the number of decimals in a number. */
|
||||
_n(
|
||||
'The price cannot have more than %d decimal place.',
|
||||
'The price cannot have more than %d decimal places.',
|
||||
precision,
|
||||
'jetpack-paypal-payments'
|
||||
),
|
||||
precision
|
||||
),
|
||||
} );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( this.state.fieldPriceError ) {
|
||||
this.setState( { fieldPriceError: null } );
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate email
|
||||
*
|
||||
* Stores error message in state.fieldEmailError
|
||||
*
|
||||
* @return {boolean} True when valid, false when invalid
|
||||
*/
|
||||
validateEmail = () => {
|
||||
const { email } = this.props.attributes;
|
||||
if ( ! email ) {
|
||||
this.setState( {
|
||||
fieldEmailError: __(
|
||||
'We want to make sure payments reach you, so please add an email address.',
|
||||
'jetpack-paypal-payments'
|
||||
),
|
||||
} );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! emailValidatorValidate( email ) ) {
|
||||
this.setState( {
|
||||
fieldEmailError: sprintf(
|
||||
/* translators: %s: an email address. */
|
||||
__( '%s is not a valid email address.', 'jetpack-paypal-payments' ),
|
||||
email
|
||||
),
|
||||
} );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( this.state.fieldEmailError ) {
|
||||
this.setState( { fieldEmailError: null } );
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate title
|
||||
*
|
||||
* Stores error message in state.fieldTitleError
|
||||
*
|
||||
* @return {boolean} True when valid, false when invalid
|
||||
*/
|
||||
validateTitle = () => {
|
||||
const { title } = this.props.attributes;
|
||||
if ( ! title ) {
|
||||
this.setState( {
|
||||
fieldTitleError: __(
|
||||
'Please add a brief title so that people know what they’re paying for.',
|
||||
'jetpack-paypal-payments'
|
||||
),
|
||||
} );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( this.state.fieldTitleError ) {
|
||||
this.setState( { fieldTitleError: null } );
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
handleEmailChange = email => {
|
||||
this.props.setAttributes( { email } );
|
||||
this.setState( { fieldEmailError: null } );
|
||||
};
|
||||
|
||||
handleContentChange = content => {
|
||||
this.props.setAttributes( { content } );
|
||||
};
|
||||
|
||||
handlePriceChange = price => {
|
||||
price = parseFloat( price );
|
||||
if ( ! isNaN( price ) ) {
|
||||
this.props.setAttributes( { price } );
|
||||
} else {
|
||||
this.props.setAttributes( { price: undefined } );
|
||||
}
|
||||
this.setState( { fieldPriceError: null } );
|
||||
};
|
||||
|
||||
handleCurrencyChange = currency => {
|
||||
this.props.setAttributes( { currency } );
|
||||
};
|
||||
|
||||
handleMultipleChange = multiple => {
|
||||
this.props.setAttributes( { multiple: !! multiple } );
|
||||
};
|
||||
|
||||
handleTitleChange = title => {
|
||||
this.props.setAttributes( { title } );
|
||||
this.setState( { fieldTitleError: null } );
|
||||
};
|
||||
|
||||
getCurrencyList = SUPPORTED_CURRENCY_LIST.map( value => {
|
||||
const { symbol } = getCurrencyDefaults( value );
|
||||
// if symbol is equal to the code (e.g., 'CHF' === 'CHF'), don't duplicate it.
|
||||
// trim the dot at the end, e.g., 'kr.' becomes 'kr'
|
||||
const label = symbol === value ? value : `${ value } ${ trimEnd( symbol, '.' ) }`;
|
||||
return { value, label };
|
||||
} );
|
||||
|
||||
renderSettings = () => (
|
||||
<InspectorControls>
|
||||
<PanelBody title={ __( 'Settings', 'jetpack-paypal-payments' ) } initialOpen={ false }>
|
||||
<TextControl
|
||||
label={ __( 'Purchase link text', 'jetpack-paypal-payments' ) }
|
||||
help={ __(
|
||||
'Enter the text you want to display on a purchase link used as fallback when the PayPal button cannot be used (e.g. emails, AMP, etc.)',
|
||||
'jetpack-paypal-payments'
|
||||
) }
|
||||
className="jetpack-simple-payments__purchase-link-text"
|
||||
placeholder={ __( 'Click here to purchase', 'jetpack-paypal-payments' ) }
|
||||
onChange={ newPostLinkText =>
|
||||
this.props.setAttributes( { postLinkText: newPostLinkText } )
|
||||
}
|
||||
value={ this.props.attributes.postLinkText }
|
||||
/>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { fieldEmailError, fieldPriceError, fieldTitleError } = this.state;
|
||||
const { attributes, instanceId, isSelected, setAttributes, simplePayment } = this.props;
|
||||
const {
|
||||
content,
|
||||
currency,
|
||||
email,
|
||||
featuredMediaId,
|
||||
featuredMediaUrl,
|
||||
featuredMediaTitle,
|
||||
multiple,
|
||||
price,
|
||||
productId,
|
||||
title,
|
||||
} = attributes;
|
||||
|
||||
/**
|
||||
* The only disabled state that concerns us is when we expect a product but don't have it in
|
||||
* local state.
|
||||
*/
|
||||
const isDisabled = productId && isEmpty( simplePayment );
|
||||
|
||||
if ( ! isSelected && isDisabled ) {
|
||||
return (
|
||||
<div className="simple-payments__loading">
|
||||
<ProductPlaceholder
|
||||
aria-busy="true"
|
||||
content="█████"
|
||||
formattedPrice="█████"
|
||||
title="█████"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
! isSelected &&
|
||||
email &&
|
||||
price &&
|
||||
title &&
|
||||
! fieldEmailError &&
|
||||
! fieldPriceError &&
|
||||
! fieldTitleError
|
||||
) {
|
||||
return (
|
||||
<ProductPlaceholder
|
||||
aria-busy="false"
|
||||
content={ content }
|
||||
featuredMediaUrl={ featuredMediaUrl }
|
||||
featuredMediaTitle={ featuredMediaTitle }
|
||||
formattedPrice={ formatPriceFallback( price, currency ) }
|
||||
multiple={ multiple }
|
||||
title={ title }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = isDisabled ? Disabled : 'div';
|
||||
|
||||
return (
|
||||
<Wrapper className="wp-block-jetpack-simple-payments">
|
||||
{ this.renderSettings() }
|
||||
<FeaturedMedia
|
||||
{ ...{ featuredMediaId, featuredMediaUrl, featuredMediaTitle, setAttributes } }
|
||||
/>
|
||||
<div>
|
||||
<TextControl
|
||||
aria-describedby={ `${ instanceId }-title-error` }
|
||||
className={ clsx( 'simple-payments__field', 'simple-payments__field-title', {
|
||||
'simple-payments__field-has-error': fieldTitleError,
|
||||
} ) }
|
||||
label={ __( 'Item name', 'jetpack-paypal-payments' ) }
|
||||
onChange={ this.handleTitleChange }
|
||||
placeholder={ __( 'Item name', 'jetpack-paypal-payments' ) }
|
||||
required
|
||||
type="text"
|
||||
value={ title }
|
||||
/>
|
||||
<HelpMessage id={ `${ instanceId }-title-error` } isError>
|
||||
{ fieldTitleError }
|
||||
</HelpMessage>
|
||||
|
||||
<TextareaControl
|
||||
className="simple-payments__field simple-payments__field-content"
|
||||
label={ __( 'Describe your item in a few words', 'jetpack-paypal-payments' ) }
|
||||
onChange={ this.handleContentChange }
|
||||
placeholder={ __( 'Describe your item in a few words', 'jetpack-paypal-payments' ) }
|
||||
value={ content }
|
||||
/>
|
||||
|
||||
<div className="simple-payments__price-container">
|
||||
<SelectControl
|
||||
__next40pxDefaultSize
|
||||
className="simple-payments__field simple-payments__field-currency"
|
||||
label={ __( 'Currency', 'jetpack-paypal-payments' ) }
|
||||
onChange={ this.handleCurrencyChange }
|
||||
options={ this.getCurrencyList }
|
||||
value={ currency }
|
||||
/>
|
||||
<TextControl
|
||||
aria-describedby={ `${ instanceId }-price-error` }
|
||||
className={ clsx( 'simple-payments__field', 'simple-payments__field-price', {
|
||||
'simple-payments__field-has-error': fieldPriceError,
|
||||
} ) }
|
||||
label={ __( 'Price', 'jetpack-paypal-payments' ) }
|
||||
onChange={ this.handlePriceChange }
|
||||
placeholder={ formatPriceFallback( 0, currency, false ) }
|
||||
required
|
||||
step="1"
|
||||
type="number"
|
||||
value={ price || '' }
|
||||
/>
|
||||
<HelpMessage id={ `${ instanceId }-price-error` } isError>
|
||||
{ fieldPriceError }
|
||||
</HelpMessage>
|
||||
</div>
|
||||
|
||||
<div className="simple-payments__field-multiple">
|
||||
<ToggleControl
|
||||
checked={ Boolean( multiple ) }
|
||||
label={ __(
|
||||
'Allow people to buy more than one item at a time',
|
||||
'jetpack-paypal-payments'
|
||||
) }
|
||||
onChange={ this.handleMultipleChange }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextControl
|
||||
aria-describedby={ `${ instanceId }-email-${ fieldEmailError ? 'error' : 'help' }` }
|
||||
className={ clsx( 'simple-payments__field', 'simple-payments__field-email', {
|
||||
'simple-payments__field-has-error': fieldEmailError,
|
||||
} ) }
|
||||
label={ __( 'Email', 'jetpack-paypal-payments' ) }
|
||||
onChange={ this.handleEmailChange }
|
||||
placeholder={ __( 'Email', 'jetpack-paypal-payments' ) }
|
||||
required
|
||||
// TODO: switch this back to type="email" once Gutenberg paste handler ignores inputs of type email
|
||||
type="text"
|
||||
value={ email }
|
||||
/>
|
||||
<HelpMessage id={ `${ instanceId }-email-error` } isError>
|
||||
{ fieldEmailError }
|
||||
</HelpMessage>
|
||||
<HelpMessage id={ `${ instanceId }-email-help` }>
|
||||
{ __(
|
||||
'Enter the email address associated with your PayPal account. Don’t have an account?',
|
||||
'jetpack-paypal-payments'
|
||||
) + ' ' }
|
||||
<Link openInNewTab href="https://www.paypal.com/">
|
||||
{ __( 'Create one on PayPal', 'jetpack-paypal-payments' ) }
|
||||
</Link>
|
||||
</HelpMessage>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapSelectToProps = withSelect( ( select, props ) => {
|
||||
const { getEntityRecord } = select( 'core' );
|
||||
const { isSavingPost, getCurrentPost } = select( 'core/editor' );
|
||||
|
||||
const { productId, featuredMediaId } = props.attributes;
|
||||
|
||||
const fields = [
|
||||
[ 'content' ],
|
||||
[ 'meta', 'spay_currency' ],
|
||||
[ 'meta', 'spay_email' ],
|
||||
[ 'meta', 'spay_multiple' ],
|
||||
[ 'meta', 'spay_price' ],
|
||||
[ 'title', 'raw' ],
|
||||
[ 'featured_media' ],
|
||||
];
|
||||
|
||||
const simplePayment = productId
|
||||
? pick( getEntityRecord( 'postType', SIMPLE_PAYMENTS_PRODUCT_POST_TYPE, productId ), fields )
|
||||
: undefined;
|
||||
|
||||
const post = getCurrentPost();
|
||||
|
||||
return {
|
||||
hasPublishAction: !! post?._links?.[ 'wp:action-publish' ],
|
||||
isSaving: !! isSavingPost(),
|
||||
simplePayment,
|
||||
featuredMedia: featuredMediaId
|
||||
? getEntityRecord( 'postType', 'attachment', featuredMediaId )
|
||||
: null,
|
||||
postLinkUrl: post.link,
|
||||
};
|
||||
} );
|
||||
|
||||
export default compose( mapSelectToProps, withInstanceId )( SimplePaymentsEdit );
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { isWpcomPlatformSite } from '@automattic/jetpack-script-data';
|
||||
import { Path, SVG } from '@wordpress/components';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import { __, _x } from '@wordpress/i18n';
|
||||
import { Link } from '@wordpress/ui';
|
||||
import { DEFAULT_CURRENCY } from '../../constants';
|
||||
import edit from './edit';
|
||||
import save from './save';
|
||||
|
||||
export const icon = (
|
||||
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<Path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<Path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z" />
|
||||
</SVG>
|
||||
);
|
||||
|
||||
const supportLink = isWpcomPlatformSite()
|
||||
? 'https://wordpress.com/support/pay-with-paypal/'
|
||||
: 'https://jetpack.com/support/jetpack-blocks/pay-with-paypal/';
|
||||
|
||||
const settings = {
|
||||
title: __( 'Pay with PayPal', 'jetpack-paypal-payments' ),
|
||||
|
||||
description: (
|
||||
<Fragment>
|
||||
<p>
|
||||
{ __(
|
||||
'Lets you add credit and debit card payment buttons with minimal setup.',
|
||||
'jetpack-paypal-payments'
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ __(
|
||||
'Good for collecting donations or payments for products and services.',
|
||||
'jetpack-paypal-payments'
|
||||
) }
|
||||
</p>
|
||||
<Link openInNewTab href={ supportLink }>
|
||||
{ __( 'Support reference', 'jetpack-paypal-payments' ) }
|
||||
</Link>
|
||||
</Fragment>
|
||||
),
|
||||
|
||||
icon: {
|
||||
src: icon,
|
||||
},
|
||||
|
||||
category: 'monetize',
|
||||
|
||||
keywords: [
|
||||
_x( 'buy', 'block search term', 'jetpack-paypal-payments' ),
|
||||
_x( 'commerce', 'block search term', 'jetpack-paypal-payments' ),
|
||||
_x( 'products', 'block search term', 'jetpack-paypal-payments' ),
|
||||
_x( 'purchase', 'block search term', 'jetpack-paypal-payments' ),
|
||||
_x( 'sell', 'block search term', 'jetpack-paypal-payments' ),
|
||||
_x( 'shop', 'block search term', 'jetpack-paypal-payments' ),
|
||||
_x( 'simple', 'block search term', 'jetpack-paypal-payments' ),
|
||||
_x( 'payments', 'block search term', 'jetpack-paypal-payments' ),
|
||||
'PayPal',
|
||||
],
|
||||
|
||||
attributes: {
|
||||
currency: {
|
||||
type: 'string',
|
||||
default: DEFAULT_CURRENCY,
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: '.jetpack-simple-payments-description p',
|
||||
default: '',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
featuredMediaId: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
},
|
||||
featuredMediaUrl: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: '.jetpack-simple-payments-image img',
|
||||
attribute: 'src',
|
||||
default: null,
|
||||
},
|
||||
featuredMediaTitle: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: '.jetpack-simple-payments-image img',
|
||||
attribute: 'alt',
|
||||
default: null,
|
||||
},
|
||||
multiple: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
postLinkUrl: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: '.jetpack-simple-payments-purchase',
|
||||
attribute: 'href',
|
||||
},
|
||||
postLinkText: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: '.jetpack-simple-payments-purchase',
|
||||
default: __( 'Click here to purchase.', 'jetpack-paypal-payments' ),
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
},
|
||||
productId: {
|
||||
type: 'number',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: '.jetpack-simple-payments-title p',
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
transforms: {
|
||||
from: [
|
||||
{
|
||||
type: 'shortcode',
|
||||
tag: 'simple-payment',
|
||||
attributes: {
|
||||
productId: {
|
||||
type: 'number',
|
||||
shortcode: ( { named: { id } } ) => {
|
||||
if ( ! id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = parseInt( id, 10 );
|
||||
if ( result ) {
|
||||
return result;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
edit,
|
||||
|
||||
save,
|
||||
|
||||
supports: {
|
||||
align: false,
|
||||
className: false,
|
||||
customClassName: false,
|
||||
html: false,
|
||||
// Disabled due several problems because the block uses custom post type to store information
|
||||
// https://github.com/Automattic/jetpack/issues/11789
|
||||
reusable: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default settings;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { formatPriceFallback } from '../../utils';
|
||||
|
||||
export default function Save( { attributes } ) {
|
||||
const {
|
||||
content,
|
||||
currency,
|
||||
featuredMediaUrl,
|
||||
featuredMediaTitle,
|
||||
postLinkUrl,
|
||||
postLinkText,
|
||||
price,
|
||||
productId,
|
||||
title,
|
||||
} = attributes;
|
||||
if ( ! productId ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ `jetpack-simple-payments-wrapper jetpack-simple-payments-${ productId }` }>
|
||||
<div className="jetpack-simple-payments-product">
|
||||
{ featuredMediaUrl && (
|
||||
<div className="jetpack-simple-payments-product-image">
|
||||
<div className="jetpack-simple-payments-image">
|
||||
<figure>
|
||||
<img src={ featuredMediaUrl } alt={ featuredMediaTitle } />
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
<div className="jetpack-simple-payments-details">
|
||||
<div className="jetpack-simple-payments-title">
|
||||
<p>{ title }</p>
|
||||
</div>
|
||||
<div className="jetpack-simple-payments-description">
|
||||
<p>{ content }</p>
|
||||
</div>
|
||||
<div className="jetpack-simple-payments-price">
|
||||
<p>{ formatPriceFallback( price, currency ) }</p>
|
||||
</div>
|
||||
<a
|
||||
className="jetpack-simple-payments-purchase"
|
||||
href={ postLinkUrl }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ postLinkText }
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* PayPal Payments package.
|
||||
*
|
||||
* @package automattic/jetpack-paypal-payments
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack;
|
||||
|
||||
/**
|
||||
* Class PayPal_Payments.
|
||||
*/
|
||||
class PayPal_Payments {
|
||||
|
||||
const PACKAGE_VERSION = '0.7.6';
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* Read-only REST controller for jp_pay_order.
|
||||
*
|
||||
* Orders should only be created through the internal payment processing flow,
|
||||
* not directly via the REST API.
|
||||
*
|
||||
* @package automattic/jetpack-paypal-payments
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Paypal_Payments;
|
||||
|
||||
use WP_Error;
|
||||
use WP_REST_Posts_Controller;
|
||||
|
||||
/**
|
||||
* Extends WP_REST_Posts_Controller to disable create, update, and delete operations.
|
||||
*/
|
||||
class Order_REST_Controller extends WP_REST_Posts_Controller {
|
||||
|
||||
/**
|
||||
* Deny order creation via the REST API.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function create_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return new WP_Error(
|
||||
'rest_cannot_create',
|
||||
__( 'Orders can only be created through the payment processing flow.', 'jetpack-paypal-payments' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny order updates via the REST API.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function update_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return new WP_Error(
|
||||
'rest_cannot_update',
|
||||
__( 'Orders cannot be modified via the REST API.', 'jetpack-paypal-payments' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny order deletion via the REST API.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function delete_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return new WP_Error(
|
||||
'rest_cannot_delete',
|
||||
__( 'Orders cannot be deleted via the REST API.', 'jetpack-paypal-payments' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* PayPal_Payments_Currencies: Utils for displaying and managing currencies.
|
||||
*
|
||||
* @package automattic/jetpack-paypal-payments
|
||||
*/
|
||||
|
||||
/**
|
||||
* General currencies specific functionality
|
||||
*/
|
||||
class PayPal_Payments_Currencies {
|
||||
/**
|
||||
* Currencies definition
|
||||
*/
|
||||
const CURRENCIES = array(
|
||||
'USD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'GBP' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '£',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'JPY' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '¥',
|
||||
'decimal' => 0,
|
||||
),
|
||||
'BRL' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'R$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'EUR' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '€',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'NZD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'NZ$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'AUD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'A$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'CAD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'C$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'ILS' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '₪',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'RUB' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '₽',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'MXN' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'MX$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'MYR' => array(
|
||||
'format' => '%2$s%1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'RM',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'SEK' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Skr',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'HUF' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Ft',
|
||||
'decimal' => 0, // Decimals are supported by Stripe but not by PayPal.
|
||||
),
|
||||
'CHF' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'CHF',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'CZK' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Kč',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'DKK' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Dkr',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'HKD' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'HK$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'NOK' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'Kr',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'PHP' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '₱',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'PLN' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'PLN',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'SGD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'S$',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'TWD' => array(
|
||||
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
|
||||
'symbol' => 'NT$',
|
||||
'decimal' => 0, // Decimals are supported by Stripe but not by PayPal.
|
||||
),
|
||||
'THB' => array(
|
||||
'format' => '%2$s%1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '฿',
|
||||
'decimal' => 2,
|
||||
),
|
||||
'INR' => array(
|
||||
'format' => '%2$s %1$s', // 1: Symbol 2: currency value
|
||||
'symbol' => '₹',
|
||||
'decimal' => 0,
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Format a price with currency.
|
||||
*
|
||||
* Uses currency-aware formatting to output a formatted price with a simple fallback.
|
||||
*
|
||||
* Largely inspired by WordPress.com's Store_Price::display_currency
|
||||
*
|
||||
* @param string $price Price.
|
||||
* @param string $currency Currency.
|
||||
* @param bool $symbol Whether to display the currency symbol.
|
||||
* @return string Formatted price.
|
||||
*/
|
||||
public static function format_price( $price, $currency, $symbol = true ) {
|
||||
$price = floatval( $price );
|
||||
|
||||
// Try to parse using NumberFormatter if available
|
||||
if ( class_exists( 'NumberFormatter' ) ) {
|
||||
$formatter = new NumberFormatter( get_locale(), NumberFormatter::DECIMAL );
|
||||
$parsed = $formatter->parse( (string) $price );
|
||||
if ( false !== $parsed ) {
|
||||
$price = (float) $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to unspecified currency symbol like `¤1,234.05`.
|
||||
// @link https://en.wikipedia.org/wiki/Currency_sign_(typography).
|
||||
if ( ! array_key_exists( $currency, self::CURRENCIES ) ) {
|
||||
return ( $symbol ? '¤' : '' ) . number_format_i18n( $price, 2 );
|
||||
}
|
||||
|
||||
$currency_details = self::CURRENCIES[ $currency ];
|
||||
|
||||
// Ensure USD displays as 1234.56 even in non-US locales.
|
||||
$amount = 'USD' === $currency
|
||||
? number_format( $price, $currency_details['decimal'], '.', ',' )
|
||||
: number_format_i18n( $price, $currency_details['decimal'] );
|
||||
|
||||
return sprintf(
|
||||
$currency_details['format'],
|
||||
$symbol ? $currency_details['symbol'] : '',
|
||||
$amount
|
||||
);
|
||||
}
|
||||
}
|
||||
+800
@@ -0,0 +1,800 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple Payments lets users embed a PayPal button fully integrated with wpcom to sell products on the site.
|
||||
* This is not a proper module yet, because not all the pieces are in place. Until everything is shipped, it can be turned
|
||||
* into module that can be enabled/disabled.
|
||||
*
|
||||
* @package automattic/jetpack-paypal-payments
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Paypal_Payments;
|
||||
|
||||
use Automattic\Jetpack\Assets;
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
|
||||
use Automattic\Jetpack\PayPal_Payments;
|
||||
use PayPal_Payments_Currencies;
|
||||
use WP_Post;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple_Payments
|
||||
*/
|
||||
class Simple_Payments {
|
||||
|
||||
const PACKAGE_VERSION = PayPal_Payments::PACKAGE_VERSION;
|
||||
|
||||
/**
|
||||
* Post type order.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $post_type_order = 'jp_pay_order';
|
||||
|
||||
/**
|
||||
* Post type product.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $post_type_product = 'jp_pay_product';
|
||||
|
||||
/**
|
||||
* Define simple payment shortcode.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $shortcode = 'simple-payment';
|
||||
|
||||
/**
|
||||
* Define simple payment CSS prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $css_classname_prefix = 'jetpack-simple-payments';
|
||||
|
||||
/**
|
||||
* Which plan the user is on.
|
||||
*
|
||||
* @var string value_bundle or jetpack_premium
|
||||
*/
|
||||
public static $required_plan;
|
||||
|
||||
/**
|
||||
* Instance of the class.
|
||||
*
|
||||
* @var Simple_Payments
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Construction function.
|
||||
*/
|
||||
private function __construct() {}
|
||||
|
||||
/**
|
||||
* Create instance of class.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
self::$instance->register_init_hooks();
|
||||
self::$required_plan = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ? 'value_bundle' : 'jetpack_premium';
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register scripts and styles.
|
||||
*/
|
||||
private function register_scripts_and_styles() {
|
||||
/**
|
||||
* Paypal heavily discourages putting that script in your own server:
|
||||
*
|
||||
* @see https://developer.paypal.com/docs/integration/direct/express-checkout/integration-jsv4/add-paypal-button/
|
||||
*/
|
||||
wp_register_script(
|
||||
'paypal-checkout-js',
|
||||
'https://www.paypalobjects.com/api/checkout.js',
|
||||
array(),
|
||||
null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
||||
true
|
||||
);
|
||||
Assets::register_script(
|
||||
'jetpack-paypal-express-checkout',
|
||||
'./paypal-express-checkout.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'dependencies' => array(
|
||||
'jquery',
|
||||
'paypal-checkout-js',
|
||||
),
|
||||
)
|
||||
);
|
||||
wp_register_style(
|
||||
'jetpack-simple-payments',
|
||||
plugin_dir_url( __FILE__ ) . '/../../../dist/legacy-simple-payments.css',
|
||||
array( 'dashicons' ),
|
||||
self::PACKAGE_VERSION,
|
||||
false /* @phan-suppress-current-line PhanTypeMismatchArgument */
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register init hooks.
|
||||
*/
|
||||
private function register_init_hooks() {
|
||||
add_action( 'init', array( $this, 'init_hook_action' ) );
|
||||
add_action( 'rest_api_init', array( $this, 'register_meta_fields_in_rest_api' ) );
|
||||
add_filter( 'rest_prepare_' . self::$post_type_product, array( $this, 'redact_spay_email_for_unauthorized' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the shortcode.
|
||||
*/
|
||||
private function register_shortcode() {
|
||||
add_shortcode( self::$shortcode, array( $this, 'parse_shortcode' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions that are run on init.
|
||||
*/
|
||||
public function init_hook_action() {
|
||||
add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
|
||||
add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
|
||||
if ( ! is_admin() ) {
|
||||
$this->register_scripts_and_styles();
|
||||
}
|
||||
$this->register_shortcode();
|
||||
$this->setup_cpts();
|
||||
|
||||
add_filter( 'the_content', array( $this, 'remove_auto_paragraph_from_product_description' ), 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the static assets needed in the frontend.
|
||||
*/
|
||||
public function enqueue_frontend_assets() {
|
||||
if ( ! wp_style_is( 'jetpack-simple-payments', 'enqueued' ) ) {
|
||||
wp_enqueue_style( 'jetpack-simple-payments' );
|
||||
}
|
||||
|
||||
if ( ! wp_script_is( 'jetpack-paypal-express-checkout', 'enqueued' ) ) {
|
||||
wp_enqueue_script( 'jetpack-paypal-express-checkout' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an inline script for setting up the PayPal checkout button.
|
||||
*
|
||||
* @param string $id Product ID.
|
||||
* @param string $dom_id ID of the DOM element with the purchase message.
|
||||
* @param boolean $is_multiple Whether multiple items of the same product can be purchased.
|
||||
*/
|
||||
public function setup_paypal_checkout_button( $id, $dom_id, $is_multiple ) {
|
||||
wp_add_inline_script(
|
||||
'jetpack-paypal-express-checkout',
|
||||
sprintf(
|
||||
"try{PaypalExpressCheckout.renderButton( '%d', '%d', %s, '%d' );}catch(e){}",
|
||||
intval( $this->get_blog_id() ),
|
||||
intval( $id ),
|
||||
wp_json_encode( $dom_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ),
|
||||
intval( $is_multiple )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove auto paragraph from product description.
|
||||
*
|
||||
* @param string $content - the content of the post.
|
||||
*/
|
||||
public function remove_auto_paragraph_from_product_description( $content ) {
|
||||
if ( get_post_type() === self::$post_type_product ) {
|
||||
remove_filter( 'the_content', 'wpautop' );
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/** Return the blog ID */
|
||||
public function get_blog_id() {
|
||||
return ( new Manager() )->get_site_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check whether Simple Payments are enabled for given site.
|
||||
*
|
||||
* @return bool True if Simple Payments are enabled, false otherwise.
|
||||
*/
|
||||
public static function is_enabled_jetpack_simple_payments() {
|
||||
/**
|
||||
* Can be used by plugin authors to disable the conflicting output of Simple Payments.
|
||||
*
|
||||
* @since 6.3.0
|
||||
*
|
||||
* @param bool True if Simple Payments should be disabled, false otherwise.
|
||||
*/
|
||||
if ( apply_filters( 'jetpack_disable_simple_payments', false ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ( ( defined( 'IS_WPCOM' ) && IS_WPCOM )
|
||||
|| ( new Manager() )->is_connected()
|
||||
&& Jetpack_Plan::supports( 'simple-payments' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a WP_Post representation of a product
|
||||
*
|
||||
* @param int $id The ID of the product.
|
||||
*
|
||||
* @return array|false|WP_Post
|
||||
*/
|
||||
private function get_product( $id ) {
|
||||
if ( ! $id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$product = get_post( $id );
|
||||
if ( ! $product || is_wp_error( $product ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( $product->post_type !== self::$post_type_product || 'publish' !== $product->post_status ) {
|
||||
return false;
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the content from a shortcode
|
||||
*
|
||||
* @param array $attrs Shortcode attributes.
|
||||
* @param mixed $content unused.
|
||||
*
|
||||
* @return string|void
|
||||
*/
|
||||
public function parse_shortcode( $attrs, $content = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( empty( $attrs['id'] ) ) {
|
||||
return;
|
||||
}
|
||||
$product = $this->get_product( $attrs['id'] );
|
||||
if ( ! $product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We allow for overriding the presentation labels.
|
||||
$data = shortcode_atts(
|
||||
array(
|
||||
'blog_id' => $this->get_blog_id(),
|
||||
'dom_id' => uniqid( self::$css_classname_prefix . '-' . $product->ID . '_', true ),
|
||||
'class' => self::$css_classname_prefix . '-' . $product->ID,
|
||||
'title' => get_the_title( $product ),
|
||||
'description' => $product->post_content,
|
||||
'cta' => get_post_meta( $product->ID, 'spay_cta', true ),
|
||||
'multiple' => get_post_meta( $product->ID, 'spay_multiple', true ) || '0',
|
||||
),
|
||||
$attrs
|
||||
);
|
||||
|
||||
$data['price'] = $this->format_price(
|
||||
get_post_meta( $product->ID, 'spay_price', true ),
|
||||
get_post_meta( $product->ID, 'spay_currency', true )
|
||||
);
|
||||
|
||||
$data['id'] = $attrs['id'];
|
||||
|
||||
if ( ! self::is_enabled_jetpack_simple_payments() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_frontend_assets();
|
||||
$this->setup_paypal_checkout_button( $attrs['id'], $data['dom_id'], $data['multiple'] );
|
||||
|
||||
return $this->output_shortcode( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML output to use as PayPal purchase box.
|
||||
*
|
||||
* @param string $dom_id ID of the DOM element with the purchase message.
|
||||
* @param boolean $is_multiple Whether multiple items of the same product can be purchased.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function output_purchase_box( $dom_id, $is_multiple ) {
|
||||
$items = '';
|
||||
$css_prefix = self::$css_classname_prefix;
|
||||
|
||||
if ( $is_multiple ) {
|
||||
$items = sprintf(
|
||||
'
|
||||
<div class="%1$s">
|
||||
<input class="%2$s" type="number" value="1" min="1" id="%3$s" />
|
||||
</div>
|
||||
',
|
||||
esc_attr( "{$css_prefix}-items" ),
|
||||
esc_attr( "{$css_prefix}-items-number" ),
|
||||
esc_attr( "{$dom_id}_number" )
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<div class="%1$s" id="%2$s"></div><div class="%3$s">%4$s<div class="%5$s" id="%6$s"></div></div>',
|
||||
esc_attr( "{$css_prefix}-purchase-message" ),
|
||||
esc_attr( "{$dom_id}-message-container" ),
|
||||
esc_attr( "{$css_prefix}-purchase-box" ),
|
||||
$items,
|
||||
esc_attr( "{$css_prefix}-button" ),
|
||||
esc_attr( "{$dom_id}_button" )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML output to replace the `simple-payments` shortcode.
|
||||
*
|
||||
* @param array $data Product data.
|
||||
* @return string
|
||||
*/
|
||||
public function output_shortcode( $data ) {
|
||||
$css_prefix = self::$css_classname_prefix;
|
||||
|
||||
$image = '';
|
||||
if ( has_post_thumbnail( $data['id'] ) ) {
|
||||
$image = sprintf(
|
||||
'<div class="%1$s"><div class="%2$s">%3$s</div></div>',
|
||||
esc_attr( "{$css_prefix}-product-image" ),
|
||||
esc_attr( "{$css_prefix}-image" ),
|
||||
get_the_post_thumbnail( $data['id'], 'full' )
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'
|
||||
<div class="%1$s">
|
||||
<div class="%2$s">
|
||||
%3$s
|
||||
<div class="%4$s">
|
||||
<div class="%5$s"><p>%6$s</p></div>
|
||||
<div class="%7$s"><p>%8$s</p></div>
|
||||
<div class="%9$s"><p>%10$s</p></div>
|
||||
%11$s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
',
|
||||
esc_attr( "{$data['class']} {$css_prefix}-wrapper" ),
|
||||
esc_attr( "{$css_prefix}-product" ),
|
||||
$image,
|
||||
esc_attr( "{$css_prefix}-details" ),
|
||||
esc_attr( "{$css_prefix}-title" ),
|
||||
esc_html( $data['title'] ),
|
||||
esc_attr( "{$css_prefix}-description" ),
|
||||
wp_kses( $data['description'], wp_kses_allowed_html( 'post' ) ),
|
||||
esc_attr( "{$css_prefix}-price" ),
|
||||
esc_html( $data['price'] ),
|
||||
$this->output_purchase_box( $data['dom_id'], $data['multiple'] )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a price with currency
|
||||
*
|
||||
* Uses currency-aware formatting to output a formatted price with a simple fallback.
|
||||
*
|
||||
* Largely inspired by WordPress.com's Store_Price::display_currency
|
||||
*
|
||||
* @param string $price Price.
|
||||
* @param string $currency Currency.
|
||||
* @return string Formatted price.
|
||||
*/
|
||||
private function format_price( $price, $currency ) {
|
||||
return PayPal_Payments_Currencies::format_price( $price, $currency );
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows custom post types to be used by REST API.
|
||||
*
|
||||
* @param array $post_types - the allows post types.
|
||||
* @see hook 'rest_api_allowed_post_types'
|
||||
* @return array
|
||||
*/
|
||||
public function allow_rest_api_types( $post_types ) {
|
||||
$post_types[] = self::$post_type_order;
|
||||
$post_types[] = self::$post_type_product;
|
||||
return $post_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge $post_meta with additional meta information.
|
||||
*
|
||||
* @param array $post_meta - the post's meta information.
|
||||
*/
|
||||
public function allow_sync_post_meta( $post_meta ) {
|
||||
return array_merge(
|
||||
$post_meta,
|
||||
array(
|
||||
'spay_paypal_id',
|
||||
'spay_status',
|
||||
'spay_product_id',
|
||||
'spay_quantity',
|
||||
'spay_price',
|
||||
'spay_customer_email',
|
||||
'spay_currency',
|
||||
'spay_cta',
|
||||
'spay_email',
|
||||
'spay_multiple',
|
||||
'spay_formatted_price',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable Simple payments custom meta values for access through the REST API.
|
||||
* Field's value will be exposed on a .meta key in the endpoint response,
|
||||
* and WordPress will handle setting up the callbacks for reading and writing
|
||||
* to that meta key.
|
||||
*
|
||||
* @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/
|
||||
*/
|
||||
public function register_meta_fields_in_rest_api() {
|
||||
register_meta(
|
||||
'post',
|
||||
'spay_price',
|
||||
array(
|
||||
'description' => esc_html__( 'Simple payments; price.', 'jetpack-paypal-payments' ),
|
||||
'object_subtype' => self::$post_type_product,
|
||||
'sanitize_callback' => array( $this, 'sanitize_price' ),
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'number',
|
||||
)
|
||||
);
|
||||
|
||||
register_meta(
|
||||
'post',
|
||||
'spay_currency',
|
||||
array(
|
||||
'description' => esc_html__( 'Simple payments; currency code.', 'jetpack-paypal-payments' ),
|
||||
'object_subtype' => self::$post_type_product,
|
||||
'sanitize_callback' => array( $this, 'sanitize_currency' ),
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
)
|
||||
);
|
||||
|
||||
register_meta(
|
||||
'post',
|
||||
'spay_cta',
|
||||
array(
|
||||
'description' => esc_html__( 'Simple payments; text with "Buy" or other CTA', 'jetpack-paypal-payments' ),
|
||||
'object_subtype' => self::$post_type_product,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
)
|
||||
);
|
||||
|
||||
register_meta(
|
||||
'post',
|
||||
'spay_multiple',
|
||||
array(
|
||||
'description' => esc_html__( 'Simple payments; allow multiple items', 'jetpack-paypal-payments' ),
|
||||
'object_subtype' => self::$post_type_product,
|
||||
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'boolean',
|
||||
)
|
||||
);
|
||||
|
||||
register_meta(
|
||||
'post',
|
||||
'spay_email',
|
||||
array(
|
||||
'description' => esc_html__( 'Simple payments button; paypal email.', 'jetpack-paypal-payments' ),
|
||||
'object_subtype' => self::$post_type_product,
|
||||
'sanitize_callback' => 'sanitize_email',
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
)
|
||||
);
|
||||
|
||||
register_meta(
|
||||
'post',
|
||||
'spay_status',
|
||||
array(
|
||||
'description' => esc_html__( 'Simple payments; status.', 'jetpack-paypal-payments' ),
|
||||
'object_subtype' => self::$post_type_product,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the seller's PayPal email (`spay_email`) from REST responses when the
|
||||
* requester cannot edit the product. The meta stays `show_in_rest => true` so
|
||||
* the block editor's read/write round-trip keeps working — but unauthenticated
|
||||
* or read-only callers no longer see the address in collection or single-item
|
||||
* responses for the `jp_pay_product` post type.
|
||||
*
|
||||
* @param mixed $response The response object (expected: \WP_REST_Response).
|
||||
* @param mixed $post The product post (expected: \WP_Post).
|
||||
* @return mixed
|
||||
*/
|
||||
public function redact_spay_email_for_unauthorized( $response, $post ) {
|
||||
if ( ! $response instanceof \WP_REST_Response || ! $post instanceof WP_Post ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ( current_user_can( 'edit_post', $post->ID ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = $response->get_data();
|
||||
if ( isset( $data['meta']['spay_email'] ) ) {
|
||||
unset( $data['meta']['spay_email'] );
|
||||
$response->set_data( $data );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize three-character ISO-4217 Simple payments currency
|
||||
*
|
||||
* List has to be in sync with list at the block's client side and widget's backend side:
|
||||
*
|
||||
* @param array $currency - list of currencies.
|
||||
* @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/extensions/blocks/simple-payments/constants.js#L9-L39
|
||||
* @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/modules/widgets/simple-payments.php#L19-L44
|
||||
*
|
||||
* Currencies should be supported by PayPal:
|
||||
* @link https://developer.paypal.com/docs/api/reference/currency-codes/
|
||||
*
|
||||
* Indian Rupee (INR) not supported because at the time of the creation of this file
|
||||
* because it's limited to in-country PayPal India accounts only.
|
||||
* Discussion: https://github.com/Automattic/wp-calypso/pull/28236
|
||||
*/
|
||||
public static function sanitize_currency( $currency ) {
|
||||
$valid_currencies = array(
|
||||
'USD',
|
||||
'EUR',
|
||||
'AUD',
|
||||
'BRL',
|
||||
'CAD',
|
||||
'CZK',
|
||||
'DKK',
|
||||
'HKD',
|
||||
'HUF',
|
||||
'ILS',
|
||||
'JPY',
|
||||
'MYR',
|
||||
'MXN',
|
||||
'TWD',
|
||||
'NZD',
|
||||
'NOK',
|
||||
'PHP',
|
||||
'PLN',
|
||||
'GBP',
|
||||
'RUB',
|
||||
'SGD',
|
||||
'SEK',
|
||||
'CHF',
|
||||
'THB',
|
||||
);
|
||||
|
||||
return in_array( $currency, $valid_currencies, true ) ? $currency : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize price:
|
||||
*
|
||||
* Positive integers and floats
|
||||
* Supports two decimal places.
|
||||
* Maximum length: 10.
|
||||
*
|
||||
* See `price` from PayPal docs:
|
||||
*
|
||||
* @link https://developer.paypal.com/docs/api/orders/v1/#definition-item
|
||||
*
|
||||
* @param string $price - the price we want to sanitize.
|
||||
* @return null|string
|
||||
*/
|
||||
public static function sanitize_price( $price ) {
|
||||
return preg_match( '/^[0-9]{0,10}(\.[0-9]{0,2})?$/', $price ) ? $price : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the custom post types for the module.
|
||||
*/
|
||||
public function setup_cpts() {
|
||||
/*
|
||||
* ORDER data structure. holds:
|
||||
* title = customer_name | 4xproduct_name
|
||||
* excerpt = customer_name + customer contact info + customer notes from paypal form
|
||||
* metadata:
|
||||
* spay_paypal_id - paypal id of transaction
|
||||
* spay_status
|
||||
* spay_product_id - post_id of bought product
|
||||
* spay_quantity - quantity of product
|
||||
* spay_price - item price at the time of purchase
|
||||
* spay_customer_email - customer email
|
||||
* ... (WIP)
|
||||
*/
|
||||
$order_capabilities = array(
|
||||
'edit_post' => 'edit_posts',
|
||||
'read_post' => 'read_private_posts',
|
||||
'delete_post' => 'delete_posts',
|
||||
'edit_posts' => 'edit_posts',
|
||||
'edit_others_posts' => 'edit_others_posts',
|
||||
'publish_posts' => 'publish_posts',
|
||||
'read_private_posts' => 'read_private_posts',
|
||||
);
|
||||
$order_args = array(
|
||||
'label' => esc_html_x( 'Order', 'noun: a quantity of goods or items purchased or sold', 'jetpack-paypal-payments' ),
|
||||
'description' => esc_html__( 'Simple Payments orders', 'jetpack-paypal-payments' ),
|
||||
'supports' => array( 'custom-fields', 'excerpt' ),
|
||||
'hierarchical' => false,
|
||||
'public' => false,
|
||||
'show_ui' => false,
|
||||
'show_in_menu' => false,
|
||||
'show_in_admin_bar' => false,
|
||||
'show_in_nav_menus' => false,
|
||||
'can_export' => true,
|
||||
'has_archive' => false,
|
||||
'exclude_from_search' => true,
|
||||
'publicly_queryable' => false,
|
||||
'rewrite' => false,
|
||||
'capabilities' => $order_capabilities,
|
||||
'show_in_rest' => true,
|
||||
'rest_controller_class' => Order_REST_Controller::class,
|
||||
);
|
||||
register_post_type( self::$post_type_order, $order_args );
|
||||
|
||||
/*
|
||||
* PRODUCT data structure. Holds:
|
||||
* title - title
|
||||
* content - description
|
||||
* thumbnail - image
|
||||
* metadata:
|
||||
* spay_price - price
|
||||
* spay_formatted_price
|
||||
* spay_currency - currency code
|
||||
* spay_cta - text with "Buy" or other CTA
|
||||
* spay_email - paypal email
|
||||
* spay_multiple - allow for multiple items
|
||||
* spay_status - status. { enabled | disabled }
|
||||
*/
|
||||
$product_capabilities = array(
|
||||
'edit_post' => 'edit_posts',
|
||||
'read_post' => 'read_private_posts',
|
||||
'delete_post' => 'delete_posts',
|
||||
'edit_posts' => 'publish_posts',
|
||||
'edit_others_posts' => 'edit_others_posts',
|
||||
'publish_posts' => 'publish_posts',
|
||||
'read_private_posts' => 'read_private_posts',
|
||||
);
|
||||
$product_args = array(
|
||||
'label' => esc_html__( 'Product', 'jetpack-paypal-payments' ),
|
||||
'description' => esc_html__( 'Simple Payments products', 'jetpack-paypal-payments' ),
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields', 'author' ),
|
||||
'hierarchical' => false,
|
||||
'public' => false,
|
||||
'show_ui' => false,
|
||||
'show_in_menu' => false,
|
||||
'show_in_admin_bar' => false,
|
||||
'show_in_nav_menus' => false,
|
||||
'can_export' => true,
|
||||
'has_archive' => false,
|
||||
'exclude_from_search' => true,
|
||||
'publicly_queryable' => false,
|
||||
'rewrite' => false,
|
||||
'capabilities' => $product_capabilities,
|
||||
'show_in_rest' => true,
|
||||
);
|
||||
register_post_type( self::$post_type_product, $product_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the block attributes
|
||||
*
|
||||
* @param array $attrs The block attributes, expected to contain:
|
||||
* * email - an email address.
|
||||
* * price - a float between 0.01 and 9999999999.99.
|
||||
* * productId - the ID of the product being paid for.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_valid( $attrs ) {
|
||||
if ( ! $this->validate_paypal_email( $attrs ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->validate_price( $attrs ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->validate_product( $attrs ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the email address to make a payment to is valid
|
||||
*
|
||||
* @param array $attrs Key-value array of attributes.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function validate_paypal_email( $attrs ) {
|
||||
if ( empty( $attrs['email'] ) ) {
|
||||
return false;
|
||||
}
|
||||
return (bool) filter_var( $attrs['email'], FILTER_VALIDATE_EMAIL );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the price is valid
|
||||
*
|
||||
* @param array $attrs Key-value array of attributes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function validate_price( $attrs ) {
|
||||
if ( empty( $attrs['price'] ) ) {
|
||||
return false;
|
||||
}
|
||||
return (bool) self::sanitize_price( $attrs['price'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the stored product is valid
|
||||
*
|
||||
* Valid means it has a title, and the currency is accepted.
|
||||
*
|
||||
* @param array $attrs Key-value array of attributes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function validate_product( $attrs ) {
|
||||
if ( empty( $attrs['productId'] ) ) {
|
||||
return false;
|
||||
}
|
||||
$product = $this->get_product( $attrs['productId'] );
|
||||
if ( ! $product ) {
|
||||
return false;
|
||||
}
|
||||
// This title is the one used by paypal, it's set from the title set in the block content, unless the block
|
||||
// content title is blank.
|
||||
if ( ! get_the_title( $product ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currency = get_post_meta( $product->ID, 'spay_currency', true );
|
||||
return (bool) self::sanitize_currency( $currency );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Simple_Payments_Widget widget.
|
||||
*/
|
||||
public static function register_widget_simple_payments() {
|
||||
if ( ! self::is_enabled_jetpack_simple_payments() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
register_widget( 'Automattic\Jetpack\Paypal_Payments\Widgets\Simple_Payments_Widget' );
|
||||
}
|
||||
}
|
||||
Simple_Payments::get_instance();
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* This PaypalExpressCheckout global is included by wp_enqueue_script( 'jetpack-paypal-express-checkout' );
|
||||
* It handles communication with Paypal Express checkout and public-api.wordpress.com for the purposes
|
||||
* of simple-payments module.
|
||||
*/
|
||||
|
||||
/* global paypal */
|
||||
/* exported PaypalExpressCheckout */
|
||||
const PaypalExpressCheckout = {
|
||||
primaryCssClassName: 'jetpack-simple-payments',
|
||||
messageCssClassName: 'jetpack-simple-payments-purchase-message',
|
||||
|
||||
wpRestAPIHost: 'https://public-api.wordpress.com',
|
||||
wpRestAPIVersion: '/wpcom/v2',
|
||||
|
||||
getEnvironment: function () {
|
||||
if (
|
||||
localStorage &&
|
||||
localStorage.getItem &&
|
||||
localStorage.getItem( 'simple-payments-env' ) === 'sandbox'
|
||||
) {
|
||||
return 'sandbox';
|
||||
}
|
||||
return 'production';
|
||||
},
|
||||
|
||||
getCreatePaymentEndpoint: function ( blogId ) {
|
||||
return (
|
||||
PaypalExpressCheckout.wpRestAPIHost +
|
||||
PaypalExpressCheckout.wpRestAPIVersion +
|
||||
'/sites/' +
|
||||
blogId +
|
||||
'/simple-payments/paypal/payment'
|
||||
);
|
||||
},
|
||||
|
||||
getExecutePaymentEndpoint: function ( blogId, paymentId ) {
|
||||
return (
|
||||
PaypalExpressCheckout.wpRestAPIHost +
|
||||
PaypalExpressCheckout.wpRestAPIVersion +
|
||||
'/sites/' +
|
||||
blogId +
|
||||
'/simple-payments/paypal/' +
|
||||
paymentId +
|
||||
'/execute'
|
||||
);
|
||||
},
|
||||
|
||||
getNumberOfItems: function ( field, enableMultiple ) {
|
||||
if ( enableMultiple !== '1' ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const numberField = document.getElementById( field );
|
||||
|
||||
if ( ! numberField ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const number = Number( numberField.value );
|
||||
|
||||
if ( isNaN( number ) ) {
|
||||
return 1;
|
||||
}
|
||||
return number;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the DOM element-placeholder used to show message
|
||||
* about the transaction. If it doesn't exist then the function will create a new one.
|
||||
*
|
||||
* @param {string} domId - domId id of the payment button placeholder
|
||||
* @return {Element} the dom element to print the message
|
||||
*/
|
||||
getMessageContainer: function ( domId ) {
|
||||
return document.getElementById( domId + '-message-container' );
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a messange close to the Paypal button.
|
||||
* Use this function to give feedback to the user according
|
||||
* to the transaction result.
|
||||
*
|
||||
* @param {string} message - message to show
|
||||
* @param {string} domId - paypal-button element dom identifier
|
||||
* @param {boolean} [isError] - defines if it's a message error. Not TRUE as default.
|
||||
*/
|
||||
showMessage: function ( message, domId, isError ) {
|
||||
const domEl = PaypalExpressCheckout.getMessageContainer( domId );
|
||||
|
||||
// set css classes
|
||||
let cssClasses = PaypalExpressCheckout.messageCssClassName + ' show ';
|
||||
cssClasses += isError ? 'error' : 'success';
|
||||
|
||||
// show message 1s after PayPal popup is closed
|
||||
setTimeout( function () {
|
||||
domEl.innerHTML = message;
|
||||
domEl.setAttribute( 'class', cssClasses );
|
||||
}, 1000 );
|
||||
},
|
||||
|
||||
showError: function ( message, domId ) {
|
||||
PaypalExpressCheckout.showMessage( message, domId, true );
|
||||
},
|
||||
|
||||
processErrorMessage: function ( errorResponse ) {
|
||||
const error = errorResponse ? errorResponse.responseJSON : null;
|
||||
const defaultMessage = 'There was an issue processing your payment.';
|
||||
|
||||
if ( ! error ) {
|
||||
return '<p>' + defaultMessage + '</p>';
|
||||
}
|
||||
|
||||
if ( error.additional_errors ) {
|
||||
const messages = [];
|
||||
error.additional_errors.forEach( function ( additionalError ) {
|
||||
if ( additionalError.message ) {
|
||||
messages.push( '<p>' + additionalError.message.toString() + '</p>' );
|
||||
}
|
||||
} );
|
||||
return messages.join( '' );
|
||||
}
|
||||
|
||||
return '<p>' + ( error.message || defaultMessage ) + '</p>';
|
||||
},
|
||||
|
||||
processSuccessMessage: function ( successResponse ) {
|
||||
const message = successResponse.message;
|
||||
const defaultMessage = 'Thank you. Your purchase was successful!';
|
||||
|
||||
if ( ! message ) {
|
||||
return '<p>' + defaultMessage + '</p>';
|
||||
}
|
||||
|
||||
return '<p>' + message + '</p>';
|
||||
},
|
||||
|
||||
cleanAndHideMessage: function ( domId ) {
|
||||
const domEl = PaypalExpressCheckout.getMessageContainer( domId );
|
||||
domEl.setAttribute( 'class', PaypalExpressCheckout.messageCssClassName );
|
||||
domEl.innerHTML = '';
|
||||
},
|
||||
|
||||
renderButton: function ( blogId, buttonId, domId, enableMultiple ) {
|
||||
const env = PaypalExpressCheckout.getEnvironment();
|
||||
|
||||
if ( ! paypal ) {
|
||||
throw new Error( 'PayPal module is required by PaypalExpressCheckout' );
|
||||
}
|
||||
|
||||
const buttonDomId = domId + '_button';
|
||||
|
||||
paypal.Button.render(
|
||||
{
|
||||
env: env,
|
||||
commit: true,
|
||||
|
||||
style: {
|
||||
label: 'pay',
|
||||
shape: 'rect',
|
||||
color: 'silver',
|
||||
size: 'responsive',
|
||||
fundingicons: true,
|
||||
},
|
||||
|
||||
payment: function () {
|
||||
PaypalExpressCheckout.cleanAndHideMessage( domId );
|
||||
|
||||
const payload = {
|
||||
number: PaypalExpressCheckout.getNumberOfItems( domId + '_number', enableMultiple ),
|
||||
buttonId: buttonId,
|
||||
env: env,
|
||||
};
|
||||
|
||||
return new paypal.Promise( function ( resolve, reject ) {
|
||||
// eslint-disable-next-line no-undef
|
||||
jQuery
|
||||
.post( PaypalExpressCheckout.getCreatePaymentEndpoint( blogId ), payload )
|
||||
.done( function ( paymentResponse ) {
|
||||
if ( ! paymentResponse ) {
|
||||
PaypalExpressCheckout.showError(
|
||||
PaypalExpressCheckout.processErrorMessage(),
|
||||
domId
|
||||
);
|
||||
return reject( new Error( 'server_error' ) );
|
||||
}
|
||||
|
||||
resolve( paymentResponse.id );
|
||||
} )
|
||||
.fail( function ( paymentError ) {
|
||||
const paymentErrorMessage =
|
||||
PaypalExpressCheckout.processErrorMessage( paymentError );
|
||||
PaypalExpressCheckout.showError( paymentErrorMessage, domId );
|
||||
|
||||
const code =
|
||||
paymentError.responseJSON && paymentError.responseJSON.code
|
||||
? paymentError.responseJSON.code
|
||||
: 'server_error';
|
||||
|
||||
reject( new Error( code ) );
|
||||
} );
|
||||
} );
|
||||
},
|
||||
|
||||
onAuthorize: function ( onAuthData ) {
|
||||
const payload = {
|
||||
buttonId: buttonId,
|
||||
payerId: onAuthData.payerID,
|
||||
env: env,
|
||||
};
|
||||
return new paypal.Promise( function ( resolve, reject ) {
|
||||
// eslint-disable-next-line no-undef
|
||||
jQuery
|
||||
.post(
|
||||
PaypalExpressCheckout.getExecutePaymentEndpoint( blogId, onAuthData.paymentID ),
|
||||
payload
|
||||
)
|
||||
.done( function ( authResponse ) {
|
||||
if ( ! authResponse ) {
|
||||
PaypalExpressCheckout.showError(
|
||||
PaypalExpressCheckout.processErrorMessage(),
|
||||
domId
|
||||
);
|
||||
return reject( new Error( 'server_error' ) );
|
||||
}
|
||||
|
||||
PaypalExpressCheckout.showMessage(
|
||||
PaypalExpressCheckout.processSuccessMessage( authResponse ),
|
||||
domId
|
||||
);
|
||||
resolve();
|
||||
} )
|
||||
.fail( function ( authError ) {
|
||||
const authErrorMessage = PaypalExpressCheckout.processErrorMessage( authError );
|
||||
PaypalExpressCheckout.showError( authErrorMessage, domId );
|
||||
|
||||
const code =
|
||||
authError.responseJSON && authError.responseJSON.code
|
||||
? authError.responseJSON.code
|
||||
: 'server_error';
|
||||
|
||||
reject( new Error( code ) );
|
||||
} );
|
||||
} );
|
||||
},
|
||||
},
|
||||
buttonDomId
|
||||
);
|
||||
},
|
||||
};
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
.jetpack-simple-payments-wrapper {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
/* Higher specificity in order to reset paragraph style */
|
||||
body .jetpack-simple-payments-wrapper .jetpack-simple-payments-details p {
|
||||
margin: 0 0 1.5em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-product {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-product-image {
|
||||
flex: 0 0 30%;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-image {
|
||||
box-sizing: border-box;
|
||||
min-width: 70px;
|
||||
padding-top: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Higher specificity in order to trump theme's style */
|
||||
body .jetpack-simple-payments-wrapper .jetpack-simple-payments-product-image .jetpack-simple-payments-image img {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
height: auto;
|
||||
left: 50%;
|
||||
margin: 0;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-title p,
|
||||
.jetpack-simple-payments-price p {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-purchase-box {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-button {
|
||||
max-width: 340px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-items {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
input[type="number"].jetpack-simple-payments-items-number {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
max-width: 60px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
input[type="number"].jetpack-simple-payments-items-number::-webkit-inner-spin-button,
|
||||
input[type="number"].jetpack-simple-payments-items-number::-webkit-outer-spin-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-button iframe {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-purchase-message {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
border: 2px solid #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
|
||||
display: none;
|
||||
margin-bottom: 1.5em;
|
||||
min-height: 48px;
|
||||
padding: 1em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-purchase-message::before {
|
||||
font-family: dashicons !important;
|
||||
font-size: 48px !important;
|
||||
line-height: 1 !important;
|
||||
position: absolute;
|
||||
speak: none;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-purchase-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-purchase-message.success::before {
|
||||
color: #4ab866;
|
||||
content: "\f147";
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-purchase-message.error::before {
|
||||
color: #d94f4f;
|
||||
content: "\f335";
|
||||
}
|
||||
|
||||
/* Higher specificity in order to reset */
|
||||
body .jetpack-simple-payments-wrapper .jetpack-simple-payments-purchase-message p {
|
||||
color: #222;
|
||||
margin: 0 0 0.5em;
|
||||
padding: 0 0 0 40px;
|
||||
}
|
||||
|
||||
body .jetpack-simple-payments-wrapper .jetpack-simple-payments-purchase-message p:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-description {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 400px) {
|
||||
|
||||
.jetpack-simple-payments-product {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-product-image + .jetpack-simple-payments-details {
|
||||
flex-basis: 70%;
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.is-email .jetpack-simple-payments-product {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.is-email .jetpack-simple-payments-product-image {
|
||||
display: table-cell;
|
||||
width: 30%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.is-email .jetpack-simple-payments-image {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.is-email .jetpack-simple-payments-image figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.jetpack-simple-payments-details {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.is-email .jetpack-simple-payments-product-image + .jetpack-simple-payments-details {
|
||||
display: table-cell;
|
||||
width: 70%;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 3,
|
||||
"name": "jetpack/paypal-payment-buttons",
|
||||
"version": "0.1.0",
|
||||
"title": "PayPal Payment Buttons",
|
||||
"category": "monetize",
|
||||
"icon": "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'><path fill='none' d='M0 0h24v24H0V0z' /><path d='M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z' /></svg>",
|
||||
"description": "Custom block for displaying PayPal Payment Buttons.",
|
||||
"example": {},
|
||||
"attributes": {
|
||||
"buttonType": {
|
||||
"type": "string",
|
||||
"enum": [ "stacked", "single" ],
|
||||
"default": "stacked"
|
||||
},
|
||||
"scriptSrc": {
|
||||
"type": "string"
|
||||
},
|
||||
"hostedButtonId": {
|
||||
"type": "string"
|
||||
},
|
||||
"buttonText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"html": false
|
||||
},
|
||||
"textdomain": "jetpack-paypal-payments"
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
/**
|
||||
* PayPal Payment Buttons block lets users embed a PayPal button to sell products on their site.
|
||||
*
|
||||
* @package automattic/jetpack-paypal-payments
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\PaypalPayments;
|
||||
|
||||
use Automattic\Jetpack\Assets;
|
||||
use Automattic\Jetpack\Blocks;
|
||||
|
||||
/**
|
||||
* Class PayPal_Payment_Buttons
|
||||
*
|
||||
* @package Automattic\Jetpack\PaypalPayments
|
||||
*/
|
||||
class PayPal_Payment_Buttons {
|
||||
/**
|
||||
* The block full slugname.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const BLOCK_NAME = 'jetpack/paypal-payment-buttons';
|
||||
|
||||
/**
|
||||
* PayPal partner attribution ID used for tracking.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PAYPAL_PARTNER_ATTRIBUTION_ID = 'WooNCPS_Ecom_Wordpress';
|
||||
|
||||
/**
|
||||
* Validates and sanitizes a script URL to ensure it's from an allowed PayPal domain.
|
||||
*
|
||||
* @param string $url The URL to validate and sanitize.
|
||||
* @return string|false The sanitized URL, or false if URL is not from an allowed PayPal domain.
|
||||
*/
|
||||
public static function sanitize_paypal_script_url( $url ) {
|
||||
if ( empty( $url ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parsed_url = wp_parse_url( $url );
|
||||
if ( ! $parsed_url || empty( $parsed_url['host'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize the host
|
||||
$host = strtolower( $parsed_url['host'] );
|
||||
$host = rtrim( $host, '.' );
|
||||
|
||||
// Only allow specific PayPal domains
|
||||
$allowed_hosts = array(
|
||||
'www.paypal.com',
|
||||
'paypal.com',
|
||||
'www.sandbox.paypal.com',
|
||||
'sandbox.paypal.com',
|
||||
);
|
||||
|
||||
if ( ! in_array( $host, $allowed_hosts, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rebuild the URL with HTTPS
|
||||
$sanitized_url = 'https://' . $host;
|
||||
|
||||
if ( isset( $parsed_url['path'] ) ) {
|
||||
$sanitized_url .= $parsed_url['path'];
|
||||
}
|
||||
if ( isset( $parsed_url['query'] ) ) {
|
||||
// If we have escaped ampersands in the query string, we need to unescape them.
|
||||
$sanitized_url .= '?' . str_replace( '&', '&', $parsed_url['query'] );
|
||||
}
|
||||
|
||||
return $sanitized_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the block for use in Gutenberg
|
||||
* This is done via an action so that we can disable
|
||||
* registration if we need to.
|
||||
*/
|
||||
public static function register_block() {
|
||||
Blocks::jetpack_register_block(
|
||||
__DIR__,
|
||||
array(
|
||||
'render_callback' => array( __CLASS__, 'render_block' ),
|
||||
'plan_check' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
*
|
||||
* @param array $attributes The block attributes.
|
||||
* @param string $content The block content.
|
||||
* @return string|void
|
||||
*/
|
||||
public static function render_block( $attributes, $content ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$button_type = $attributes['buttonType'] ?? '';
|
||||
$script_src = $attributes['scriptSrc'] ?? '';
|
||||
$hosted_button_id = $attributes['hostedButtonId'] ?? '';
|
||||
$button_text = $attributes['buttonText'] ?? '';
|
||||
|
||||
if ( empty( $button_type ) || empty( $hosted_button_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For stacked buttons, we need both scriptSrc and hostedButtonId
|
||||
if ( 'stacked' === $button_type && empty( $script_src ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For single buttons, we need buttonText
|
||||
if ( 'single' === $button_type && empty( $button_text ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'stacked' === $button_type ) {
|
||||
// Sanitize the script URL to ensure it's from an allowed PayPal domain
|
||||
$sanitized_url = self::sanitize_paypal_script_url( $script_src );
|
||||
if ( false === $sanitized_url ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't include the version number here. If we do, it is appended to the URL and causes a 400 response.
|
||||
wp_enqueue_script( 'paypal-payment-buttons-block-head', $sanitized_url, array(), null, false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
||||
add_filter(
|
||||
'script_loader_tag',
|
||||
function ( $tag, $handle, $src ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( 'paypal-payment-buttons-block-head' === $handle ) {
|
||||
// Add namespace to avoid conflicts with other PayPal SDK versions
|
||||
if ( ! str_contains( $tag, 'data-namespace' ) ) {
|
||||
$tag = preg_replace( '/(\s+)src=([\'"])/', '$1 data-namespace="paypal_payment_buttons" src=$2', $tag );
|
||||
}
|
||||
// Add partner attribution ID
|
||||
if ( ! str_contains( $tag, 'data-paypal-partner-attribution-id' ) ) {
|
||||
$tag = preg_replace( '/(\s+)src=([\'"])/', '$1 data-paypal-partner-attribution-id="' . self::PAYPAL_PARTNER_ATTRIBUTION_ID . '" src=$2', $tag );
|
||||
}
|
||||
}
|
||||
return $tag;
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
|
||||
// Generate the button HTML and inline script
|
||||
$container_id = 'paypal-container-' . $hosted_button_id;
|
||||
$button_html = '<div id="' . esc_attr( $container_id ) . '"></div>';
|
||||
|
||||
$inline_script = sprintf(
|
||||
'(window.paypal_payment_buttons || window.paypal).HostedButtons({
|
||||
hostedButtonId: %s,
|
||||
}).render(%s);',
|
||||
wp_json_encode( $hosted_button_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ),
|
||||
wp_json_encode( '#' . $container_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP )
|
||||
);
|
||||
|
||||
wp_add_inline_script( 'paypal-payment-buttons-block-head', $inline_script );
|
||||
|
||||
return $button_html;
|
||||
}
|
||||
|
||||
// Single button type - generate the complete form HTML
|
||||
if ( 'single' === $button_type ) {
|
||||
self::register_hooks();
|
||||
|
||||
$payment_id = esc_attr( $hosted_button_id );
|
||||
$button_text_escaped = esc_attr( $button_text );
|
||||
$action_url = esc_url( 'https://www.paypal.com/ncp/payment/' . $payment_id . '?at_code=' . self::PAYPAL_PARTNER_ATTRIBUTION_ID );
|
||||
|
||||
$button_html = sprintf(
|
||||
'<style>.pp-%1$s{text-align:center;border:none;border-radius:0.25rem;min-width:11.625rem;padding:0 2rem;height:2.625rem;font-weight:bold;background-color:#FFD140;color:#000000;font-family:"Helvetica Neue",Arial,sans-serif;font-size:1rem;line-height:1.25rem;cursor:pointer;}</style>
|
||||
<div>
|
||||
<form action="%2$s" method="post" target="_blank" style="display:inline-grid;justify-items:center;align-content:start;gap:0.5rem;">
|
||||
<input class="pp-%1$s" type="submit" value="%3$s" />
|
||||
<img src="https://www.paypalobjects.com/images/Debit_Credit_APM.svg" alt="cards" />
|
||||
<section style="font-size: 0.75rem;"> Powered by <img src="https://www.paypalobjects.com/paypal-ui/logos/svg/paypal-wordmark-color.svg" alt="paypal" style="height:0.875rem;vertical-align:middle;"/></section>
|
||||
</form>
|
||||
</div>',
|
||||
$payment_id,
|
||||
$action_url,
|
||||
$button_text_escaped
|
||||
);
|
||||
|
||||
return $button_html;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load editor styles for the block.
|
||||
* These are loaded via enqueue_block_assets to ensure proper loading in the editor iframe context.
|
||||
*/
|
||||
public static function load_editor_styles() {
|
||||
$handle = 'jp-paypal-payments-ncps-blocks';
|
||||
|
||||
Assets::register_script(
|
||||
$handle,
|
||||
'../../dist/paypal-payment-buttons/editor.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'css_path' => '../../dist/paypal-payment-buttons/editor.css',
|
||||
'textdomain' => 'jetpack-paypal-payments',
|
||||
)
|
||||
);
|
||||
wp_enqueue_style( $handle );
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads scripts
|
||||
*/
|
||||
public static function load_editor_scripts() {
|
||||
Assets::register_script(
|
||||
'jp-paypal-payments-ncps-blocks',
|
||||
'../../dist/paypal-payment-buttons/editor.js',
|
||||
__FILE__,
|
||||
array(
|
||||
'in_footer' => true,
|
||||
'textdomain' => 'jetpack-paypal-payments',
|
||||
'enqueue' => true,
|
||||
// Editor styles are loaded separately, see load_editor_styles().
|
||||
'css_path' => null,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add display to the allowed styles.
|
||||
*
|
||||
* @see https://developer.wordpress.org/reference/hooks/safe_style_css/
|
||||
*
|
||||
* @param array $safe_styles The allowed styles.
|
||||
* @return array The allowed styles.
|
||||
*/
|
||||
public static function add_style_display( array $safe_styles ): array {
|
||||
$safe_styles[] = 'display';
|
||||
return $safe_styles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public static function register_hooks() {
|
||||
add_filter( 'safe_style_css', array( __CLASS__, 'add_style_display' ) );
|
||||
}
|
||||
}
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
import { isWpcomPlatformSite } from '@automattic/jetpack-script-data';
|
||||
import { PlainText, useBlockProps } from '@wordpress/block-editor';
|
||||
import {
|
||||
Notice,
|
||||
Placeholder,
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalToggleGroupControl as ToggleGroupControl,
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalItemGroup as ItemGroup,
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalItem as Item,
|
||||
} from '@wordpress/components';
|
||||
import { createInterpolateElement, useEffect, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Link } from '@wordpress/ui';
|
||||
import PayPalIcon from './icon';
|
||||
|
||||
import './editor.scss';
|
||||
|
||||
const BUTTON_ID_PATTERN = '[A-Za-z0-9_-]+';
|
||||
|
||||
const extractScriptSrc = codeHead => {
|
||||
const match = codeHead.match(
|
||||
/src="(https:\/\/(www\.)?(sandbox\.)?paypal\.com\/sdk\/js\?[^"]+)"/
|
||||
);
|
||||
return match ? match[ 1 ] : '';
|
||||
};
|
||||
|
||||
const extractHostedButtonId = codeBody => {
|
||||
let buttonId = '';
|
||||
|
||||
// Try to extract from hostedButtonId property first (stacked buttons)
|
||||
const hostedButtonMatch = codeBody.match(
|
||||
new RegExp( `hostedButtonId:\\s*["'](${ BUTTON_ID_PATTERN })["']` )
|
||||
);
|
||||
if ( hostedButtonMatch ) {
|
||||
buttonId = hostedButtonMatch[ 1 ];
|
||||
}
|
||||
|
||||
// Try to extract from form action URL (single buttons)
|
||||
// Support international domains, protocol-relative URLs, and case-insensitive domain matching
|
||||
// Extract ID before any query parameters or spaces
|
||||
if ( ! buttonId ) {
|
||||
const actionMatch = codeBody.match(
|
||||
/action\s*=\s*["'](?:https?:)?\/\/(?:www\.)?(?:sandbox\.)?paypal\.[a-z.]+\/ncp\/payment\/([A-Za-z0-9_-]+)\s*(?:\?[^"']*)?["']/i
|
||||
);
|
||||
if ( actionMatch ) {
|
||||
buttonId = actionMatch[ 1 ];
|
||||
}
|
||||
}
|
||||
|
||||
return buttonId.trim();
|
||||
};
|
||||
|
||||
const extractButtonText = codeBody => {
|
||||
// Extract button text from input value attribute (single buttons)
|
||||
// Support spaces around equals sign and both self-closing and non-self-closing tags
|
||||
const inputMatch = codeBody.match( /<input[^>]*value\s*=\s*["']([^"']+)["'][^>]*\/?>/ );
|
||||
return inputMatch ? inputMatch[ 1 ].trim() : '';
|
||||
};
|
||||
|
||||
const generateHeadCode = scriptSrc => {
|
||||
if ( ! scriptSrc ) {
|
||||
return '';
|
||||
}
|
||||
return `<script src="${ scriptSrc }" data-namespace="paypal_payment_buttons"></script>`;
|
||||
};
|
||||
|
||||
const generateBodyCode = ( hostedButtonId, buttonType = 'stacked', buttonText = '' ) => {
|
||||
if ( ! hostedButtonId ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( buttonType === 'single' ) {
|
||||
return `<style>.pp-${ hostedButtonId }{text-align:center;border:none;border-radius:0.25rem;min-width:11.625rem;padding:0 2rem;height:2.625rem;font-weight:bold;background-color:#FFD140;color:#000000;font-family:"Helvetica Neue",Arial,sans-serif;font-size:1rem;line-height:1.25rem;cursor:pointer;}</style>
|
||||
<form action="https://www.paypal.com/ncp/payment/${ hostedButtonId }" method="post" target="_blank" style="display:inline-grid;justify-items:center;align-content:start;gap:0.5rem;">
|
||||
<input class="pp-${ hostedButtonId }" type="submit" value="${ buttonText || 'Pay Now' }" />
|
||||
<img src="https://www.paypalobjects.com/images/Debit_Credit_APM.svg" alt="cards" />
|
||||
<section style="font-size: 0.75rem;"> Powered by <img src="https://www.paypalobjects.com/paypal-ui/logos/svg/paypal-wordmark-color.svg" alt="paypal" style="height:0.875rem;vertical-align:middle;"/></section>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
return `<div id="paypal-container-${ hostedButtonId }"></div>
|
||||
<script>
|
||||
(window.paypal_payment_buttons || window.paypal).HostedButtons({
|
||||
hostedButtonId: "${ hostedButtonId }",
|
||||
}).render("#paypal-container-${ hostedButtonId }")
|
||||
</script>`;
|
||||
};
|
||||
|
||||
const validScriptSrc = scriptSrc =>
|
||||
/^https:\/\/(www\.)?(sandbox\.)?paypal\.com\/sdk\/js\?client-id=/.test( scriptSrc );
|
||||
|
||||
const validHostedButtonId = hostedButtonId => {
|
||||
// Validate the button ID format
|
||||
return new RegExp( `^${ BUTTON_ID_PATTERN }$` ).test( hostedButtonId );
|
||||
};
|
||||
|
||||
const validButtonText = buttonText =>
|
||||
buttonText && buttonText.trim().length > 0 && buttonText.length <= 50;
|
||||
|
||||
/**
|
||||
* Get PayPal signup URL with platform-specific tracking parameters
|
||||
*
|
||||
* @return {string} The PayPal signup URL
|
||||
*/
|
||||
const getPayPalSignupUrl = () => {
|
||||
const isWpcom = isWpcomPlatformSite();
|
||||
const utmSource = isWpcom ? 'wp_com' : 'wp_org';
|
||||
const atCode = isWpcom ? 'wp_com' : 'wp_org';
|
||||
return `https://www.paypal.com/bizsignup/entry?product=payment_button&utm_source=${ utmSource }&at_code=${ atCode }`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get PayPal login URL with platform-specific tracking parameters
|
||||
*
|
||||
* @return {string} The PayPal login URL
|
||||
*/
|
||||
const getPayPalLoginUrl = () => {
|
||||
const isWpcom = isWpcomPlatformSite();
|
||||
const utmSource = isWpcom ? 'wp_com' : 'wp_org';
|
||||
const atCode = isWpcom ? 'wp_com' : 'wp_org';
|
||||
return `https://www.paypal.com/ncp/buttons/create?utm_source=${ utmSource }&at_code=${ atCode }`;
|
||||
};
|
||||
|
||||
/**
|
||||
* PayPal Single Button Preview component (rendered directly)
|
||||
*
|
||||
* @param {object} root0 - The component props
|
||||
* @param {string} root0.buttonText - The button text
|
||||
* @return {Element} The PayPal single button preview component
|
||||
*/
|
||||
const PayPalSingleButtonPreview = ( { buttonText } ) => {
|
||||
const paypalButtonStyles = {
|
||||
textAlign: 'center',
|
||||
border: 'none',
|
||||
borderRadius: '0.25rem',
|
||||
minWidth: '11.625rem',
|
||||
padding: '0 2rem',
|
||||
height: '2.625rem',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#FFD140',
|
||||
color: '#000000',
|
||||
fontFamily: '"Helvetica Neue", Arial, sans-serif',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.25rem',
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'none', // Prevent clicking in editor
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={ { textAlign: 'center' } }>
|
||||
<form
|
||||
style={ {
|
||||
display: 'inline-grid',
|
||||
justifyItems: 'center',
|
||||
alignContent: 'start',
|
||||
gap: '0.5rem',
|
||||
} }
|
||||
>
|
||||
<input type="button" value={ buttonText } style={ paypalButtonStyles } />
|
||||
<img src="https://www.paypalobjects.com/images/Debit_Credit_APM.svg" alt="cards" />
|
||||
<section style={ { fontSize: '0.75rem' } }>
|
||||
Powered by{ ' ' }
|
||||
<img
|
||||
src="https://www.paypalobjects.com/paypal-ui/logos/svg/paypal-wordmark-color.svg"
|
||||
alt="paypal"
|
||||
style={ { height: '0.875rem', verticalAlign: 'middle' } }
|
||||
/>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if we have the required data for a preview (only single buttons)
|
||||
*
|
||||
* @param {object} attributes - The block attributes
|
||||
* @return {boolean} Whether preview can be shown
|
||||
*/
|
||||
const canShowPreview = attributes => {
|
||||
const { buttonType, hostedButtonId, buttonText } = attributes;
|
||||
|
||||
if ( ! hostedButtonId || ! validHostedButtonId( hostedButtonId ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( buttonType === 'single' ) {
|
||||
return buttonText && validButtonText( buttonText );
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* PayPal Preview component router
|
||||
*
|
||||
* @param {object} root0 - The component props
|
||||
* @param {object} root0.attributes - The block attributes
|
||||
* @return {Element|null} The PayPal preview component or null
|
||||
*/
|
||||
const PayPalPreview = ( { attributes } ) => {
|
||||
const { buttonType, buttonText } = attributes;
|
||||
|
||||
if ( ! canShowPreview( attributes ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only render preview for single button type
|
||||
if ( buttonType === 'single' ) {
|
||||
return <PayPalSingleButtonPreview buttonText={ buttonText } />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function Edit( { attributes, setAttributes, isSelected } ) {
|
||||
const { buttonType, scriptSrc, hostedButtonId, buttonText } = attributes;
|
||||
const [ notice, setNotice ] = useState( null );
|
||||
const [ rawHeadCode, setRawHeadCode ] = useState( '' );
|
||||
const [ rawBodyCode, setRawBodyCode ] = useState( '' );
|
||||
|
||||
const stackedInstructions = __(
|
||||
'Stacked Buttons (Recommended): This option lets you present all of your product information and PayPal payment method upfront on your website.',
|
||||
'jetpack-paypal-payments'
|
||||
);
|
||||
const singleInstructions = __(
|
||||
'Single Button: This option lets you quickly paste a single button on your site, with no product information.',
|
||||
'jetpack-paypal-payments'
|
||||
);
|
||||
|
||||
// Initialize raw code when valid extracted values exist
|
||||
useEffect( () => {
|
||||
if ( ! rawHeadCode && scriptSrc && buttonType === 'stacked' ) {
|
||||
setRawHeadCode( generateHeadCode( scriptSrc ) );
|
||||
}
|
||||
}, [ scriptSrc, rawHeadCode, buttonType ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( ! rawBodyCode && hostedButtonId ) {
|
||||
setRawBodyCode( generateBodyCode( hostedButtonId, buttonType, buttonText ) );
|
||||
}
|
||||
}, [ hostedButtonId, rawBodyCode, buttonType, buttonText ] );
|
||||
|
||||
useEffect( () => {
|
||||
// Check if user has pasted invalid code that couldn't be extracted
|
||||
if ( 'stacked' === buttonType && rawHeadCode && rawHeadCode.trim() && ! scriptSrc ) {
|
||||
return setNotice(
|
||||
<Notice status="error" isDismissible={ false }>
|
||||
{ __(
|
||||
'Invalid PayPal script URL. Please paste code from PayPal.com.',
|
||||
'jetpack-paypal-payments'
|
||||
) }
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
|
||||
if ( rawBodyCode && rawBodyCode.trim() && ! hostedButtonId ) {
|
||||
return setNotice(
|
||||
<Notice status="error" isDismissible={ false }>
|
||||
{ __(
|
||||
'Invalid PayPal button code. Please paste code from PayPal.com.',
|
||||
'jetpack-paypal-payments'
|
||||
) }
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
|
||||
// Validate extracted values
|
||||
if ( 'stacked' === buttonType && scriptSrc && ! validScriptSrc( scriptSrc ) ) {
|
||||
return setNotice(
|
||||
<Notice status="error" isDismissible={ false }>
|
||||
{ __( 'Invalid PayPal script URL.', 'jetpack-paypal-payments' ) }
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
|
||||
if ( hostedButtonId && ! validHostedButtonId( hostedButtonId ) ) {
|
||||
return setNotice(
|
||||
<Notice status="error" isDismissible={ false }>
|
||||
{ __( 'Invalid PayPal button ID.', 'jetpack-paypal-payments' ) }
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'single' === buttonType && buttonText && ! validButtonText( buttonText ) ) {
|
||||
return setNotice(
|
||||
<Notice status="error" isDismissible={ false }>
|
||||
{ __( 'Button text must be between 1 and 50 characters.', 'jetpack-paypal-payments' ) }
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
|
||||
setNotice( null );
|
||||
}, [ buttonType, scriptSrc, hostedButtonId, buttonText, rawHeadCode, rawBodyCode ] );
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
// Early return for preview rendering
|
||||
if ( ! isSelected && ! notice && canShowPreview( attributes ) ) {
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<PayPalPreview attributes={ attributes } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const stackedButtonCodeLabel = __( 'Part 2 code', 'jetpack-paypal-payments' );
|
||||
const stackedButtonCodePlaceholder = __(
|
||||
'Paste the part 2 code here…',
|
||||
'jetpack-paypal-payments'
|
||||
);
|
||||
|
||||
const singleButtonCodeLabel = __( 'Single button code', 'jetpack-paypal-payments' );
|
||||
const singleButtonCodePlaceholder = __(
|
||||
'Paste the single button code here…',
|
||||
'jetpack-paypal-payments'
|
||||
);
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Placeholder
|
||||
icon={ PayPalIcon }
|
||||
label={ __( 'PayPal Payment Buttons', 'jetpack-paypal-payments' ) }
|
||||
isColumnLayout
|
||||
instructions={ buttonType === 'stacked' ? stackedInstructions : singleInstructions }
|
||||
notices={ notice }
|
||||
>
|
||||
<ItemGroup>
|
||||
<Item>
|
||||
{ createInterpolateElement(
|
||||
__(
|
||||
'1. <SignupLink><strong>Sign up</strong></SignupLink> or <LoginLink><strong>log in</strong></LoginLink> to PayPal to get your Payment Button code.',
|
||||
'jetpack-paypal-payments'
|
||||
),
|
||||
{
|
||||
SignupLink: <Link openInNewTab href={ getPayPalSignupUrl() } />,
|
||||
LoginLink: <Link openInNewTab href={ getPayPalLoginUrl() } />,
|
||||
strong: <strong />,
|
||||
}
|
||||
) }
|
||||
</Item>
|
||||
<Item>
|
||||
{ 'stacked' === buttonType &&
|
||||
__(
|
||||
'2. After login, choose Payment Buttons. Enter your product or service details, and build the buttons. Copy the button code for Stacked Buttons (copy html code).',
|
||||
'jetpack-paypal-payments'
|
||||
) }
|
||||
{ 'single' === buttonType &&
|
||||
__(
|
||||
'2. After login, choose Payment Buttons. Enter your product or service details, and build the buttons. Copy the button code for Single Button.',
|
||||
'jetpack-paypal-payments'
|
||||
) }
|
||||
</Item>
|
||||
<Item>{ __( '3. Paste the code below.', 'jetpack-paypal-payments' ) }</Item>
|
||||
</ItemGroup>
|
||||
<ToggleGroupControl
|
||||
label={ __( 'Button type', 'jetpack-paypal-payments' ) }
|
||||
value={ buttonType }
|
||||
hideLabelFromVision
|
||||
onChange={ type => {
|
||||
const newAttributes = { buttonType: type };
|
||||
newAttributes.scriptSrc = '';
|
||||
newAttributes.buttonText = '';
|
||||
newAttributes.hostedButtonId = '';
|
||||
|
||||
setRawHeadCode( '' );
|
||||
setRawBodyCode( '' );
|
||||
|
||||
setAttributes( newAttributes );
|
||||
} }
|
||||
isBlock
|
||||
__nextHasNoMarginBottom={ true }
|
||||
__next40pxDefaultSize={ true }
|
||||
>
|
||||
<ToggleGroupControlOption
|
||||
value="stacked"
|
||||
label={ __( 'Stacked Buttons (Recommended)', 'jetpack-paypal-payments' ) }
|
||||
aria-label={ __(
|
||||
'Stacked Buttons are the recommended option for better conversion rates.',
|
||||
'jetpack-paypal-payments'
|
||||
) }
|
||||
showTooltip={ true }
|
||||
/>
|
||||
<ToggleGroupControlOption
|
||||
value="single"
|
||||
label={ __( 'Single Button', 'jetpack-paypal-payments' ) }
|
||||
/>
|
||||
</ToggleGroupControl>
|
||||
{ 'stacked' === buttonType && (
|
||||
<PlainText
|
||||
value={ rawHeadCode }
|
||||
onChange={ code => {
|
||||
setRawHeadCode( code );
|
||||
const extractedSrc = extractScriptSrc( code );
|
||||
setAttributes( {
|
||||
scriptSrc: extractedSrc,
|
||||
} );
|
||||
} }
|
||||
placeholder={ __( 'Paste the part 1 code here…', 'jetpack-paypal-payments' ) }
|
||||
aria-label={ __( 'Part 1 code', 'jetpack-paypal-payments' ) }
|
||||
name="paypal-payment-buttons-code-head"
|
||||
/>
|
||||
) }
|
||||
<PlainText
|
||||
value={ rawBodyCode }
|
||||
onChange={ code => {
|
||||
setRawBodyCode( code );
|
||||
const extractedButtonId = extractHostedButtonId( code );
|
||||
const extractedButtonText = extractButtonText( code );
|
||||
setAttributes( {
|
||||
hostedButtonId: extractedButtonId,
|
||||
buttonText: extractedButtonText,
|
||||
} );
|
||||
} }
|
||||
placeholder={
|
||||
'stacked' === buttonType ? stackedButtonCodePlaceholder : singleButtonCodePlaceholder
|
||||
}
|
||||
aria-label={ 'stacked' === buttonType ? stackedButtonCodeLabel : singleButtonCodeLabel }
|
||||
name="paypal-payment-buttons-code-body"
|
||||
/>
|
||||
</Placeholder>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { registerJetpackBlockFromMetadata } from '../block/register-jetpack-block';
|
||||
import metadata from './block.json';
|
||||
import edit from './edit';
|
||||
import PayPalIcon from './icon';
|
||||
import './editor.scss';
|
||||
|
||||
registerJetpackBlockFromMetadata( metadata, {
|
||||
edit,
|
||||
save: () => null,
|
||||
icon: PayPalIcon,
|
||||
} );
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { SVG, Path } from '@wordpress/components';
|
||||
|
||||
export default (
|
||||
<SVG xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 154.7 190.5">
|
||||
<Path
|
||||
fill="#001c64"
|
||||
d="M60.5 38.1a5.5 5.5 0 0 0-5.4 4.6l-9 57.2-8.3 52.5 8.3-52.5a5.5 5.5 0 0 1 5.4-4.6H78a54 54 0 0 0 53.8-51.6 50 50 0 0 0-23.4-5.6z"
|
||||
/>
|
||||
<Path
|
||||
fill="#0070e0"
|
||||
d="M131.7 43.7a54 54 0 0 1-53.8 51.6H51.5c-2.7 0-5 2-5.4 4.6l-8.3 52.5-5.2 33a4.5 4.5 0 0 0 4.4 5.1h28.7a5.5 5.5 0 0 0 5.4-4.6l7.6-48a5.5 5.5 0 0 1 5.4-4.5H101a54 54 0 0 0 53.2-45.7c3-18.7-6.5-35.6-22.5-44z"
|
||||
/>
|
||||
<Path
|
||||
fill="#003087"
|
||||
d="M28 0a5.5 5.5 0 0 0-5.5 4.6L.1 147.2a4.5 4.5 0 0 0 4.4 5.2h33.3l8.3-52.5 9-57.2a5.5 5.5 0 0 1 5.4-4.6h47.8c8.7 0 16.6 2 23.4 5.6C132 19.7 112.4 0 85.3 0z"
|
||||
/>
|
||||
</SVG>
|
||||
);
|
||||
+635
@@ -0,0 +1,635 @@
|
||||
<?php
|
||||
/**
|
||||
* Pay with PayPal (aka Simple Payments)
|
||||
*
|
||||
* Display a Pay with PayPal button as a Widget.
|
||||
*
|
||||
* @package automattic/jetpack-paypal-payments
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Paypal_Payments\Widgets;
|
||||
|
||||
use Automattic\Jetpack\PayPal_Payments;
|
||||
use Automattic\Jetpack\Paypal_Payments\Simple_Payments;
|
||||
use Automattic\Jetpack\Tracking;
|
||||
use Jetpack;
|
||||
use WP_Error;
|
||||
use WP_Widget;
|
||||
|
||||
// Disable direct access/execution to/of the widget code.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'Simple_Payments_Widget' ) ) {
|
||||
/**
|
||||
* Pay with PayPal (aka Simple Payments)
|
||||
*
|
||||
* Display a Pay with PayPal button as a Widget.
|
||||
*/
|
||||
class Simple_Payments_Widget extends WP_Widget {
|
||||
/**
|
||||
* The package version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $package_version = PayPal_Payments::PACKAGE_VERSION;
|
||||
|
||||
/**
|
||||
* Currencies should be supported by PayPal:
|
||||
*
|
||||
* @var array $supported_currency_list
|
||||
* @link https://developer.paypal.com/docs/api/reference/currency-codes/
|
||||
*
|
||||
* List has to be in sync with list at the block's client side and API's backend side:
|
||||
* @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/extensions/blocks/simple-payments/constants.js#L9-L39
|
||||
* @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/modules/simple-payments/simple-payments.php#L386-L415
|
||||
*
|
||||
* Indian Rupee (INR) is listed here for backwards compatibility with previously added widgets.
|
||||
* It's not supported by Pay with PayPal because at the time of the creation of this file
|
||||
* because it's limited to in-country PayPal India accounts only.
|
||||
* Discussion: https://github.com/Automattic/wp-calypso/pull/28236
|
||||
*/
|
||||
public static $supported_currency_list = array(
|
||||
'USD' => '$',
|
||||
'GBP' => '£',
|
||||
'JPY' => '¥',
|
||||
'BRL' => 'R$',
|
||||
'EUR' => '€',
|
||||
'NZD' => 'NZ$',
|
||||
'AUD' => 'A$',
|
||||
'CAD' => 'C$',
|
||||
'INR' => '₹',
|
||||
'ILS' => '₪',
|
||||
'RUB' => '₽',
|
||||
'MXN' => 'MX$',
|
||||
'SEK' => 'Skr',
|
||||
'HUF' => 'Ft',
|
||||
'CHF' => 'CHF',
|
||||
'CZK' => 'Kč',
|
||||
'DKK' => 'Dkr',
|
||||
'HKD' => 'HK$',
|
||||
'NOK' => 'Kr',
|
||||
'PHP' => '₱',
|
||||
'PLN' => 'PLN',
|
||||
'SGD' => 'S$',
|
||||
'TWD' => 'NT$',
|
||||
'THB' => '฿',
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'jetpack_simple_payments_widget',
|
||||
/** This filter is documented in modules/widgets/facebook-likebox.php */
|
||||
apply_filters( 'jetpack_widget_name', __( 'Pay with PayPal', 'jetpack-paypal-payments' ) ),
|
||||
array(
|
||||
'classname' => 'simple-payments',
|
||||
'description' => __( 'Add a Pay with PayPal button as a Widget.', 'jetpack-paypal-payments' ),
|
||||
'customize_selective_refresh' => true,
|
||||
)
|
||||
);
|
||||
|
||||
global $pagenow;
|
||||
if ( is_customize_preview() || 'widgets.php' === $pagenow ) {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_styles' ) );
|
||||
}
|
||||
|
||||
if ( is_customize_preview() && Simple_Payments::is_enabled_jetpack_simple_payments() ) {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
|
||||
|
||||
add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) );
|
||||
add_action( 'wp_ajax_customize-jetpack-simple-payments-buttons-get', array( $this, 'ajax_get_payment_buttons' ) );
|
||||
add_action( 'wp_ajax_customize-jetpack-simple-payments-button-save', array( $this, 'ajax_save_payment_button' ) );
|
||||
add_action( 'wp_ajax_customize-jetpack-simple-payments-button-delete', array( $this, 'ajax_delete_payment_button' ) );
|
||||
}
|
||||
|
||||
add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_simple_payment_widget' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of the widgets hidden from the Legacy Widget block.
|
||||
*
|
||||
* This is used to hide the Pay with PayPal from the Legacy Widget block.
|
||||
*
|
||||
* @param array $widget_types the widget types that are currently hidden.
|
||||
* @return array Widget types hidden from the Legacy Widget block
|
||||
*/
|
||||
public function hide_simple_payment_widget( $widget_types ) {
|
||||
$widget_types[] = 'simple_payments_widget';
|
||||
return $widget_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an associative array of default values.
|
||||
*
|
||||
* These values are used in new widgets.
|
||||
*
|
||||
* @return array Default values for the widget options.
|
||||
*/
|
||||
private function defaults() {
|
||||
$current_user = wp_get_current_user();
|
||||
$default_product_id = $this->get_first_product_id();
|
||||
|
||||
return array(
|
||||
'title' => '',
|
||||
'product_post_id' => $default_product_id,
|
||||
'form_action' => '',
|
||||
'form_product_id' => 0,
|
||||
'form_product_title' => '',
|
||||
'form_product_description' => '',
|
||||
'form_product_image_id' => 0,
|
||||
'form_product_image_src' => '',
|
||||
'form_product_currency' => '',
|
||||
'form_product_price' => '',
|
||||
'form_product_multiple' => '',
|
||||
'form_product_email' => $current_user->user_email,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a nonce for customizing menus.
|
||||
*
|
||||
* @param array $nonces Array of nonces.
|
||||
* @return array $nonces Modified array of nonces.
|
||||
*/
|
||||
public function filter_nonces( $nonces ) {
|
||||
$nonces['customize-jetpack-simple-payments'] = wp_create_nonce( 'customize-jetpack-simple-payments' );
|
||||
return $nonces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue styles.
|
||||
*/
|
||||
public function enqueue_style() {
|
||||
wp_enqueue_style( 'simple-payments-widget-style', plugins_url( 'simple-payments/style.css', __FILE__ ), array(), '20180518' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin styles.
|
||||
*/
|
||||
public function admin_enqueue_styles() {
|
||||
wp_enqueue_style(
|
||||
'simple-payments-widget-customizer',
|
||||
plugins_url( 'simple-payments/customizer.css', __FILE__ ),
|
||||
array(),
|
||||
$this->package_version
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin scripts.
|
||||
*/
|
||||
public function admin_enqueue_scripts() {
|
||||
wp_enqueue_media();
|
||||
wp_enqueue_script(
|
||||
'simple-payments-widget-customizer',
|
||||
plugins_url( '/simple-payments/customizer.js', __FILE__ ),
|
||||
array( 'jquery' ),
|
||||
$this->package_version,
|
||||
true
|
||||
);
|
||||
wp_localize_script(
|
||||
'simple-payments-widget-customizer',
|
||||
'jpSimplePaymentsStrings',
|
||||
array(
|
||||
'deleteConfirmation' => __( 'Are you sure you want to delete this item? It will be disabled and removed from all locations where it currently appears.', 'jetpack-paypal-payments' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payment buttons.
|
||||
*/
|
||||
public function ajax_get_payment_buttons() {
|
||||
if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) {
|
||||
wp_send_json_error( 'bad_nonce', 400, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'customize' ) ) {
|
||||
wp_send_json_error( 'customize_not_allowed', 403, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
$post_type_object = get_post_type_object( Simple_Payments::$post_type_product );
|
||||
if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
|
||||
wp_send_json_error( 'insufficient_post_permissions', 403, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
$product_posts = get_posts(
|
||||
array(
|
||||
'numberposts' => 100,
|
||||
'orderby' => 'date',
|
||||
'post_type' => Simple_Payments::$post_type_product,
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
|
||||
$formatted_products = array_map( array( $this, 'format_product_post_for_ajax_reponse' ), $product_posts );
|
||||
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_success( $formatted_products, null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format product_post object.
|
||||
*
|
||||
* @param object $product_post - info about the post the product is on.
|
||||
*/
|
||||
public function format_product_post_for_ajax_reponse( $product_post ) {
|
||||
return array(
|
||||
'ID' => $product_post->ID,
|
||||
'post_title' => $product_post->post_title,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle saving the simple payments widget.
|
||||
*/
|
||||
public function ajax_save_payment_button() {
|
||||
if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) {
|
||||
wp_send_json_error( 'bad_nonce', 400, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'customize' ) ) {
|
||||
wp_send_json_error( 'customize_not_allowed', 403, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
$post_type_object = get_post_type_object( Simple_Payments::$post_type_product );
|
||||
if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
|
||||
wp_send_json_error( 'insufficient_post_permissions', 403, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
|
||||
wp_send_json_error( 'missing_params', 400, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
$params = wp_unslash( $_POST['params'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Manually validated by validate_ajax_params().
|
||||
$errors = $this->validate_ajax_params( $params );
|
||||
if ( ! empty( $errors->errors ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( $errors, null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
$product_post_id = isset( $params['product_post_id'] ) ? (int) $params['product_post_id'] : 0;
|
||||
|
||||
$product_post = array(
|
||||
'ID' => $product_post_id,
|
||||
'post_type' => Simple_Payments::$post_type_product,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $params['post_title'],
|
||||
'post_content' => $params['post_content'],
|
||||
'_thumbnail_id' => ! empty( $params['image_id'] ) ? $params['image_id'] : -1,
|
||||
'meta_input' => array(
|
||||
'spay_currency' => $params['currency'],
|
||||
'spay_price' => $params['price'],
|
||||
'spay_multiple' => isset( $params['multiple'] ) ? (int) $params['multiple'] : 0,
|
||||
'spay_email' => is_email( $params['email'] ),
|
||||
),
|
||||
);
|
||||
|
||||
if ( empty( $product_post_id ) ) {
|
||||
$product_post_id = wp_insert_post( $product_post );
|
||||
} else {
|
||||
$product_post_id = wp_update_post( $product_post );
|
||||
}
|
||||
|
||||
if ( ! $product_post_id || is_wp_error( $product_post_id ) ) {
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_error( $product_post_id, null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
$tracks_properties = array(
|
||||
'id' => $product_post_id,
|
||||
'currency' => $params['currency'],
|
||||
'price' => $params['price'],
|
||||
);
|
||||
if ( 0 === $product_post['ID'] ) {
|
||||
$this->record_event( 'created', 'create', $tracks_properties );
|
||||
} else {
|
||||
$this->record_event( 'updated', 'update', $tracks_properties );
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'product_post_id' => $product_post_id,
|
||||
'product_post_title' => $params['post_title'],
|
||||
),
|
||||
null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deleting the simple payment widget.
|
||||
*/
|
||||
public function ajax_delete_payment_button() {
|
||||
if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) {
|
||||
wp_send_json_error( 'bad_nonce', 400, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'customize' ) ) {
|
||||
wp_send_json_error( 'customize_not_allowed', 403, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
|
||||
wp_send_json_error( 'missing_params', 400, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
$params = wp_unslash( $_POST['params'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Manually validated just below.
|
||||
$illegal_params = array_diff( array_keys( $params ), array( 'product_post_id' ) );
|
||||
if ( ! empty( $illegal_params ) ) {
|
||||
wp_send_json_error( 'illegal_params', 400, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
$product_id = (int) $params['product_post_id'];
|
||||
$product_post = get_post( $product_id );
|
||||
|
||||
$return = array( 'status' => $product_post->post_status );
|
||||
|
||||
wp_delete_post( $product_id, true );
|
||||
$status = get_post_status( $product_id );
|
||||
if ( false === $status ) {
|
||||
$return['status'] = 'deleted';
|
||||
}
|
||||
|
||||
$this->record_event( 'deleted', 'delete', array( 'id' => $product_id ) );
|
||||
|
||||
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
|
||||
wp_send_json_success( $return, null, JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of decimal places on string representing a price.
|
||||
*
|
||||
* @param string $number Price to check.
|
||||
* @return int|null number of decimal places.
|
||||
*/
|
||||
private function get_decimal_places( $number ) {
|
||||
$parts = explode( '.', $number );
|
||||
if ( count( $parts ) > 2 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isset( $parts[1] ) ? strlen( $parts[1] ) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate ajax parameters.
|
||||
*
|
||||
* @param array $params - the parameters.
|
||||
*/
|
||||
public function validate_ajax_params( $params ) {
|
||||
$errors = new WP_Error();
|
||||
|
||||
$illegal_params = array_diff( array_keys( $params ), array( 'product_post_id', 'post_title', 'post_content', 'image_id', 'currency', 'price', 'multiple', 'email' ) );
|
||||
if ( ! empty( $illegal_params ) ) {
|
||||
$errors->add( 'illegal_params', __( 'Invalid parameters.', 'jetpack-paypal-payments' ) );
|
||||
}
|
||||
|
||||
if ( empty( $params['post_title'] ) ) {
|
||||
$errors->add( 'post_title', __( "People need to know what they're paying for! Please add a brief title.", 'jetpack-paypal-payments' ) );
|
||||
}
|
||||
|
||||
if ( empty( $params['price'] ) || ! is_numeric( $params['price'] ) || (float) $params['price'] <= 0 ) {
|
||||
$errors->add( 'price', __( 'Everything comes with a price tag these days. Please add a your product price.', 'jetpack-paypal-payments' ) );
|
||||
}
|
||||
|
||||
// Japan's Yen is the only supported currency with a zero decimal precision.
|
||||
$precision = strtoupper( $params['currency'] ) === 'JPY' ? 0 : 2;
|
||||
$price_decimal_places = $this->get_decimal_places( $params['price'] );
|
||||
if ( $price_decimal_places === null || $price_decimal_places > $precision ) {
|
||||
$errors->add( 'price', __( 'Invalid price', 'jetpack-paypal-payments' ) );
|
||||
}
|
||||
|
||||
if ( empty( $params['email'] ) || ! is_email( $params['email'] ) ) {
|
||||
$errors->add( 'email', __( 'We want to make sure payments reach you, so please add an email address.', 'jetpack-paypal-payments' ) );
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id of the first product.
|
||||
*/
|
||||
public function get_first_product_id() {
|
||||
$product_posts = get_posts(
|
||||
array(
|
||||
'numberposts' => 1,
|
||||
'orderby' => 'date',
|
||||
'post_type' => Simple_Payments::$post_type_product,
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
|
||||
return ! empty( $product_posts ) ? $product_posts[0]->ID : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display of widget.
|
||||
*
|
||||
* @see WP_Widget::widget()
|
||||
*
|
||||
* @html-template-var array $instance
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
*/
|
||||
public function widget( $args, $instance ) {
|
||||
$instance = wp_parse_args( $instance, $this->defaults() );
|
||||
|
||||
// Enqueue front end assets.
|
||||
$this->enqueue_style();
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
/** This filter is documented in core/src/wp-includes/default-widgets.php */
|
||||
$title = apply_filters( 'widget_title', $instance['title'] );
|
||||
if ( ! empty( $title ) ) {
|
||||
echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
echo '<div class="jetpack-simple-payments-content">';
|
||||
|
||||
if ( ! empty( $instance['form_action'] ) && in_array( $instance['form_action'], array( 'add', 'edit' ), true ) && is_customize_preview() ) {
|
||||
require __DIR__ . '/simple-payments/widget.php';
|
||||
} else {
|
||||
$jsp = Simple_Payments::get_instance();
|
||||
$simple_payments_button = $jsp->parse_shortcode(
|
||||
array(
|
||||
'id' => $instance['product_post_id'],
|
||||
)
|
||||
);
|
||||
|
||||
if ( $simple_payments_button !== null || is_customize_preview() ) {
|
||||
echo $simple_payments_button; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
}
|
||||
|
||||
echo '</div><!--simple-payments-->';
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
/** This action is already documented in modules/widgets/gravatar-profile.php */
|
||||
do_action( 'jetpack_stats_extra', 'widget_view', 'simple_payments' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latests field value from either the old instance or the new instance.
|
||||
*
|
||||
* @param array $new_instance mixed Array of values for the new form instance.
|
||||
* @param array $old_instance mixed Array of values for the old form instance.
|
||||
* @param mixed $field mixed Field value.
|
||||
*/
|
||||
private function get_latest_field_value( $new_instance, $old_instance, $field ) {
|
||||
return ! empty( $new_instance[ $field ] )
|
||||
? sanitize_text_field( $new_instance[ $field ] )
|
||||
: $old_instance[ $field ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the product fields from the product post. If no post found
|
||||
* it returns the default values.
|
||||
*
|
||||
* @param int $product_post_id Product Post ID.
|
||||
* @return array $fields Product Fields from the Product Post.
|
||||
*/
|
||||
private function get_product_from_post( $product_post_id ) {
|
||||
$product_post = get_post( $product_post_id );
|
||||
$form_product_id = $product_post_id;
|
||||
if ( ! empty( $product_post ) ) {
|
||||
$form_product_image_id = get_post_thumbnail_id( $product_post_id );
|
||||
|
||||
return array(
|
||||
'form_product_id' => $form_product_id,
|
||||
'form_product_title' => get_the_title( $product_post ),
|
||||
'form_product_description' => $product_post->post_content,
|
||||
'form_product_image_id' => $form_product_image_id,
|
||||
'form_product_image_src' => wp_get_attachment_image_url( $form_product_image_id, 'thumbnail' ),
|
||||
'form_product_currency' => get_post_meta( $product_post_id, 'spay_currency', true ),
|
||||
'form_product_price' => get_post_meta( $product_post_id, 'spay_price', true ),
|
||||
'form_product_multiple' => get_post_meta( $product_post_id, 'spay_multiple', true ) || '0',
|
||||
'form_product_email' => get_post_meta( $product_post_id, 'spay_email', true ),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->defaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a Track event and bump a MC stat.
|
||||
*
|
||||
* @param string $stat_name - the name of the stat.
|
||||
* @param string $event_action - the action we're recording.
|
||||
* @param array $event_properties - proprties of the event.
|
||||
*/
|
||||
private function record_event( $stat_name, $event_action, $event_properties = array() ) {
|
||||
$current_user = wp_get_current_user();
|
||||
|
||||
// `bumps_stats_extra` only exists on .com
|
||||
if ( function_exists( 'bump_stats_extras' ) && function_exists( 'require_lib' ) ) {
|
||||
require_lib( 'tracks/client' );
|
||||
tracks_record_event( $current_user, 'simple_payments_button_' . $event_action, $event_properties );
|
||||
/** This action is documented in modules/widgets/social-media-icons.php */
|
||||
do_action( 'jetpack_bump_stats_extra', 'simple_payments', $stat_name );
|
||||
return;
|
||||
}
|
||||
|
||||
$tracking = new Tracking();
|
||||
$tracking->tracks_record_event( $current_user, 'jetpack_wpa_simple_payments_button_' . $event_action, $event_properties );
|
||||
|
||||
if ( class_exists( 'Jetpack' ) ) {
|
||||
$jetpack = Jetpack::init();
|
||||
// $jetpack->stat automatically prepends the stat group with 'jetpack-'
|
||||
$jetpack->stat( 'simple_payments', $stat_name );
|
||||
$jetpack->do_stats( 'server_side' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved.
|
||||
*
|
||||
* @see WP_Widget::update()
|
||||
*
|
||||
* @param array $new_instance Values just sent to be saved.
|
||||
* @param array $old_instance Previously saved values from database.
|
||||
*
|
||||
* @return array Updated safe values to be saved.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ) {
|
||||
$defaults = $this->defaults();
|
||||
// do not overrite `product_post_id` for `$new_instance` with the defaults.
|
||||
$new_instance = wp_parse_args( $new_instance, array_diff_key( $defaults, array( 'product_post_id' => 0 ) ) );
|
||||
$old_instance = wp_parse_args( $old_instance, $defaults );
|
||||
|
||||
$required_widget_props = array(
|
||||
'title' => $this->get_latest_field_value( $new_instance, $old_instance, 'title' ),
|
||||
'product_post_id' => $this->get_latest_field_value( $new_instance, $old_instance, 'product_post_id' ),
|
||||
'form_action' => $this->get_latest_field_value( $new_instance, $old_instance, 'form_action' ),
|
||||
);
|
||||
|
||||
if ( strcmp( $new_instance['form_action'], $old_instance['form_action'] ) !== 0 ) {
|
||||
if ( 'edit' === $new_instance['form_action'] ) {
|
||||
return array_merge( $this->get_product_from_post( (int) $old_instance['product_post_id'] ), $required_widget_props );
|
||||
}
|
||||
|
||||
if ( 'clear' === $new_instance['form_action'] ) {
|
||||
return array_merge( $this->defaults(), $required_widget_props );
|
||||
}
|
||||
}
|
||||
|
||||
$form_product_image_id = (int) $new_instance['form_product_image_id'];
|
||||
|
||||
$form_product_email = ! empty( $new_instance['form_product_email'] )
|
||||
? sanitize_text_field( $new_instance['form_product_email'] )
|
||||
: $defaults['form_product_email'];
|
||||
|
||||
return array_merge(
|
||||
$required_widget_props,
|
||||
array(
|
||||
'form_product_id' => (int) $new_instance['form_product_id'],
|
||||
'form_product_title' => sanitize_text_field( $new_instance['form_product_title'] ),
|
||||
'form_product_description' => sanitize_text_field( $new_instance['form_product_description'] ),
|
||||
'form_product_image_id' => $form_product_image_id,
|
||||
'form_product_image_src' => wp_get_attachment_image_url( $form_product_image_id, 'thumbnail' ),
|
||||
'form_product_currency' => sanitize_text_field( $new_instance['form_product_currency'] ),
|
||||
'form_product_price' => sanitize_text_field( $new_instance['form_product_price'] ),
|
||||
'form_product_multiple' => sanitize_text_field( $new_instance['form_product_multiple'] ),
|
||||
'form_product_email' => $form_product_email,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form.
|
||||
*
|
||||
* @see WP_Widget::form()
|
||||
*
|
||||
* @html-template-var array $instance
|
||||
* @html-template-var WP_Post[] $product_posts
|
||||
*
|
||||
* @param array $instance Previously saved values from database.
|
||||
* @return string|void
|
||||
*/
|
||||
public function form( $instance ) {
|
||||
if ( ! Simple_Payments::is_enabled_jetpack_simple_payments() ) {
|
||||
require __DIR__ . '/simple-payments/admin-warning.php';
|
||||
return;
|
||||
}
|
||||
|
||||
$instance = wp_parse_args( $instance, $this->defaults() );
|
||||
|
||||
$product_posts = get_posts( // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
array(
|
||||
'numberposts' => 100,
|
||||
'orderby' => 'date',
|
||||
'post_type' => Simple_Payments::$post_type_product,
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
|
||||
require __DIR__ . '/simple-payments/form.php';
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Display the Pay with PayPal Form.
|
||||
*
|
||||
* @html-template Automattic\Jetpack\Paypal_Payments\Widgets\Simple_Payments_Widget::form
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
?>
|
||||
<div class='jetpack-simple-payments-disabled-error'>
|
||||
<p>
|
||||
<?php
|
||||
/**
|
||||
* Show error and help if Pay with PayPal is disabled.
|
||||
*
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
$support_url = ( defined( 'IS_WPCOM' ) && IS_WPCOM )
|
||||
? 'https://wordpress.com/support/pay-with-paypal/'
|
||||
: 'https://jetpack.com/support/pay-with-paypal/';
|
||||
printf(
|
||||
wp_kses(
|
||||
// translators: variable is a link to the support page.
|
||||
__( 'Your plan doesn\'t include Pay with PayPal. <a href="%s" rel="noopener noreferrer" target="_blank">Learn more and upgrade</a>.', 'jetpack-paypal-payments' ),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
'rel' => array(),
|
||||
'target' => array(),
|
||||
),
|
||||
)
|
||||
),
|
||||
esc_url( $support_url )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
.widget-content .jetpack-simple-payments,
|
||||
.widget-content .jetpack-simple-payments-form {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-disabled-error {
|
||||
background: #fff;
|
||||
border-left: 4px solid #dc3232;
|
||||
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
|
||||
margin: 5px 0 15px;
|
||||
padding: 1px 12px;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-form .invalid {
|
||||
border: 1px solid #d63638;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-form .cost label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-image-fieldset {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-image-fieldset .placeholder {
|
||||
border: 1px dashed #c3c4c7;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
padding: 9px 0;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin: 4px 0 1em;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-image {
|
||||
max-width: 100%;
|
||||
margin-top: 4px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-image img {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px dashed #c3c4c7;
|
||||
padding: 4px;
|
||||
height: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-image img:hover {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-form .field-currency {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-form .field-price {
|
||||
display: inline-block;
|
||||
line-height: 20px;
|
||||
width: 58%;
|
||||
}
|
||||
|
||||
.widget-content .jetpack-simple-payments-form .alignleft button,
|
||||
.widget-content .jetpack-simple-payments-form .alignright span {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.widget-content .button-link:disabled,
|
||||
.widget-content .button-link:hover[disabled] {
|
||||
color: #a7aaad;
|
||||
}
|
||||
+536
@@ -0,0 +1,536 @@
|
||||
/* global jpSimplePaymentsStrings, wp, jQuery, _ */
|
||||
/* eslint no-var: 0, quote-props: 0 */
|
||||
|
||||
( function ( api, wp, $ ) {
|
||||
var $document = $( document );
|
||||
|
||||
$document.ready( function () {
|
||||
$document.on( 'widget-added', function ( event, widgetContainer ) {
|
||||
if ( widgetContainer.is( '[id*="jetpack_simple_payments_widget"]' ) ) {
|
||||
initWidget( widgetContainer );
|
||||
}
|
||||
} );
|
||||
|
||||
$document.on( 'widget-synced widget-updated', function ( event, widgetContainer ) {
|
||||
//this fires for all widgets, this prevent errors for non SP widgets
|
||||
if ( ! widgetContainer.is( '[id*="jetpack_simple_payments_widget"]' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
syncProductLists();
|
||||
|
||||
var widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
|
||||
|
||||
enableFormActions( widgetForm );
|
||||
|
||||
updateProductImage( widgetForm );
|
||||
} );
|
||||
} );
|
||||
|
||||
/**
|
||||
* Initialize the widget with event handlers
|
||||
*
|
||||
* @param {jQuery} widgetContainer - The jQuery object containing the widget container
|
||||
*/
|
||||
function initWidget( widgetContainer ) {
|
||||
var widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
|
||||
|
||||
//Add New Button
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-add-product' )
|
||||
.on( 'click', showAddNewForm( widgetForm ) );
|
||||
//Edit Button
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-edit-product' )
|
||||
.on( 'click', showEditForm( widgetForm ) );
|
||||
//Select an Image
|
||||
widgetForm
|
||||
.find(
|
||||
'.jetpack-simple-payments-image-fieldset .placeholder, .jetpack-simple-payments-image > img'
|
||||
)
|
||||
.on( 'click', selectImage( widgetForm ) );
|
||||
//Remove Image Button
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-remove-image' )
|
||||
.on( 'click', removeImage( widgetForm ) );
|
||||
//Save Product button
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-save-product' )
|
||||
.on( 'click', saveChanges( widgetForm ) );
|
||||
//Cancel Button
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-cancel-form' )
|
||||
.on( 'click', clearForm( widgetForm ) );
|
||||
//Delete Selected Product
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-delete-product' )
|
||||
.on( 'click', deleteProduct( widgetForm ) );
|
||||
//Input, Select and Checkbox change
|
||||
widgetForm.find( 'select, input, textarea, checkbox' ).on(
|
||||
'change input propertychange',
|
||||
_.debounce( function () {
|
||||
disableFormActions( widgetForm );
|
||||
}, 250 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the product lists
|
||||
*/
|
||||
function syncProductLists() {
|
||||
var request = wp.ajax.post( 'customize-jetpack-simple-payments-buttons-get', {
|
||||
'customize-jetpack-simple-payments-nonce':
|
||||
api.settings.nonce[ 'customize-jetpack-simple-payments' ],
|
||||
customize_changeset_uuid: api.settings.changeset.uuid,
|
||||
} );
|
||||
|
||||
request.done( function ( data ) {
|
||||
var selectedProduct = 0;
|
||||
|
||||
$( document )
|
||||
.find( 'select.jetpack-simple-payments-products' )
|
||||
.each( function ( index, select ) {
|
||||
var $select = $( select );
|
||||
selectedProduct = $select.val();
|
||||
|
||||
$select.find( 'option' ).remove();
|
||||
$select.append(
|
||||
$.map( data, function ( product ) {
|
||||
return $( '<option>', { value: product.ID, text: product.post_title } );
|
||||
} )
|
||||
);
|
||||
$select.val( selectedProduct );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form and disable related controls
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
*/
|
||||
function showForm( widgetForm ) {
|
||||
//reset validations
|
||||
widgetForm.find( '.invalid' ).removeClass( 'invalid' );
|
||||
//disable widget title and product selector
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-widget-title' )
|
||||
.add( '.jetpack-simple-payments-products' )
|
||||
//disable add and edit buttons
|
||||
.add( '.jetpack-simple-payments-add-product' )
|
||||
.add( '.jetpack-simple-payments-edit-product' )
|
||||
//disable save, delete and cancel until the widget update event is fired
|
||||
.add( '.jetpack-simple-payments-save-product' )
|
||||
.add( '.jetpack-simple-payments-cancel-form' )
|
||||
.add( '.jetpack-simple-payments-delete-product' )
|
||||
.attr( 'disabled', 'disabled' );
|
||||
//show form
|
||||
widgetForm.find( '.jetpack-simple-payments-form' ).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the form and enable related controls
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
*/
|
||||
function hideForm( widgetForm ) {
|
||||
//enable widget title and product selector
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-widget-title' )
|
||||
.add( '.jetpack-simple-payments-products' )
|
||||
.removeAttr( 'disabled' );
|
||||
//hide the form
|
||||
widgetForm.find( '.jetpack-simple-payments-form' ).hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the form action
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
* @param {string} action - The action to set ('add', 'edit', or 'clear')
|
||||
*/
|
||||
function changeFormAction( widgetForm, action ) {
|
||||
widgetForm.find( '.jetpack-simple-payments-form-action' ).val( action ).change();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the add new product form
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
* @return {Function} Event handler function
|
||||
*/
|
||||
function showAddNewForm( widgetForm ) {
|
||||
return function ( event ) {
|
||||
event.preventDefault();
|
||||
|
||||
showForm( widgetForm );
|
||||
changeFormAction( widgetForm, 'add' );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the edit product form
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
* @return {Function} Event handler function
|
||||
*/
|
||||
function showEditForm( widgetForm ) {
|
||||
return function ( event ) {
|
||||
event.preventDefault();
|
||||
|
||||
showForm( widgetForm );
|
||||
changeFormAction( widgetForm, 'edit' );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the form and reset state
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
* @return {Function} Event handler function
|
||||
*/
|
||||
function clearForm( widgetForm ) {
|
||||
return function ( event ) {
|
||||
event.preventDefault();
|
||||
|
||||
hideForm( widgetForm );
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-add-product, .jetpack-simple-payments-edit-product' )
|
||||
.attr( 'disabled', 'disabled' );
|
||||
changeFormAction( widgetForm, 'clear' );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable form action buttons based on current state
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
*/
|
||||
function enableFormActions( widgetForm ) {
|
||||
var isFormVisible = widgetForm.find( '.jetpack-simple-payments-form' ).is( ':visible' );
|
||||
var isProductSelectVisible = widgetForm
|
||||
.find( '.jetpack-simple-payments-products' )
|
||||
.is( ':visible' ); //areProductsVisible ?
|
||||
var isEdit = widgetForm.find( '.jetpack-simple-payments-form-action' ).val() === 'edit';
|
||||
|
||||
if ( isFormVisible ) {
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-save-product' )
|
||||
.add( '.jetpack-simple-payments-cancel-form' )
|
||||
.removeAttr( 'disabled' );
|
||||
} else {
|
||||
widgetForm.find( '.jetpack-simple-payments-add-product' ).removeAttr( 'disabled' );
|
||||
}
|
||||
|
||||
if ( isFormVisible && isEdit ) {
|
||||
widgetForm.find( '.jetpack-simple-payments-delete-product' ).removeAttr( 'disabled' );
|
||||
}
|
||||
|
||||
if ( isProductSelectVisible && ! isFormVisible ) {
|
||||
widgetForm.find( '.jetpack-simple-payments-edit-product' ).removeAttr( 'disabled' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable all form action buttons
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
*/
|
||||
function disableFormActions( widgetForm ) {
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-add-product' )
|
||||
.add( '.jetpack-simple-payments-edit-product' )
|
||||
.add( '.jetpack-simple-payments-save-product' )
|
||||
.add( '.jetpack-simple-payments-cancel-form' )
|
||||
.add( '.jetpack-simple-payments-delete-product' )
|
||||
.attr( 'disabled', 'disabled' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle image selection
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
* @return {Function} Event handler function
|
||||
*/
|
||||
function selectImage( widgetForm ) {
|
||||
return function ( event ) {
|
||||
event.preventDefault();
|
||||
|
||||
var imageContainer = widgetForm.find( '.jetpack-simple-payments-image' );
|
||||
|
||||
var mediaFrame = new wp.media.view.MediaFrame.Select( {
|
||||
title: 'Choose Product Image',
|
||||
multiple: false,
|
||||
library: { type: 'image' },
|
||||
button: { text: 'Choose Image' },
|
||||
} );
|
||||
|
||||
mediaFrame.on( 'select', function () {
|
||||
var selection = mediaFrame.state().get( 'selection' ).first().toJSON();
|
||||
//hide placeholder
|
||||
widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder' ).hide();
|
||||
|
||||
//load image from media library
|
||||
imageContainer.find( 'img' ).attr( 'src', selection.url ).show();
|
||||
|
||||
//show image and remove button
|
||||
widgetForm.find( '.jetpack-simple-payments-image' ).show();
|
||||
|
||||
//set hidden field for the selective refresh
|
||||
widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val( selection.id ).change();
|
||||
} );
|
||||
|
||||
mediaFrame.open();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle image removal
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
* @return {Function} Event handler function
|
||||
*/
|
||||
function removeImage( widgetForm ) {
|
||||
return function ( event ) {
|
||||
event.preventDefault();
|
||||
|
||||
//show placeholder
|
||||
widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder' ).show();
|
||||
|
||||
//hide image and remove button
|
||||
widgetForm.find( '.jetpack-simple-payments-image' ).hide();
|
||||
|
||||
//set hidden field for the selective refresh
|
||||
widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val( '' ).change();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the product image display
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
*/
|
||||
function updateProductImage( widgetForm ) {
|
||||
var newImageId = parseInt(
|
||||
widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val(),
|
||||
10
|
||||
);
|
||||
var newImageSrc = widgetForm.find( '.jetpack-simple-payments-form-image-src' ).val();
|
||||
|
||||
var placeholder = widgetForm.find( '.jetpack-simple-payments-image-fieldset .placeholder' );
|
||||
var image = widgetForm.find( '.jetpack-simple-payments-image > img' );
|
||||
var imageControls = widgetForm.find( '.jetpack-simple-payments-image' );
|
||||
|
||||
if ( newImageId && newImageSrc ) {
|
||||
image.attr( 'src', newImageSrc );
|
||||
placeholder.hide();
|
||||
imageControls.show();
|
||||
} else {
|
||||
placeholder.show();
|
||||
image.removeAttr( 'src' );
|
||||
imageControls.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the number of decimal places in a number string
|
||||
*
|
||||
* @param {string} number - The number string to check
|
||||
* @return {number|null} The number of decimal places or null if invalid
|
||||
*/
|
||||
function decimalPlaces( number ) {
|
||||
var parts = number.split( '.' );
|
||||
if ( parts.length > 2 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parts[ 1 ] ? parts[ 1 ].length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the form fields
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
* @return {boolean} Whether the form is valid
|
||||
*/
|
||||
function isFormValid( widgetForm ) {
|
||||
widgetForm.find( '.invalid' ).removeClass( 'invalid' );
|
||||
|
||||
var errors = false;
|
||||
|
||||
var postTitle = widgetForm.find( '.jetpack-simple-payments-form-product-title' ).val();
|
||||
if ( ! postTitle ) {
|
||||
widgetForm.find( '.jetpack-simple-payments-form-product-title' ).addClass( 'invalid' );
|
||||
errors = true;
|
||||
}
|
||||
|
||||
var productPrice = widgetForm.find( '.jetpack-simple-payments-form-product-price' ).val();
|
||||
if ( ! productPrice || isNaN( productPrice ) || parseFloat( productPrice ) <= 0 ) {
|
||||
widgetForm.find( '.jetpack-simple-payments-form-product-price' ).addClass( 'invalid' );
|
||||
errors = true;
|
||||
}
|
||||
|
||||
// Japan's Yen is the only supported currency with a zero decimal precision.
|
||||
var precision =
|
||||
widgetForm.find( '.jetpack-simple-payments-form-product-currency' ).val() === 'JPY' ? 0 : 2;
|
||||
var priceDecimalPlaces = decimalPlaces( productPrice );
|
||||
if ( priceDecimalPlaces === null || priceDecimalPlaces > precision ) {
|
||||
widgetForm.find( '.jetpack-simple-payments-form-product-price' ).addClass( 'invalid' );
|
||||
errors = true;
|
||||
}
|
||||
|
||||
var productEmail = widgetForm.find( '.jetpack-simple-payments-form-product-email' ).val();
|
||||
var isProductEmailValid =
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/^((([a-z]|\d|[!#$%&'*+\-/=?^_`{|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#$%&'*+\-/=?^_`{|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(
|
||||
productEmail
|
||||
);
|
||||
if ( ! productEmail || ! isProductEmailValid ) {
|
||||
widgetForm.find( '.jetpack-simple-payments-form-product-email' ).addClass( 'invalid' );
|
||||
errors = true;
|
||||
}
|
||||
|
||||
return ! errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product changes
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
* @return {Function} Event handler function
|
||||
*/
|
||||
function saveChanges( widgetForm ) {
|
||||
return function ( event ) {
|
||||
event.preventDefault();
|
||||
|
||||
if ( ! isFormValid( widgetForm ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var productPostId = widgetForm.find( '.jetpack-simple-payments-form-product-id' ).val();
|
||||
|
||||
disableFormActions( widgetForm );
|
||||
|
||||
widgetForm.find( '.spinner' ).show();
|
||||
|
||||
var request = wp.ajax.post( 'customize-jetpack-simple-payments-button-save', {
|
||||
'customize-jetpack-simple-payments-nonce':
|
||||
api.settings.nonce[ 'customize-jetpack-simple-payments' ],
|
||||
customize_changeset_uuid: api.settings.changeset.uuid,
|
||||
params: {
|
||||
product_post_id: productPostId,
|
||||
post_title: widgetForm.find( '.jetpack-simple-payments-form-product-title' ).val(),
|
||||
post_content: widgetForm
|
||||
.find( '.jetpack-simple-payments-form-product-description' )
|
||||
.val(),
|
||||
image_id: widgetForm.find( '.jetpack-simple-payments-form-image-id' ).val(),
|
||||
currency: widgetForm.find( '.jetpack-simple-payments-form-product-currency' ).val(),
|
||||
price: widgetForm.find( '.jetpack-simple-payments-form-product-price' ).val(),
|
||||
multiple: widgetForm
|
||||
.find( '.jetpack-simple-payments-form-product-multiple' )
|
||||
.is( ':checked' )
|
||||
? 1
|
||||
: 0,
|
||||
email: widgetForm.find( '.jetpack-simple-payments-form-product-email' ).val(),
|
||||
},
|
||||
} );
|
||||
|
||||
request.done( function ( data ) {
|
||||
var select = widgetForm.find( 'select.jetpack-simple-payments-products' );
|
||||
var productOption = select.find( 'option[value="' + productPostId + '"]' );
|
||||
|
||||
if ( productOption.length > 0 ) {
|
||||
productOption.text( data.product_post_title );
|
||||
} else {
|
||||
select.append(
|
||||
$( '<option>', {
|
||||
value: data.product_post_id,
|
||||
text: data.product_post_title,
|
||||
} )
|
||||
);
|
||||
select.val( data.product_post_id ).change();
|
||||
}
|
||||
|
||||
widgetForm.find( '.jetpack-simple-payments-products-fieldset' ).show();
|
||||
widgetForm.find( '.jetpack-simple-payments-products-warning' ).hide();
|
||||
|
||||
changeFormAction( widgetForm, 'clear' );
|
||||
hideForm( widgetForm );
|
||||
} );
|
||||
|
||||
request.fail( function ( data ) {
|
||||
var validCodes = {
|
||||
post_title: 'product-title',
|
||||
price: 'product-price',
|
||||
email: 'product-email',
|
||||
};
|
||||
|
||||
data.forEach( function ( item ) {
|
||||
if ( Object.hasOwn( validCodes, item.code ) ) {
|
||||
widgetForm
|
||||
.find( '.jetpack-simple-payments-form-' + validCodes[ item.code ] )
|
||||
.addClass( 'invalid' );
|
||||
}
|
||||
} );
|
||||
|
||||
enableFormActions( widgetForm );
|
||||
} );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a product
|
||||
*
|
||||
* @param {jQuery} widgetForm - The jQuery object containing the widget form
|
||||
* @return {Function} Event handler function
|
||||
*/
|
||||
function deleteProduct( widgetForm ) {
|
||||
return function ( event ) {
|
||||
event.preventDefault();
|
||||
|
||||
// eslint-disable-next-line no-alert
|
||||
if ( ! confirm( jpSimplePaymentsStrings.deleteConfirmation ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var formProductId = parseInt(
|
||||
widgetForm.find( '.jetpack-simple-payments-form-product-id' ).val(),
|
||||
10
|
||||
);
|
||||
if ( ! formProductId ) {
|
||||
return;
|
||||
}
|
||||
|
||||
disableFormActions( widgetForm );
|
||||
|
||||
widgetForm.find( '.spinner' ).show();
|
||||
|
||||
var request = wp.ajax.post( 'customize-jetpack-simple-payments-button-delete', {
|
||||
'customize-jetpack-simple-payments-nonce':
|
||||
api.settings.nonce[ 'customize-jetpack-simple-payments' ],
|
||||
customize_changeset_uuid: api.settings.changeset.uuid,
|
||||
params: {
|
||||
product_post_id: formProductId,
|
||||
},
|
||||
} );
|
||||
|
||||
request.done( function () {
|
||||
var productList = widgetForm.find( 'select.jetpack-simple-payments-products' )[ 0 ];
|
||||
productList.remove( productList.selectedIndex );
|
||||
productList.dispatchEvent( new Event( 'change' ) );
|
||||
|
||||
if ( $( productList ).has( 'option' ).length === 0 ) {
|
||||
//hide products select and label
|
||||
widgetForm.find( '.jetpack-simple-payments-products-fieldset' ).hide();
|
||||
//show empty products list warning
|
||||
widgetForm.find( '.jetpack-simple-payments-products-warning' ).show();
|
||||
}
|
||||
|
||||
changeFormAction( widgetForm, 'clear' );
|
||||
hideForm( widgetForm );
|
||||
} );
|
||||
};
|
||||
}
|
||||
} )( wp.customize, wp, jQuery );
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
/**
|
||||
* Display the Pay with PayPal Form.
|
||||
*
|
||||
* @html-template Automattic\Jetpack\Paypal_Payments\Widgets\Simple_Payments_Widget::form
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- HTML template, let Phan handle it.
|
||||
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||
<?php esc_html_e( 'Widget Title', 'jetpack-paypal-payments' ); ?>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="widefat jetpack-simple-payments-widget-title"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||
value="<?php echo esc_attr( $instance['title'] ); ?>" />
|
||||
</p>
|
||||
<p class="jetpack-simple-payments-products-fieldset"
|
||||
<?php
|
||||
if ( empty( $product_posts ) ) {
|
||||
echo 'style="display:none;"';
|
||||
}
|
||||
?>
|
||||
>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'product_post_id' ) ); ?>">
|
||||
<?php esc_html_e( 'Select a Pay with PayPal button:', 'jetpack-paypal-payments' ); ?>
|
||||
</label>
|
||||
<select
|
||||
class="widefat jetpack-simple-payments-products"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'product_post_id' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'product_post_id' ) ); ?>">
|
||||
<?php foreach ( $product_posts as $product_post ) { ?>
|
||||
<option value="<?php echo (int) $product_post->ID; ?>" <?php selected( (int) $instance['product_post_id'], $product_post->ID ); ?>>
|
||||
<?php echo esc_attr( get_the_title( $product_post ) ); ?>
|
||||
</option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</p>
|
||||
<?php if ( is_customize_preview() ) { ?>
|
||||
<p class="jetpack-simple-payments-products-warning"
|
||||
<?php
|
||||
if ( ! empty( $product_posts ) ) {
|
||||
echo 'style="display:none;"';
|
||||
}
|
||||
?>
|
||||
>
|
||||
<?php esc_html_e( "Looks like you don't have any products. You can create one using the Add New button below.", 'jetpack-paypal-payments' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<div class="alignleft">
|
||||
<button class="button jetpack-simple-payments-edit-product" <?php disabled( empty( $product_posts ), true ); ?>>
|
||||
<?php esc_html_e( 'Edit Selected', 'jetpack-paypal-payments' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="alignright">
|
||||
<button class="button jetpack-simple-payments-add-product"><?php esc_html_e( 'Add New', 'jetpack-paypal-payments' ); ?></button>
|
||||
</div>
|
||||
<br class="clear">
|
||||
</p>
|
||||
<hr />
|
||||
<div class="jetpack-simple-payments-form" style="display: none;">
|
||||
<input
|
||||
type="hidden"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_action' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_action' ) ); ?>"
|
||||
value="<?php echo esc_attr( $instance['form_action'] ); ?>"
|
||||
class="jetpack-simple-payments-form-action" />
|
||||
<input
|
||||
type="hidden"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_product_id' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_product_id' ) ); ?>"
|
||||
value="<?php echo esc_attr( $instance['form_product_id'] ); ?>"
|
||||
class="jetpack-simple-payments-form-product-id" />
|
||||
<input
|
||||
type="hidden"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_product_image_id' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_product_image_id' ) ); ?>"
|
||||
value="<?php echo esc_attr( $instance['form_product_image_id'] ); ?>"
|
||||
class="jetpack-simple-payments-form-image-id" />
|
||||
<input
|
||||
type="hidden"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_product_image_src' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_product_image_src' ) ); ?>"
|
||||
value="<?php echo esc_attr( $instance['form_product_image_src'] ); ?>"
|
||||
class="jetpack-simple-payments-form-image-src" />
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'form_product_title' ) ); ?>">
|
||||
<?php esc_html_e( 'What is this payment for?', 'jetpack-paypal-payments' ); ?>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="widefat field-title jetpack-simple-payments-form-product-title"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_product_title' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_product_title' ) ); ?>"
|
||||
value="<?php echo esc_attr( $instance['form_product_title'] ); ?>" />
|
||||
<br />
|
||||
<small>
|
||||
<?php esc_html_e( 'For example: event tickets, charitable donations, training courses, coaching fees, etc.', 'jetpack-paypal-payments' ); ?>
|
||||
</small>
|
||||
</p>
|
||||
<div class="jetpack-simple-payments-image-fieldset">
|
||||
<label><?php esc_html_e( 'Product image', 'jetpack-paypal-payments' ); ?></label>
|
||||
<div class="placeholder"
|
||||
<?php
|
||||
if ( ! empty( $instance['form_product_image_id'] ) ) {
|
||||
echo 'style="display:none;"';
|
||||
}
|
||||
?>
|
||||
>
|
||||
<?php esc_html_e( 'Select an image', 'jetpack-paypal-payments' ); ?>
|
||||
</div>
|
||||
<div class="jetpack-simple-payments-image"
|
||||
<?php
|
||||
if ( empty( $instance['form_product_image_id'] ) ) {
|
||||
echo 'style="display:none;"';
|
||||
}
|
||||
?>
|
||||
>
|
||||
<img src="<?php echo esc_url( $instance['form_product_image_src'] ); ?>" />
|
||||
<button class="button jetpack-simple-payments-remove-image"><?php esc_html_e( 'Remove image', 'jetpack-paypal-payments' ); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'form_product_description' ) ); ?>">
|
||||
<?php esc_html_e( 'Description', 'jetpack-paypal-payments' ); ?>
|
||||
</label>
|
||||
<textarea
|
||||
class="field-description widefat jetpack-simple-payments-form-product-description"
|
||||
rows=5
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_product_description' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_product_description' ) ); ?>"><?php echo esc_textarea( $instance['form_product_description'] ); ?></textarea>
|
||||
</p>
|
||||
<p class="cost">
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'form_product_price' ) ); ?>">
|
||||
<?php esc_html_e( 'Price', 'jetpack-paypal-payments' ); ?>
|
||||
</label>
|
||||
<select
|
||||
class="field-currency widefat jetpack-simple-payments-form-product-currency"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_product_currency' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_product_currency' ) ); ?>">
|
||||
<?php
|
||||
foreach ( Automattic\Jetpack\Paypal_Payments\Widgets\Simple_Payments_Widget::$supported_currency_list as $code => $currency ) {
|
||||
?>
|
||||
<option value="<?php echo esc_attr( $code ); ?>"<?php selected( $instance['form_product_currency'], $code ); ?>>
|
||||
<?php echo esc_html( "$code $currency" ); ?>
|
||||
</option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
class="field-price widefat jetpack-simple-payments-form-product-price"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_product_price' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_product_price' ) ); ?>"
|
||||
value="<?php echo esc_attr( $instance['form_product_price'] ); ?>"
|
||||
placeholder="1.00" />
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
class="field-multiple jetpack-simple-payments-form-product-multiple"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_product_multiple' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_product_multiple' ) ); ?>"
|
||||
type="checkbox"
|
||||
value="1"
|
||||
<?php checked( $instance['form_product_multiple'], '1' ); ?> />
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'form_product_multiple' ) ); ?>">
|
||||
<?php esc_html_e( 'Allow people to buy more than one item at a time.', 'jetpack-paypal-payments' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'form_product_email' ) ); ?>">
|
||||
<?php esc_html_e( 'Email', 'jetpack-paypal-payments' ); ?>
|
||||
</label>
|
||||
<input
|
||||
class="field-email widefat jetpack-simple-payments-form-product-email"
|
||||
id="<?php echo esc_attr( $this->get_field_id( 'form_product_email' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'form_product_email' ) ); ?>"
|
||||
type="email"
|
||||
value="<?php echo esc_attr( $instance['form_product_email'] ); ?>" />
|
||||
<small>
|
||||
<?php
|
||||
printf(
|
||||
wp_kses(
|
||||
/* Translators: placeholders are a link to Paypal website and a target attribute. */
|
||||
__( 'This is where PayPal will send your money. To claim a payment, you\'ll need a <a href="%1$s" %2$s>PayPal account</a> connected to a bank account.', 'jetpack-paypal-payments' ),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
'target' => array(),
|
||||
),
|
||||
)
|
||||
),
|
||||
'https://paypal.com',
|
||||
'target="_blank"'
|
||||
);
|
||||
?>
|
||||
</small>
|
||||
</p>
|
||||
<p>
|
||||
<div class="alignleft">
|
||||
<button type="button" class="button-link button-link-delete jetpack-simple-payments-delete-product">
|
||||
<?php esc_html_e( 'Delete Product', 'jetpack-paypal-payments' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="alignright">
|
||||
<button name="<?php echo esc_attr( $this->get_field_name( 'save' ) ); ?>" class="button jetpack-simple-payments-save-product"><?php esc_html_e( 'Save', 'jetpack-paypal-payments' ); ?></button>
|
||||
<span> | <button type="button" class="button-link jetpack-simple-payments-cancel-form"><?php esc_html_e( 'Cancel', 'jetpack-paypal-payments' ); ?></button></span>
|
||||
</div>
|
||||
<br class="clear">
|
||||
</p>
|
||||
<hr />
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<p class="jetpack-simple-payments-products-warning">
|
||||
<?php
|
||||
printf(
|
||||
wp_kses(
|
||||
/* Translators: placeholder is a link to the customizer. */
|
||||
__( 'This widget adds a payment button of your choice to your sidebar. To create or edit the payment buttons themselves, <a href="%s">use the Customizer</a>.', 'jetpack-paypal-payments' ),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
),
|
||||
)
|
||||
),
|
||||
esc_url( add_query_arg( array( 'autofocus[panel]' => 'widgets' ), admin_url( 'customize.php' ) ) )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php } ?>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
@media screen and (min-width: 400px) {
|
||||
|
||||
.widget.jetpack-simple-payments .jetpack-simple-payments-product {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.widget.jetpack-simple-payments .jetpack-simple-payments-details {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Display the Pay with PayPal Widget.
|
||||
*
|
||||
* @html-template Automattic\Jetpack\Paypal_Payments\Widgets\Simple_Payments_Widget::widget
|
||||
* @package automattic/jetpack
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- HTML template, let Phan handle it.
|
||||
|
||||
?>
|
||||
<div class='jetpack-simple-payments-wrapper'>
|
||||
<div class='jetpack-simple-payments-product'>
|
||||
<div class='jetpack-simple-payments-product-image'
|
||||
<?php
|
||||
if ( empty( $instance['form_product_image_id'] ) ) {
|
||||
echo 'style="display:none;"';
|
||||
}
|
||||
?>
|
||||
>
|
||||
<div class='jetpack-simple-payments-image'>
|
||||
<?php echo wp_get_attachment_image( (int) $instance['form_product_image_id'], 'full' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class='jetpack-simple-payments-details'>
|
||||
<div class='jetpack-simple-payments-title'><p><?php echo esc_html( $instance['form_product_title'] ); ?></p></div>
|
||||
<div class='jetpack-simple-payments-description'><p><?php echo esc_html( $instance['form_product_description'] ); ?></p></div>
|
||||
<div class='jetpack-simple-payments-price'><p><?php echo esc_html( $instance['form_product_price'] ); ?> <?php echo esc_html( $instance['form_product_currency'] ); ?></p></div>
|
||||
<div class='jetpack-simple-payments-purchase-box'>
|
||||
<?php if ( $instance['form_product_multiple'] ) { ?>
|
||||
<div class='jetpack-simple-payments-items'>
|
||||
<input
|
||||
type='number'
|
||||
class='jetpack-simple-payments-items-number'
|
||||
value='1'
|
||||
min='1' />
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user