This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,318 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.7.6] - 2026-06-29
### Changed
- Internal updates.
## [0.7.5] - 2026-06-25
### Changed
- Update package dependencies. [#49831]
## [0.7.4] - 2026-06-22
### Changed
- Update package dependencies. [#49631] [#49691] [#49757]
## [0.7.3] - 2026-06-15
### Changed
- Update package dependencies. [#49273]
## [0.7.2] - 2026-06-08
### Security
- Simple Payments: Strip seller PayPal email (`spay_email`) from public REST responses while preserving editor read/write. [#49194]
### Changed
- Update dependencies. [#49354]
## [0.7.1] - 2026-06-01
### Changed
- Update package dependencies. [#48404]
## [0.7.0] - 2026-05-25
### Security
- Restrict REST API write access to `jp_pay_order` by using a read-only REST controller. [#48139]
### Changed
- Update package dependencies. [#48405] [#49012]
## [0.6.20] - 2026-05-19
### Changed
- Exclude development files from production builds. [#47365]
- Update package dependencies. [#48695]
## [0.6.19] - 2026-05-11
### Changed
- Components: Use Link from `@wordpress/ui` instead of ExternalLink. [#48529]
## [0.6.18] - 2026-05-04
### Changed
- Internal: No longer require automattic/jetpack-changelogger as a per-project dev dependency. [#48225]
## [0.6.17] - 2026-04-27
### Changed
- Update package dependencies. [#48302]
### Fixed
- Remove previous work that introduced an error. [#48322]
## [0.6.16] - 2026-04-20
### Changed
- Update package dependencies. [#48106]
## [0.6.15] - 2026-04-15
### Security
- Hide the creator email of the Simple Payments block from the REST endpoints. [#48090]
## [0.6.14] - 2026-04-11
### Changed
- Update package dependencies. [#47890] [#47998]
### Fixed
- PayPal Payments Button: fix escaping issue for stacked payments buttons [#47761]
## [0.6.13] - 2026-04-06
### Changed
- Update package dependencies. [#47899]
## [0.6.12] - 2026-03-30
### Changed
- Update package dependencies. [#47799]
## [0.6.11] - 2026-03-23
### Changed
- Update package dependencies. [#47684]
## [0.6.10] - 2026-03-16
### Changed
- Update dependencies. [#47472]
## [0.6.9] - 2026-03-09
### Changed
- Update package dependencies. [#47496] [#47499]
## [0.6.8] - 2026-03-02
### Changed
- Update dependencies. [#47038]
## [0.6.7] - 2026-02-26
### Changed
- Update package dependencies. [#47300]
## [0.6.6] - 2026-02-23
### Changed
- Update package dependencies. [#47173]
## [0.6.5] - 2026-02-16
### Changed
- Update package dependencies. [#47099]
### Fixed
- Compatibility: Clean up deprecated CSS. [#47067]
## [0.6.4] - 2026-02-10
### Changed
- Update dependencies. [#46931] [#47002]
## [0.6.3] - 2026-02-02
### Changed
- Update package dependencies. [#46854]
## [0.6.2] - 2026-01-26
### Changed
- Update package dependencies. [#46716]
## [0.6.1] - 2026-01-19
### Changed
- Update package dependencies. [#46552] [#46647]
## [0.6.0] - 2026-01-12
### Changed
- Gate PayPal payment buttons block behind conditional features. [#46536]
- Update package dependencies. [#46456]
## [0.5.18] - 2025-12-22
### Changed
- Update dependencies. [#46381]
## [0.5.17] - 2025-12-15
### Changed
- Replace use of confusing `esc_js` with `wp_json_encode`, or `intval` where appropriate. [#46229]
### Fixed
- Add back `#` selector to fix a broken selector for the container. [#46259]
## [0.5.16] - 2025-12-08
### Changed
- Internal updates.
## [0.5.15] - 2025-12-01
### Changed
- Update package dependencies. [#46072] [#46143]
## [0.5.14] - 2025-11-20
### Fixed
- Jetpack: Remove getIconColor functions from block icons. [#45992]
## [0.5.13] - 2025-11-18
### Changed
- Update dependencies. [#45745]
## [0.5.12] - 2025-11-17
### Changed
- Update package dependencies. [#45915] [#45958]
## [0.5.11] - 2025-11-10
### Changed
- Update dependencies. [#45745]
## [0.5.10] - 2025-11-03
### Changed
- Update dependencies. [#45664]
## [0.5.9] - 2025-10-20
### Changed
- Update dependencies. [#45488]
## [0.5.8] - 2025-10-06
### Security
- Improve PayPal SDK host validation for PayPal Payment Buttons. [#45343]
### Changed
- Update package dependencies. [#45334]
## [0.5.7] - 2025-09-29
### Changed
- Update dependencies. [#44736]
## [0.5.6] - 2025-09-22
### Changed
- Update dependencies. [#44736]
## [0.5.5] - 2025-09-19
### Changed
- Namespace PayPal SDK to minimize loading conflicts when using other PayPal blocks. [#45224]
- Update package dependencies. [#45173] [#45229]
## [0.5.4] - 2025-09-16
### Changed
- Improve robustness of PayPal payment buttons parsing [#45158]
## [0.5.3] - 2025-09-15
### Changed
- Update package dependencies. [#45127] [#45128]
## [0.5.2] - 2025-09-08
### Changed
- Update package dependencies. [#45027]
## [0.5.1] - 2025-09-01
### Changed
- Internal updates.
## [0.5.0] - 2025-08-25
### Changed
- Disallow inserting Simple Payments block via inserter. [#44724]
## [0.4.3] - 2025-08-18
### Changed
- Update dependencies. [#44736]
## [0.4.2] - 2025-08-14
### Changed
- Update package dependencies. [#44701]
## [0.4.1] - 2025-08-11
### Changed
- Update dependencies. [#44673]
- Update package dependencies. [#44677]
### Fixed
- I18n: Improve context hints in comments for translators. [#44686]
## [0.4.0] - 2025-08-04
### Changed
- Improve alignment of "Pay with PayPal" block labels. [#44560]
- Move link location to instructions. [#44585]
- Update copy of PayPal Payment Buttons block to add numbered steps. [#44564]
- Update dependencies. [#44551]
## [0.3.0] - 2025-07-28
### Changed
- Change copy and links for PayPal Payments Buttons block. [#44424]
- Clear PayPal Payment buttons block parameters when changing block type. [#44388]
- Update code entry inputs to mirror the PayPal.com UI. [#44387]
- Update PayPal Payment Buttons block copy to be simpler. [#44389]
## [0.2.0] - 2025-07-21
### Added
- Add new PayPal Payment block. [#43932]
### Changed
- Tests: Generate block test files with tab indents. [#44099]
- Update package dependencies. [#44337] [#44338] [#44356]
- Update PayPal Payment Buttons block to support rendering previews. [#44359]
## 0.1.0 - 2025-07-14
### Added
- Initial version. [#43315]
### Changed
- Simple Payments: Move Simple Payments block to PayPal Payments package. [#43413]
[0.7.6]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.7.5...v0.7.6
[0.7.5]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.7.4...v0.7.5
[0.7.4]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.7.3...v0.7.4
[0.7.3]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.7.2...v0.7.3
[0.7.2]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.20...v0.7.0
[0.6.20]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.19...v0.6.20
[0.6.19]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.18...v0.6.19
[0.6.18]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.17...v0.6.18
[0.6.17]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.16...v0.6.17
[0.6.16]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.15...v0.6.16
[0.6.15]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.14...v0.6.15
[0.6.14]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.13...v0.6.14
[0.6.13]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.12...v0.6.13
[0.6.12]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.11...v0.6.12
[0.6.11]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.10...v0.6.11
[0.6.10]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.9...v0.6.10
[0.6.9]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.8...v0.6.9
[0.6.8]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.7...v0.6.8
[0.6.7]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.6...v0.6.7
[0.6.6]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.5...v0.6.6
[0.6.5]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.4...v0.6.5
[0.6.4]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.3...v0.6.4
[0.6.3]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.2...v0.6.3
[0.6.2]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.1...v0.6.2
[0.6.1]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.18...v0.6.0
[0.5.18]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.17...v0.5.18
[0.5.17]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.16...v0.5.17
[0.5.16]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.15...v0.5.16
[0.5.15]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.14...v0.5.15
[0.5.14]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.13...v0.5.14
[0.5.13]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.12...v0.5.13
[0.5.12]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.11...v0.5.12
[0.5.11]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.10...v0.5.11
[0.5.10]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.9...v0.5.10
[0.5.9]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.8...v0.5.9
[0.5.8]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.7...v0.5.8
[0.5.7]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.6...v0.5.7
[0.5.6]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.5...v0.5.6
[0.5.5]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.4...v0.5.5
[0.5.4]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.3...v0.5.4
[0.5.3]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.2...v0.5.3
[0.5.2]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.4.3...v0.5.0
[0.4.3]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.4.2...v0.4.3
[0.4.2]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/Automattic/jetpack-paypal-payments/compare/v0.1.0...v0.2.0
@@ -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"
}
}
}
@@ -0,0 +1 @@
<?php return array('dependencies' => array('jetpack-script-data', 'jetpack-shared-stores', 'lodash', 'react', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-polyfill', 'wp-url', 'wp-widgets'), 'version' => '55440f151c75f99a4460');
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => 'a9212ea6c639cc1fd73d');
@@ -0,0 +1 @@
.jetpack-simple-payments-wrapper{margin-bottom:1.5em}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}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-price p,.jetpack-simple-payments-title 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:hsla(0,0%,100%,.7);border:2px solid #fff;border-radius:2px;box-shadow:0 0 0 1px rgba(200,215,225,.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;left:0;top:50%;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:""}.jetpack-simple-payments-purchase-message.error:before{color:#d94f4f;content:""}body .jetpack-simple-payments-wrapper .jetpack-simple-payments-purchase-message p{color:#222;margin: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;vertical-align:top;width:30%}.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%}
@@ -0,0 +1 @@
.jetpack-simple-payments-wrapper{margin-bottom:1.5em}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}body .jetpack-simple-payments-wrapper .jetpack-simple-payments-product-image .jetpack-simple-payments-image img{border:0;border-radius:0;height:auto;margin:0;max-height:100%;max-width:100%;padding:0;position:absolute;right:50%;top:50%;transform:translate(50%,-50%);width:auto}.jetpack-simple-payments-price p,.jetpack-simple-payments-title 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-left: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:hsla(0,0%,100%,.7);border:2px solid #fff;border-radius:2px;box-shadow:0 0 0 1px rgba(200,215,225,.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;right:0;top:50%;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:""}.jetpack-simple-payments-purchase-message.error:before{color:#d94f4f;content:""}body .jetpack-simple-payments-wrapper .jetpack-simple-payments-purchase-message p{color:#222;margin:0 0 .5em;padding:0 40px 0 0}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-right:1em}}.is-email .jetpack-simple-payments-product{display:table;width:100%}.is-email .jetpack-simple-payments-product-image{display:table-cell;vertical-align:top;width:30%}.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%}
@@ -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"
}
@@ -0,0 +1 @@
<?php return array('dependencies' => array('jetpack-script-data', 'jetpack-shared-stores', 'react', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-polyfill', 'wp-url'), 'version' => '555968be6281ee10476a');
@@ -0,0 +1 @@
.block-editor-block-list__layout .block-editor-block-list__block.has-warning.is-interactive>*{pointer-events:auto;-webkit-user-select:auto;-moz-user-select:auto;user-select:auto}.block-editor-block-list__layout .block-editor-block-list__block.has-warning.is-interactive:after{content:none}.wp-block-jetpack-paypal-payment-buttons .block-editor-plain-text{background:#fff!important;border:1px solid #1e1e1e!important;border-radius:2px!important;box-shadow:none!important;color:#1e1e1e!important;font-family:Menlo,Consolas,monaco,monospace!important;font-size:16px!important;padding:12px!important;width:auto}@media(min-width:600px){.wp-block-jetpack-paypal-payment-buttons .block-editor-plain-text{font-size:13px!important}}.wp-block-jetpack-paypal-payment-buttons .block-editor-plain-text:focus{border-color:var(--wp-admin-theme-color)!important;box-shadow:0 0 0 1px var(--wp-admin-theme-color)!important;outline:2px solid transparent!important}.wp-block-jetpack-paypal-payment-buttons .components-notice{align-self:normal}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.block-editor-block-list__layout .block-editor-block-list__block.has-warning.is-interactive>*{pointer-events:auto;-webkit-user-select:auto;-moz-user-select:auto;user-select:auto}.block-editor-block-list__layout .block-editor-block-list__block.has-warning.is-interactive:after{content:none}.wp-block-jetpack-paypal-payment-buttons .block-editor-plain-text{background:#fff!important;border:1px solid #1e1e1e!important;border-radius:2px!important;box-shadow:none!important;color:#1e1e1e!important;font-family:Menlo,Consolas,monaco,monospace!important;font-size:16px!important;padding:12px!important;width:auto}@media(min-width:600px){.wp-block-jetpack-paypal-payment-buttons .block-editor-plain-text{font-size:13px!important}}.wp-block-jetpack-paypal-payment-buttons .block-editor-plain-text:focus{border-color:var(--wp-admin-theme-color)!important;box-shadow:0 0 0 1px var(--wp-admin-theme-color)!important;outline:2px solid transparent!important}.wp-block-jetpack-paypal-payment-buttons .components-notice{align-self:normal}
@@ -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"
}
}
}
@@ -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;
}
}
@@ -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;
},
};
@@ -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 youre 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 cant 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 theyre 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. Dont 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 );
@@ -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;
@@ -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>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -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';
}
@@ -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 )
);
}
}
@@ -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' => '&#163;',
'decimal' => 2,
),
'JPY' => array(
'format' => '%1$s%2$s', // 1: Symbol 2: currency value
'symbol' => '&#165;',
'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' => '&#8364;',
'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
);
}
}
@@ -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();
@@ -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
);
},
};
@@ -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%;
}
@@ -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"
}
@@ -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( '&amp;', '&', $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' ) );
}
}
@@ -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>
);
}
@@ -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,
} );
@@ -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>
);
@@ -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' => '&#163;',
'JPY' => '&#165;',
'BRL' => 'R$',
'EUR' => '&#8364;',
'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';
}
}
}
@@ -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>
@@ -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;
}
@@ -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 );
@@ -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 } ?>
@@ -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;
}
}
@@ -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>