simple-payments.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. <?php
  2. /*
  3. * Simple Payments lets users embed a PayPal button fully integrated with wpcom to sell products on the site.
  4. * This is not a proper module yet, because not all the pieces are in place. Until everything is shipped, it can be turned
  5. * into module that can be enabled/disabled.
  6. */
  7. class Jetpack_Simple_Payments {
  8. // These have to be under 20 chars because that is CPT limit.
  9. static $post_type_order = 'jp_pay_order';
  10. static $post_type_product = 'jp_pay_product';
  11. static $shortcode = 'simple-payment';
  12. static $css_classname_prefix = 'jetpack-simple-payments';
  13. // Increase this number each time there's a change in CSS or JS to bust cache.
  14. static $version = '0.25';
  15. // Classic singleton pattern:
  16. private static $instance;
  17. private function __construct() {}
  18. static function getInstance() {
  19. if ( ! self::$instance ) {
  20. self::$instance = new self();
  21. self::$instance->register_init_hook();
  22. }
  23. return self::$instance;
  24. }
  25. private function register_scripts_and_styles() {
  26. /**
  27. * Paypal heavily discourages putting that script in your own server:
  28. * @see https://developer.paypal.com/docs/integration/direct/express-checkout/integration-jsv4/add-paypal-button/
  29. */
  30. wp_register_script( 'paypal-checkout-js', 'https://www.paypalobjects.com/api/checkout.js', array(), null, true );
  31. wp_register_script( 'paypal-express-checkout', plugins_url( '/paypal-express-checkout.js', __FILE__ ),
  32. array( 'jquery', 'paypal-checkout-js' ), self::$version );
  33. wp_register_style( 'jetpack-simple-payments', plugins_url( '/simple-payments.css', __FILE__ ), array( 'dashicons' ) );
  34. }
  35. private function register_init_hook() {
  36. add_action( 'init', array( $this, 'init_hook_action' ) );
  37. }
  38. private function register_shortcode() {
  39. add_shortcode( self::$shortcode, array( $this, 'parse_shortcode' ) );
  40. }
  41. public function init_hook_action() {
  42. add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
  43. add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
  44. $this->register_scripts_and_styles();
  45. $this->register_shortcode();
  46. $this->setup_cpts();
  47. add_filter( 'the_content', array( $this, 'remove_auto_paragraph_from_product_description' ), 0 );
  48. }
  49. function remove_auto_paragraph_from_product_description( $content ) {
  50. if ( get_post_type() === self::$post_type_product ) {
  51. remove_filter( 'the_content', 'wpautop' );
  52. }
  53. return $content;
  54. }
  55. function get_blog_id() {
  56. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
  57. return get_current_blog_id();
  58. }
  59. return Jetpack_Options::get_option( 'id' );
  60. }
  61. /**
  62. * Used to check whether Simple Payments are enabled for given site.
  63. *
  64. * @return bool True if Simple Payments are enabled, false otherwise.
  65. */
  66. function is_enabled_jetpack_simple_payments() {
  67. /**
  68. * Can be used by plugin authors to disable the conflicting output of Simple Payments.
  69. *
  70. * @since 6.3.0
  71. *
  72. * @param bool True if Simple Payments should be disabled, false otherwise.
  73. */
  74. if ( apply_filters( 'jetpack_disable_simple_payments', false ) ) {
  75. return false;
  76. }
  77. // For WPCOM sites
  78. if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'has_blog_sticker' ) ) {
  79. $site_id = $this->get_blog_id();
  80. return has_blog_sticker( 'premium-plan', $site_id ) || has_blog_sticker( 'business-plan', $site_id );
  81. }
  82. // For all Jetpack sites
  83. return Jetpack::is_active() && Jetpack::active_plan_supports( 'simple-payments');
  84. }
  85. function parse_shortcode( $attrs, $content = false ) {
  86. if ( empty( $attrs['id'] ) ) {
  87. return;
  88. }
  89. $product = get_post( $attrs['id'] );
  90. if ( ! $product || is_wp_error( $product ) ) {
  91. return;
  92. }
  93. if ( $product->post_type !== self::$post_type_product || 'trash' === $product->post_status ) {
  94. return;
  95. }
  96. // We allow for overriding the presentation labels
  97. $data = shortcode_atts( array(
  98. 'blog_id' => $this->get_blog_id(),
  99. 'dom_id' => uniqid( self::$css_classname_prefix . '-' . $product->ID . '_', true ),
  100. 'class' => self::$css_classname_prefix . '-' . $product->ID,
  101. 'title' => get_the_title( $product ),
  102. 'description' => $product->post_content,
  103. 'cta' => get_post_meta( $product->ID, 'spay_cta', true ),
  104. 'multiple' => get_post_meta( $product->ID, 'spay_multiple', true ) || '0'
  105. ), $attrs );
  106. $data['price'] = $this->format_price(
  107. get_post_meta( $product->ID, 'spay_formatted_price', true ),
  108. get_post_meta( $product->ID, 'spay_price', true ),
  109. get_post_meta( $product->ID, 'spay_currency', true ),
  110. $data
  111. );
  112. $data['id'] = $attrs['id'];
  113. if( ! wp_style_is( 'jetpack-simple-payments', 'enqueue' ) ) {
  114. wp_enqueue_style( 'jetpack-simple-payments' );
  115. }
  116. if ( ! $this->is_enabled_jetpack_simple_payments() ) {
  117. return $this->output_admin_warning( $data );
  118. }
  119. if ( ! wp_script_is( 'paypal-express-checkout', 'enqueued' ) ) {
  120. wp_enqueue_script( 'paypal-express-checkout' );
  121. }
  122. wp_add_inline_script( 'paypal-express-checkout', sprintf(
  123. "try{PaypalExpressCheckout.renderButton( '%d', '%d', '%s', '%d' );}catch(e){}",
  124. esc_js( $data['blog_id'] ),
  125. esc_js( $attrs['id'] ),
  126. esc_js( $data['dom_id'] ),
  127. esc_js( $data['multiple'] )
  128. ) );
  129. return $this->output_shortcode( $data );
  130. }
  131. function output_admin_warning( $data ) {
  132. if ( ! current_user_can( 'manage_options' ) ) {
  133. return;
  134. }
  135. $css_prefix = self::$css_classname_prefix;
  136. $support_url = ( defined( 'IS_WPCOM' ) && IS_WPCOM )
  137. ? 'https://support.wordpress.com/simple-payments/'
  138. : 'https://jetpack.com/support/simple-payment-button/';
  139. return sprintf( '
  140. <div class="%1$s">
  141. <div class="%2$s">
  142. <div class="%3$s">
  143. <div class="%4$s" id="%5$s">
  144. <p>%6$s</p>
  145. <p>%7$s</p>
  146. </div>
  147. </div>
  148. </div>
  149. </div>
  150. ',
  151. esc_attr( "{$data['class']} ${css_prefix}-wrapper" ),
  152. esc_attr( "${css_prefix}-product" ),
  153. esc_attr( "${css_prefix}-details" ),
  154. esc_attr( "${css_prefix}-purchase-message show error" ),
  155. esc_attr( "{$data['dom_id']}-message-container" ),
  156. sprintf(
  157. wp_kses(
  158. __( 'Your plan doesn\'t include Simple Payments. <a href="%s" rel="noopener noreferrer" target="_blank">Learn more and upgrade</a>.', 'jetpack' ),
  159. array( 'a' => array( 'href' => array(), 'rel' => array(), 'target' => array() ) )
  160. ),
  161. esc_url( $support_url )
  162. ),
  163. esc_html__( '(Only administrators will see this message.)', 'jetpack' )
  164. );
  165. }
  166. function output_shortcode( $data ) {
  167. $items = '';
  168. $css_prefix = self::$css_classname_prefix;
  169. if ( $data['multiple'] ) {
  170. $items = sprintf( '
  171. <div class="%1$s">
  172. <input class="%2$s" type="number" value="1" min="1" id="%3$s" />
  173. </div>
  174. ',
  175. esc_attr( "${css_prefix}-items" ),
  176. esc_attr( "${css_prefix}-items-number" ),
  177. esc_attr( "{$data['dom_id']}_number" )
  178. );
  179. }
  180. $image = "";
  181. if( has_post_thumbnail( $data['id'] ) ) {
  182. $image = sprintf( '<div class="%1$s"><div class="%2$s">%3$s</div></div>',
  183. esc_attr( "${css_prefix}-product-image" ),
  184. esc_attr( "${css_prefix}-image" ),
  185. get_the_post_thumbnail( $data['id'], 'full' )
  186. );
  187. }
  188. return sprintf( '
  189. <div class="%1$s">
  190. <div class="%2$s">
  191. %3$s
  192. <div class="%4$s">
  193. <div class="%5$s"><p>%6$s</p></div>
  194. <div class="%7$s"><p>%8$s</p></div>
  195. <div class="%9$s"><p>%10$s</p></div>
  196. <div class="%11$s" id="%12$s"></div>
  197. <div class="%13$s">
  198. %14$s
  199. <div class="%15$s" id="%16$s"></div>
  200. </div>
  201. </div>
  202. </div>
  203. </div>
  204. ',
  205. esc_attr( "{$data['class']} ${css_prefix}-wrapper" ),
  206. esc_attr( "${css_prefix}-product" ),
  207. $image,
  208. esc_attr( "${css_prefix}-details" ),
  209. esc_attr( "${css_prefix}-title" ),
  210. $data['title'],
  211. esc_attr( "${css_prefix}-description" ),
  212. $data['description'],
  213. esc_attr( "${css_prefix}-price" ),
  214. esc_html( $data['price'] ),
  215. esc_attr( "${css_prefix}-purchase-message" ),
  216. esc_attr( "{$data['dom_id']}-message-container" ),
  217. esc_attr( "${css_prefix}-purchase-box" ),
  218. $items,
  219. esc_attr( "${css_prefix}-button" ),
  220. esc_attr( "{$data['dom_id']}_button" )
  221. );
  222. }
  223. function format_price( $formatted_price, $price, $currency, $all_data ) {
  224. if ( $formatted_price ) {
  225. return $formatted_price;
  226. }
  227. return "$price $currency";
  228. }
  229. /**
  230. * Allows custom post types to be used by REST API.
  231. * @param $post_types
  232. * @see hook 'rest_api_allowed_post_types'
  233. * @return array
  234. */
  235. function allow_rest_api_types( $post_types ) {
  236. $post_types[] = self::$post_type_order;
  237. $post_types[] = self::$post_type_product;
  238. return $post_types;
  239. }
  240. function allow_sync_post_meta( $post_meta ) {
  241. return array_merge( $post_meta, array(
  242. 'spay_paypal_id',
  243. 'spay_status',
  244. 'spay_product_id',
  245. 'spay_quantity',
  246. 'spay_price',
  247. 'spay_customer_email',
  248. 'spay_currency',
  249. 'spay_cta',
  250. 'spay_email',
  251. 'spay_multiple',
  252. 'spay_formatted_price',
  253. ) );
  254. }
  255. /**
  256. * Sets up the custom post types for the module.
  257. */
  258. function setup_cpts() {
  259. /*
  260. * ORDER data structure. holds:
  261. * title = customer_name | 4xproduct_name
  262. * excerpt = customer_name + customer contact info + customer notes from paypal form
  263. * metadata:
  264. * spay_paypal_id - paypal id of transaction
  265. * spay_status
  266. * spay_product_id - post_id of bought product
  267. * spay_quantity - quantity of product
  268. * spay_price - item price at the time of purchase
  269. * spay_customer_email - customer email
  270. * ... (WIP)
  271. */
  272. $order_capabilities = array(
  273. 'edit_post' => 'edit_posts',
  274. 'read_post' => 'read_private_posts',
  275. 'delete_post' => 'delete_posts',
  276. 'edit_posts' => 'edit_posts',
  277. 'edit_others_posts' => 'edit_others_posts',
  278. 'publish_posts' => 'publish_posts',
  279. 'read_private_posts' => 'read_private_posts',
  280. );
  281. $order_args = array(
  282. 'label' => esc_html_x( 'Order', 'noun: a quantity of goods or items purchased or sold', 'jetpack' ),
  283. 'description' => esc_html__( 'Simple Payments orders', 'jetpack' ),
  284. 'supports' => array( 'custom-fields', 'excerpt' ),
  285. 'hierarchical' => false,
  286. 'public' => false,
  287. 'show_ui' => false,
  288. 'show_in_menu' => false,
  289. 'show_in_admin_bar' => false,
  290. 'show_in_nav_menus' => false,
  291. 'can_export' => true,
  292. 'has_archive' => false,
  293. 'exclude_from_search' => true,
  294. 'publicly_queryable' => false,
  295. 'rewrite' => false,
  296. 'capabilities' => $order_capabilities,
  297. 'show_in_rest' => true,
  298. );
  299. register_post_type( self::$post_type_order, $order_args );
  300. /*
  301. * PRODUCT data structure. Holds:
  302. * title - title
  303. * content - description
  304. * thumbnail - image
  305. * metadata:
  306. * spay_price - price
  307. * spay_formatted_price
  308. * spay_currency - currency code
  309. * spay_cta - text with "Buy" or other CTA
  310. * spay_email - paypal email
  311. * spay_multiple - allow for multiple items
  312. * spay_status - status. { enabled | disabled }
  313. */
  314. $product_capabilities = array(
  315. 'edit_post' => 'edit_posts',
  316. 'read_post' => 'read_private_posts',
  317. 'delete_post' => 'delete_posts',
  318. 'edit_posts' => 'edit_posts',
  319. 'edit_others_posts' => 'edit_others_posts',
  320. 'publish_posts' => 'publish_posts',
  321. 'read_private_posts' => 'read_private_posts',
  322. );
  323. $product_args = array(
  324. 'label' => esc_html__( 'Product', 'jetpack' ),
  325. 'description' => esc_html__( 'Simple Payments products', 'jetpack' ),
  326. 'supports' => array( 'title', 'editor','thumbnail', 'custom-fields', 'author' ),
  327. 'hierarchical' => false,
  328. 'public' => false,
  329. 'show_ui' => false,
  330. 'show_in_menu' => false,
  331. 'show_in_admin_bar' => false,
  332. 'show_in_nav_menus' => false,
  333. 'can_export' => true,
  334. 'has_archive' => false,
  335. 'exclude_from_search' => true,
  336. 'publicly_queryable' => false,
  337. 'rewrite' => false,
  338. 'capabilities' => $product_capabilities,
  339. 'show_in_rest' => true,
  340. );
  341. register_post_type( self::$post_type_product, $product_args );
  342. }
  343. }
  344. Jetpack_Simple_Payments::getInstance();