' . esc_html__( 'SSL Certificate', 'paid-memberships-pro' ) . '';
+ $ssl_certificate_link = '' . esc_html__( 'SSL Certificate', 'paid-memberships-pro' ) . ' ';
// translators: %s: Link to SSL Certificate docs.
printf( esc_html__('Your %s must be installed by your web host. Use this field to display your seal or other trusted merchant images. This field does not accept JavaScript.', 'paid-memberships-pro' ), $ssl_certificate_link );
?>
diff --git a/adminpages/reports.php b/adminpages/reports.php
index 523034c51..165b0b9c7 100644
--- a/adminpages/reports.php
+++ b/adminpages/reports.php
@@ -8,14 +8,17 @@
/**
* Load the Paid Memberships Pro dashboard-area header
*/
-require_once( dirname( __FILE__ ) . '/admin_header.php' );
+require_once( dirname( __FILE__ ) . '/admin_header.php' ); ?>
+
+
+
-
diff --git a/blocks/account-invoices-section/block.js b/blocks/account-invoices-section/block.js
index 5a74e696b..d9899f3f5 100644
--- a/blocks/account-invoices-section/block.js
+++ b/blocks/account-invoices-section/block.js
@@ -19,15 +19,24 @@
export default registerBlockType(
'pmpro/account-invoices-section',
{
- title: __( 'Membership Account: Invoices', 'paid-memberships-pro' ),
- description: __( 'Displays the member\'s invoices.', 'paid-memberships-pro' ),
- category: 'pmpro',
+ title: __( 'PMPro Page: Account Invoices', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section that displays a list of the last 5 membership invoices for the active member.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'archive',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'account', 'paid-memberships-pro' ),
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'order', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'purchases', 'paid-memberships-pro' ),
+ __( 'receipt', 'paid-memberships-pro' ),
+ __( 'user', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/account-links-section/block.js b/blocks/account-links-section/block.js
index 6ee306b74..cc4884a29 100644
--- a/blocks/account-links-section/block.js
+++ b/blocks/account-links-section/block.js
@@ -19,15 +19,24 @@
export default registerBlockType(
'pmpro/account-links-section',
{
- title: __( 'Membership Account: Links', 'paid-memberships-pro' ),
- description: __( 'Displays the member\'s member links. This block is only visible if other Add Ons or custom code have added links.', 'paid-memberships-pro' ),
- category: 'pmpro',
+ title: __( 'PMPro Page: Account Links', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section that displays custom links available for the active member only. This block is only visible if other Add Ons or custom code have added links.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'external',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'access', 'paid-memberships-pro' ),
+ __( 'account', 'paid-memberships-pro' ),
+ __( 'link', 'paid-memberships-pro' ),
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'quick link', 'paid-memberships-pro' ),
+ __( 'user', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/account-membership-section/block.js b/blocks/account-membership-section/block.js
index d1b4a9b7e..41b0f8a59 100644
--- a/blocks/account-membership-section/block.js
+++ b/blocks/account-membership-section/block.js
@@ -19,15 +19,22 @@
export default registerBlockType(
'pmpro/account-membership-section',
{
- title: __( 'Membership Account: Memberships', 'paid-memberships-pro' ),
- description: __( 'Displays the member\'s membership information.', 'paid-memberships-pro' ),
- category: 'pmpro',
+ title: __( 'PMPro Page: Account Memberships', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section to display the member\'s active membership information with links to view all membership options, update billing information, and change or cancel membership.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'groups',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'active', 'paid-memberships-pro' ),
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'purchases', 'paid-memberships-pro' ),
+ __( 'user', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/account-page/block.js b/blocks/account-page/block.js
index 4cce0ad7c..b2c1c2706 100644
--- a/blocks/account-page/block.js
+++ b/blocks/account-page/block.js
@@ -21,15 +21,29 @@
export default registerBlockType(
'pmpro/account-page',
{
- title: __( 'Membership Account Page', 'paid-memberships-pro' ),
- description: __( 'Displays the sections of the Membership Account page as selected below.', 'paid-memberships-pro' ),
- category: 'pmpro',
+ title: __( 'PMPro Page: Account (Full)', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section to display the selected sections of the Membership Account page including Memberships, Profile, Invoices, and Member Links. These sections can also be added via separate blocks.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'admin-users',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'account', 'paid-memberships-pro' ),
+ __( 'billing', 'paid-memberships-pro' ),
+ __( 'invoice', 'paid-memberships-pro' ),
+ __( 'links', 'paid-memberships-pro' ),
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'order', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'profile', 'paid-memberships-pro' ),
+ __( 'purchases', 'paid-memberships-pro' ),
+ __( 'quick link', 'paid-memberships-pro' ),
+ __( 'receipt', 'paid-memberships-pro' ),
+ __( 'user', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/account-profile-section/block.js b/blocks/account-profile-section/block.js
index 5c1ae6402..8bed94b58 100644
--- a/blocks/account-profile-section/block.js
+++ b/blocks/account-profile-section/block.js
@@ -20,15 +20,21 @@
export default registerBlockType(
'pmpro/account-profile-section',
{
- title: __( 'Membership Account: Profile', 'paid-memberships-pro' ),
- description: __( 'Displays the member\'s profile information.', 'paid-memberships-pro' ),
- category: 'pmpro',
+ title: __( 'PMPro Page: Account Profile View', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section that displays the member\'s profile as read-only information with a link to edit fields or their change password.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'admin-users',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'fields', 'paid-memberships-pro' ),
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'user', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/billing-page/block.js b/blocks/billing-page/block.js
index b5ad68cf6..7a152f3f0 100644
--- a/blocks/billing-page/block.js
+++ b/blocks/billing-page/block.js
@@ -19,15 +19,21 @@
export default registerBlockType(
'pmpro/billing-page',
{
- title: __( 'Membership Billing Page', 'paid-memberships-pro' ),
- description: __( 'Displays the member\'s billing information and allows them to update the payment method.', 'paid-memberships-pro' ),
- category: 'pmpro',
+ title: __( 'PMPro Page: Billing', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section to display the member\'s billing information. Members can update their subscription payment method from this form.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'list-view',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'credit card', 'paid-memberships-pro' ),
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'payment method', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/blocks.js b/blocks/blocks.js
index 60aa74c8e..c3b95a5d9 100644
--- a/blocks/blocks.js
+++ b/blocks/blocks.js
@@ -108,4 +108,5 @@ import './login/block.js';
L5.33,4.91L9.46,3.96z"/>
;
wp.blocks.updateCategory( 'pmpro', { icon: PMProSVG } );
+ wp.blocks.updateCategory( 'pmpro-pages', { icon: PMProSVG } );
} )();
diff --git a/blocks/blocks.php b/blocks/blocks.php
index bad8547eb..4c6f51d4b 100644
--- a/blocks/blocks.php
+++ b/blocks/blocks.php
@@ -40,6 +40,10 @@ function pmpro_place_blocks_in_panel( $categories, $post_or_context ) {
'slug' => 'pmpro',
'title' => __( 'Paid Memberships Pro', 'paid-memberships-pro' ),
),
+ array(
+ 'slug' => 'pmpro-pages',
+ 'title' => __( 'Paid Memberships Pro Pages', 'paid-memberships-pro' ),
+ ),
)
);
}
diff --git a/blocks/cancel-page/block.js b/blocks/cancel-page/block.js
index ae8032bb5..caf8be4e1 100644
--- a/blocks/cancel-page/block.js
+++ b/blocks/cancel-page/block.js
@@ -19,15 +19,21 @@
export default registerBlockType(
'pmpro/cancel-page',
{
- title: __( 'Membership Cancel Page', 'paid-memberships-pro' ),
- description: __( 'Generates the Membership Cancel page.', 'paid-memberships-pro' ),
- category: 'pmpro',
+ title: __( 'PMPro Page: Cancel', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section where members can cancel their membership and active subscription if applicable.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'no',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'payment method', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'terminate', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/checkout-button/block.js b/blocks/checkout-button/block.js
index 92bb8f969..26a0a4b3d 100644
--- a/blocks/checkout-button/block.js
+++ b/blocks/checkout-button/block.js
@@ -29,17 +29,20 @@ export default registerBlockType(
'pmpro/checkout-button',
{
title: __( 'Membership Checkout Button', 'paid-memberships-pro' ),
- description: __( 'Displays a button-styled link to Membership Checkout for the specified level.', 'paid-memberships-pro' ),
+ description: __( 'Inserts a button that links directly to membership checkout for the selected level.', 'paid-memberships-pro' ),
category: 'pmpro',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#658B24',
src: 'migrate',
},
- keywords: [
- __( 'pmpro', 'paid-memberships-pro' ),
+ keywords: [
__( 'buy', 'paid-memberships-pro' ),
__( 'level', 'paid-memberships-pro' ),
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'purchase', 'paid-memberships-pro' ),
],
supports: {
},
diff --git a/blocks/checkout-page/block.js b/blocks/checkout-page/block.js
index 91219b1ab..9a6897c1f 100644
--- a/blocks/checkout-page/block.js
+++ b/blocks/checkout-page/block.js
@@ -26,14 +26,21 @@ const {
'pmpro/checkout-page',
{
title: __( 'Membership Checkout Form', 'paid-memberships-pro' ),
- description: __( 'Displays the Membership Checkout form.', 'paid-memberships-pro' ),
+ description: __( 'Dynamic form that allows users to complete free registration or paid checkout for the selected membership level.', 'paid-memberships-pro' ),
category: 'pmpro',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#658B24',
src: 'list-view',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'buy', 'paid-memberships-pro' ),
+ __( 'purchase', 'paid-memberships-pro' ),
+ __( 'sell', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/confirmation-page/block.js b/blocks/confirmation-page/block.js
index fe9adfe09..beae62af1 100644
--- a/blocks/confirmation-page/block.js
+++ b/blocks/confirmation-page/block.js
@@ -19,15 +19,23 @@
export default registerBlockType(
'pmpro/confirmation-page',
{
- title: __( 'Membership Confirmation Page', 'paid-memberships-pro' ),
- description: __( 'Displays the member\'s Membership Confirmation after Membership Checkout.', 'paid-memberships-pro' ),
- category: 'pmpro',
+ title: __( 'PMPro Page: Confirmation', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section that displays a confirmation message and purchase information for the active member immediately after membership registration and checkout.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'yes',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'member', 'paid-memberships-pro' ),
+ __( 'buy', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'purchase', 'paid-memberships-pro' ),
+ __( 'receipt', 'paid-memberships-pro' ),
+ __( 'success', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/invoice-page/block.js b/blocks/invoice-page/block.js
index d3bd882f3..30b2129fb 100644
--- a/blocks/invoice-page/block.js
+++ b/blocks/invoice-page/block.js
@@ -19,15 +19,22 @@
export default registerBlockType(
'pmpro/invoice-page',
{
- title: __( 'Membership Invoice Page', 'paid-memberships-pro' ),
- description: __( 'Displays the member\'s Membership Invoices.', 'paid-memberships-pro' ),
- category: 'pmpro',
+ title: __( 'PMPro Page: Invoice', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section that displays a list of all invoices (purchase history) for the active member. Each invoice can be selected and viewed in full detail.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'archive',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'history', 'paid-memberships-pro' ),
+ __( 'order', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'purchases', 'paid-memberships-pro' ),
+ __( 'receipt', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/levels-page/block.js b/blocks/levels-page/block.js
index b2610f3b4..b0c210c33 100644
--- a/blocks/levels-page/block.js
+++ b/blocks/levels-page/block.js
@@ -19,15 +19,21 @@
export default registerBlockType(
'pmpro/levels-page',
{
- title: __( 'Membership Levels List', 'paid-memberships-pro' ),
- description: __( 'Displays a list of Membership Levels. To change the order, go to Memberships > Settings > Levels.', 'paid-memberships-pro' ),
+ title: __( 'Membership Levels and Pricing Table', 'paid-memberships-pro' ),
+ description: __( 'Dynamic page section that displays a list of membership levels and pricing, linked to membership checkout. To reorder the display, navigate to Memberships > Settings > Levels.', 'paid-memberships-pro' ),
category: 'pmpro',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#658B24',
src: 'list-view',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'level', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'price', 'paid-memberships-pro' ),
+ __( 'pricing table', 'paid-memberships-pro' ),
+ ],
supports: {
},
attributes: {
diff --git a/blocks/login/block.js b/blocks/login/block.js
index 434b21901..6b570e95c 100644
--- a/blocks/login/block.js
+++ b/blocks/login/block.js
@@ -19,37 +19,38 @@ const { Fragment } = wp.element;
/**
* Register block
*/
-export default registerBlockType("pmpro/login-form", {
- title: __("Log in Form", "paid-memberships-pro"),
- description: __(
- "Displays a Log In Form for Paid Memberships Pro.",
- "paid-memberships-pro"
- ),
- category: "pmpro",
- icon: {
- background: "#2997c8",
- foreground: "#ffffff",
- src: "unlock",
- },
- keywords: [
- __("pmpro", "paid-memberships-pro"),
- __("login", "paid-memberships-pro"),
- __("form", "paid-memberships-pro"),
- __("log in", "paid-memberships-pro"),
- ],
- supports: {},
- edit: (props) => {
- return [
-
-
-
- {__("Paid Memberships Pro", "paid-memberships-pro")}
- {__("Log in Form", "paid-memberships-pro")}
-
- ,
- ];
- },
- save() {
- return null;
- },
-});
+export default registerBlockType(
+ 'pmpro/login-form',
+ {
+ title: __( 'Login Form', 'paid-memberships-pro' ),
+ description: __( 'Dynamic form that allows users to log in or recover a loast password. Logged in users can see a welcome message with the selected custom menu.', 'paid-memberships-pro' ),
+ category: 'pmpro',
+ icon: {
+ background: '#FFFFFF',
+ foreground: '#658B24',
+ src: 'unlock',
+ },
+ keywords: [
+ __( 'log in', 'paid-memberships-pro' ),
+ __( 'lost password', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'password reset', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ ],
+ supports: {},
+ edit: (props) => {
+ return [
+
+
+
+ {__("Paid Memberships Pro", "paid-memberships-pro")}
+ {__("Log in Form", "paid-memberships-pro")}
+
+ ,
+ ];
+ },
+ save() {
+ return null;
+ },
+ }
+);
diff --git a/blocks/member-profile-edit/block.js b/blocks/member-profile-edit/block.js
index 563c381c7..85c95daac 100644
--- a/blocks/member-profile-edit/block.js
+++ b/blocks/member-profile-edit/block.js
@@ -13,31 +13,36 @@ const { registerBlockType } = wp.blocks;
/**
* Register block
*/
-export default registerBlockType("pmpro/member-profile-edit", {
- title: __("Member Profile Edit", "paid-memberships-pro"),
- description: __("Allow member profile editing.", "paid-memberships-pro"),
- category: "pmpro",
- icon: {
- background: "#2997c8",
- foreground: "#ffffff",
- src: "admin-users",
- },
- keywords: [
- __("pmpro", "paid-memberships-pro"),
- __("member", "paid-memberships-pro"),
- __("profile", "paid-memberships-pro"),
- ],
- edit: (props) => {
- return (
-
- {__("Paid Memberships Pro", "paid-memberships-pro")}
-
- {__("Member Profile Edit", "paid-memberships-pro")}
-
-
- );
- },
- save() {
- return null;
- },
-});
+export default registerBlockType(
+ 'pmpro/member-profile-edit',
+ {
+ title: __( 'PMPro Page: Account Profile Edit', 'paid-memberships-pro' ),
+ description: __( 'Dynaimc form that allows the current logged in member to edit their default user profile information and any custom user profile fields.', 'paid-memberships-pro' ),
+ category: 'pmpro-pages',
+ icon: {
+ background: '#FFFFFF',
+ foreground: '#1A688B',
+ src: 'admin-users',
+ },
+ keywords: [
+ __( 'custom field', 'paid-memberships-pro' ),
+ __( 'fields', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'user fields', 'paid-memberships-pro' ),
+ ],
+ edit: (props) => {
+ return (
+
+ {__("Paid Memberships Pro", "paid-memberships-pro")}
+
+ {__("Member Profile Edit", "paid-memberships-pro")}
+
+
+ );
+ },
+ save() {
+ return null;
+ },
+ }
+);
diff --git a/blocks/membership/block.js b/blocks/membership/block.js
index a6419b958..3db915146 100644
--- a/blocks/membership/block.js
+++ b/blocks/membership/block.js
@@ -9,11 +9,12 @@
*/
const { __ } = wp.i18n;
const {
- registerBlockType,
+ registerBlockType
} = wp.blocks;
const {
PanelBody,
CheckboxControl,
+ SelectControl,
} = wp.components;
const {
InspectorControls,
@@ -28,15 +29,25 @@ const all_levels = [{ value: 0, label: "Non-Members" }].concat( pmpro.all_level_
export default registerBlockType(
'pmpro/membership',
{
- title: __( 'Require Membership Block', 'paid-memberships-pro' ),
- description: __( 'Control the visibility of nested blocks for members or non-members.', 'paid-memberships-pro' ),
+ title: __( 'Membership Required Block', 'paid-memberships-pro' ),
+ description: __( 'Nest blocks within this wrapper to control the inner block visibility by membership level or for non-members only.', 'paid-memberships-pro' ),
category: 'pmpro',
icon: {
- background: '#2997c8',
- foreground: '#ffffff',
+ background: '#FFFFFF',
+ foreground: '#1A688B',
src: 'visibility',
},
- keywords: [ __( 'pmpro', 'paid-memberships-pro' ) ],
+ keywords: [
+ __( 'block visibility', 'paid-memberships-pro' ),
+ __( 'confitional', 'paid-memberships-pro' ),
+ __( 'content', 'paid-memberships-pro' ),
+ __( 'hide', 'paid-memberships-pro' ),
+ __( 'hidden', 'paid-memberships-pro' ),
+ __( 'paid memberships pro', 'paid-memberships-pro' ),
+ __( 'pmpro', 'paid-memberships-pro' ),
+ __( 'private', 'paid-memberships-pro' ),
+ __( 'restrict', 'paid-memberships-pro' ),
+ ],
attributes: {
levels: {
type: 'array',
@@ -46,9 +57,13 @@ const all_levels = [{ value: 0, label: "Non-Members" }].concat( pmpro.all_level_
type: 'string',
default:'',
},
+ show_noaccess: {
+ type: 'boolean',
+ default: false,
+ },
},
edit: props => {
- const { attributes: {levels, uid}, setAttributes, isSelected } = props;
+ const { attributes: {levels, uid, show_noaccess}, setAttributes, isSelected } = props;
if( uid=='' ) {
var rand = Math.random()+"";
setAttributes( { uid:rand } );
@@ -69,7 +84,7 @@ const all_levels = [{ value: 0, label: "Non-Members" }].concat( pmpro.all_level_
}
}
return [
-
levelID == level.value ) }
onChange = { setLevelsAttribute }
@@ -79,17 +94,31 @@ const all_levels = [{ value: 0, label: "Non-Members" }].concat( pmpro.all_level_
return [
isSelected &&
-
+
+ { __( 'Which membership levels can view this block?', 'paid-memberships-pro' ) }
{checkboxes}
+
+ { __( 'What should users without access see?', 'paid-memberships-pro' ) }
+ Advanced Settings page.", "paid-memberships-pro" ) }
+ options={ [
+ { label: __( "Show nothing", 'paid-memberships-pro' ), value: '0' },
+ { label: __( "Show the 'no access' message", 'paid-memberships-pro' ), value: '1' },
+ ] }
+ onChange={ show_noaccess => setAttributes( { show_noaccess } ) }
+ />
,
isSelected &&
-
{ __( 'Require Membership', 'paid-memberships-pro' ) }
+
{ __( 'Membership Required', 'paid-memberships-pro' ) }
+
(
@@ -98,7 +127,7 @@ const all_levels = [{ value: 0, label: "Non-Members" }].concat( pmpro.all_level_
/>
,
! isSelected &&
-
{ __( 'Require Membership', 'paid-memberships-pro' ) }
+
{ __( 'Membership Required', 'paid-memberships-pro' ) }
(
diff --git a/blocks/membership/block.php b/blocks/membership/block.php
index 5c2acd095..4c58b567e 100644
--- a/blocks/membership/block.php
+++ b/blocks/membership/block.php
@@ -44,6 +44,8 @@ function render_dynamic_block( $attributes, $content ) {
} else {
if ( pmpro_hasMembershipLevel( $attributes['levels'] ) ) {
return do_blocks( $content );
+ } elseif ( ! empty( $attributes['show_noaccess'] ) ) {
+ return pmpro_get_no_access_message( NULL, $attributes['levels'] );
}
}
}
diff --git a/classes/class-pmpro-site-health.php b/classes/class-pmpro-site-health.php
index fcf6cbe7c..a66baeff0 100644
--- a/classes/class-pmpro-site-health.php
+++ b/classes/class-pmpro-site-health.php
@@ -100,7 +100,11 @@ public function debug_information( $info ) {
'pmpro-pages' => [
'label' => __( 'Membership Pages', 'paid-memberships-pro' ),
'value' => self::get_pmpro_pages(),
- ]
+ ],
+ 'pmpro-library-conflicts' => [
+ 'label' => __( 'Library Conflicts', 'paid-memberships-pro' ),
+ 'value' => self::get_library_conflicts(),
+ ],
],
];
@@ -316,18 +320,7 @@ public function get_cron_jobs() {
$cron_times = [];
// These are our crons.
- $expected_crons = [
- 'pmpro_cron_expire_memberships',
- 'pmpro_cron_expiration_warnings',
- 'pmpro_cron_credit_card_expiring_warnings',
- 'pmpro_cron_admin_activity_email',
- ];
-
- $gateway = pmpro_getOption( 'gateway' );
-
- if ( 'stripe' === $gateway ) {
- $expected_crons[] = 'pmpro_cron_stripe_subscription_updates';
- }
+ $expected_crons = array_keys( pmpro_get_crons() );
// Find any of our crons and when their next run is.
if ( $crons ) {
@@ -369,7 +362,7 @@ public function get_pmpro_pages() {
global $pmpro_pages;
$page_information = array();
-
+
if( !empty( $pmpro_pages ) ){
foreach( $pmpro_pages as $key => $val ){
@@ -472,6 +465,37 @@ public function get_htaccess_cache_usage() {
return __( 'Off', 'paid-memberships-pro' );
}
+ /**
+ * Get library conflicts.
+ *
+ * @since 2.8
+ *
+ * @return string|string[] The member page information
+ */
+ function get_library_conflicts() {
+ // Get the current list of library conflicts.
+ $library_conflicts = get_option( 'pmpro_library_conflicts' );
+
+ // If there are no library conflicts, return a message.
+ if ( empty( $library_conflicts ) ) {
+ return __( 'No library conflicts detected.', 'paid-memberships-pro' );
+ }
+
+ // Format data to be displayed in site health.
+ $return_arr = array();
+
+ // Loop through all libraries that have conflicts.
+ foreach ( $library_conflicts as $library_name => $conflicting_plugins ) {
+ $conflict_strings = array();
+ // Loop through all plugins that have conflicts with this library.
+ foreach ( $conflicting_plugins as $conflicting_plugin_path => $conflicting_plugin_data ) {
+ $conflict_strings[] = 'v' . $conflicting_plugin_data['version'] . ' (' . $conflicting_plugin_data['timestamp'] . ')' . ' - ' . $conflicting_plugin_path;
+ }
+ $return_arr[ $library_name ] = implode( ' | ', $conflict_strings );
+ }
+ return $return_arr;
+ }
+
/**
* Get the constants site health information.
*
@@ -544,5 +568,4 @@ public function get_constants() {
return $constants_formatted;
}
-
}
diff --git a/classes/class-pmpro-wisdom-integration.php b/classes/class-pmpro-wisdom-integration.php
new file mode 100644
index 000000000..3e2de24f8
--- /dev/null
+++ b/classes/class-pmpro-wisdom-integration.php
@@ -0,0 +1,490 @@
+ true,
+ 'pmpro-discountcodes' => true,
+ 'pmpro-pagesettings' => true,
+ 'pmpro-paymentsettings' => true,
+ 'pmpro-emailsettings' => true,
+ 'pmpro-emailtemplates' => true,
+ 'pmpro-advancedsettings' => true,
+ ];
+
+ /**
+ * The Wisdom Tracker object.
+ *
+ * @since 2.8
+ *
+ * @var PMPro_Wisdom_Tracker
+ */
+ public $wisdom_tracker;
+
+ /**
+ * Set up and return the class instance.
+ *
+ * @since 2.8
+ *
+ * @return self
+ */
+ public static function instance() {
+ if ( ! isset( self::$instance ) ) {
+ self::$instance = new self;
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Prevent new public instances by having a private constructor.
+ */
+ private function __construct() {
+ // Nothing to do here.
+ }
+
+ /**
+ * Set up the Wisdom tracker.
+ *
+ * @since 2.8
+ */
+ public function setup_wisdom() {
+ require_once PMPRO_DIR . '/classes/class-pmpro-wisdom-tracker.php';
+
+ // Sync the options together.
+ add_action( 'add_option_wisdom_allow_tracking', [ $this, 'sync_wisdom_setting_to_plugin' ], 10, 2 );
+ add_action( 'update_option_wisdom_allow_tracking', [ $this, 'sync_wisdom_setting_to_plugin' ], 10, 2 );
+ add_action( 'add_option_' . $this->plugin_option, [ $this, 'sync_plugin_setting_to_wisdom' ], 10, 2 );
+ add_action( 'update_option_' . $this->plugin_option, [ $this, 'sync_plugin_setting_to_wisdom' ], 10, 2 );
+
+ // Additional Wisdom customizations.
+ add_filter( 'wisdom_is_local_' . $this->plugin_slug, [ $this, 'bypass_local_tracking' ] );
+ add_filter( 'wisdom_notice_text_' . $this->plugin_slug, [ $this, 'override_notice' ] );
+ add_filter( 'wisdom_tracker_data_' . $this->plugin_slug, [ $this, 'add_stats' ] );
+
+ // Set up the tracker object.
+ $this->wisdom_tracker = new PMPro_Wisdom_Tracker(
+ PMPRO_BASE_FILE,
+ $this->plugin_slug,
+ 'https://asimov.paidmembershipspro.com',
+ [
+ $this->plugin_option,
+ ],
+ true,
+ false,
+ 1
+ );
+
+ // Adjust tracking hooks.
+ $this->remove_wisdom_notices_from_non_plugin_screens();
+ }
+
+ /**
+ * When the Wisdom setting for tracking is changed, sync the plugin setting to match.
+ *
+ * @since 2.8
+ *
+ * @param array|null $old_value The old value of the option.
+ * @param array $value The new value of the option.
+ */
+ public function sync_wisdom_setting_to_plugin( $old_value, $value ) {
+ $opt_out = ! empty( $value[ $this->plugin_slug ] ) ? 0 : 1;
+ pmpro_setOption( $this->plugin_option, $opt_out );
+ }
+
+ /**
+ * When the plugin setting for tracking is changed, sync the Wisdom setting to match.
+ *
+ * @since 2.8
+ *
+ * @param array|null $old_value The old value of the option.
+ * @param array $value The new value of the option.
+ */
+ public function sync_plugin_setting_to_wisdom( $old_value, $value ) {
+ // Only handle opt in when needed.
+ if ( ! isset( $value ) ) {
+ return;
+ }
+
+ // Only update when changing the value.
+ if ( isset( $old_value ) && (int) $old_value === (int) $value ) {
+ return;
+ }
+
+ $opt_out = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
+
+ // Update opt-in.
+ $this->wisdom_tracker->set_is_tracking_allowed( ! $opt_out, $this->plugin_slug );
+ $this->wisdom_tracker->set_can_collect_email( ! $opt_out, $this->plugin_slug );
+ }
+
+ /**
+ * Bypass local tracking for additional local URLs.
+ *
+ * @since 2.8
+ *
+ * @param bool $is_local Whether the site is recognized as a local site.
+ *
+ * @return bool Whether the site is recognized as a local site.
+ */
+ public function bypass_local_tracking( $is_local = false ) {
+ if ( true === $is_local || 'production' !== wp_get_environment_type() ) {
+ return $is_local;
+ }
+
+ $url = network_site_url( '/' );
+
+ $url = strtolower( trim( $url ) );
+ $url_parts = parse_url( $url );
+ $host = ! empty( $url_parts['host'] ) ? $url_parts['host'] : false;
+
+ if ( empty( $host ) ) {
+ return $is_local;
+ }
+
+ if ( 'localhost' === $host ) {
+ return true;
+ }
+
+ if ( false !== ip2long( $host ) && ! filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
+ return true;
+ }
+
+ $tlds_to_check = [
+ '.local',
+ '.test',
+ ];
+
+ foreach ( $tlds_to_check as $tld ) {
+ $minus_tld = strlen( $host ) - strlen( $tld );
+
+ if ( $minus_tld === strpos( $host, $tld ) ) {
+ return true;
+ }
+ }
+
+ return $is_local;
+ }
+
+ /**
+ * Override the notice for the Wisdom Tracker opt-in.
+ *
+ * @since 2.8
+ *
+ * @return string
+ */
+ public function override_notice() {
+ $message = esc_html__( 'Share your usage data to help us improve Paid Memberships Pro. We use this data to analyze how our plugin is meeting your needs and identify new opportunities to help you create a thriving membership business. You can always visit the Advanced Settings and change this preference.', 'paid-memberships-pro' );
+ $link = '' . esc_html__( 'Read more about what data we collect.', 'paid-memberships-pro' ) . ' ';
+ return $message . ' ' . $link;
+ }
+
+ /**
+ * Remove Wisdom notices from non-plugin screens.
+ *
+ * @since 2.8
+ */
+ public function remove_wisdom_notices_from_non_plugin_screens() {
+ $settings_page = ! empty( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
+
+ // Check if we are on a settings page using isset() which is faster than in_array().
+ if ( isset( $this->plugin_pages[ $settings_page ] ) ) {
+ return;
+ }
+
+ // Remove the notices from the non-plugin settings pages.
+ remove_action( 'admin_notices', [ $this->wisdom_tracker, 'optin_notice' ] );
+ remove_action( 'admin_notices', [ $this->wisdom_tracker, 'marketing_notice' ] );
+ }
+
+ /**
+ * Add custom stats for the plugin to the data being tracked.
+ *
+ * @since 2.8
+ *
+ * @param array $stats The data to be sent to the Wisdom plugin site.
+ *
+ * @return array The data to be sent to the Wisdom plugin site.
+ */
+ public function add_stats( $stats ) {
+ global $wpdb;
+
+ // License info.
+ $license_check = get_option( 'pmpro_license_check', 'No Value' );
+
+ $license_plan = 'No Value';
+
+ if ( is_array( $license_check ) && isset( $license_check['license'] ) ) {
+ $license_plan = $license_check['license'];
+ }
+
+ $stats['plugin_options_fields']['pmpro_license_key'] = get_option( 'pmpro_license_key', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_license_plan'] = $license_plan;
+
+ // Gateway info.
+ $stats['plugin_options_fields'] = array_merge( $stats['plugin_options_fields'], $this->get_gateway_info() );
+
+ // Levels info.
+ $levels_info = $this->get_levels_info();
+ $stats['plugin_options_fields']['pmpro_level_count'] = $levels_info['pmpro_level_count'];
+ $stats['plugin_options_fields']['pmpro_level_setups'] = $levels_info['pmpro_level_setups'];
+ $stats['plugin_options_fields']['pmpro_has_free_level'] = $levels_info['pmpro_has_free_level'];
+ $stats['plugin_options_fields']['pmpro_has_paid_level'] = $levels_info['pmpro_has_paid_level'];
+
+ // Members info.
+ $stats['plugin_options_fields']['pmpro_members_count'] = pmpro_getSignups( 'all time' );
+ $stats['plugin_options_fields']['pmpro_members_cancelled_count'] = pmpro_getCancellations( 'all time' );
+
+ // Orders info.
+ $stats['plugin_options_fields']['pmpro_orders_count'] = $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->pmpro_membership_orders}`" );
+
+ // Features.
+ $stats['plugin_options_fields']['pmpro_hide_toolbar'] = get_option( 'pmpro_hide_toolbar', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_block_dashboard'] = get_option( 'pmpro_block_dashboard', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_filterqueries'] = get_option( 'pmpro_filterqueries', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_showexcerpts'] = get_option( 'pmpro_showexcerpts', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_spamprotection'] = get_option( 'pmpro_spamprotection', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_recaptcha'] = get_option( 'pmpro_recaptcha', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_maxnotificationpriority'] = get_option( 'pmpro_maxnotificationpriority', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_activity_email_frequency'] = get_option( 'pmpro_activity_email_frequency', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_hideads'] = get_option( 'pmpro_hideads', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_redirecttosubscription'] = get_option( 'pmpro_redirecttosubscription', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_only_filter_pmpro_emails'] = get_option( 'pmpro_only_filter_pmpro_emails', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_email_member_notification'] = get_option( 'pmpro_email_member_notification', 'No Value' );
+ $stats['plugin_options_fields']['pmpro_use_ssl'] = get_option( 'pmpro_pmpro_use_ssl', 'No Value' );
+ $ssl_seal = get_option( 'pmpro_sslseal', '' );
+ if ( ! empty( $ssl_seal ) ) {
+ $stats['plugin_options_fields']['pmpro_sslseal'] = 'Yes';
+ } else {
+ $stats['plugin_options_fields']['pmpro_sslseal'] = 'No';
+ }
+ $stats['plugin_options_fields']['pmpro_nuclear_HTTPS'] = get_option( 'pmpro_nuclear_HTTPS', 'No Value' );
+
+ // Add Ons.
+ $addons_info = $this->get_addons_info();
+
+ $stats['plugin_options_fields']['addons_active'] = $addons_info['addons_active'];
+ $stats['plugin_options_fields']['addons_inactive'] = $addons_info['addons_inactive'];
+ $stats['plugin_options_fields']['addons_update_available'] = $addons_info['addons_update_available'];
+
+ // Flatten the arrays.
+ foreach ( $stats['plugin_options_fields'] as $option => $value ) {
+ if ( is_object( $value ) || is_array( $value ) ) {
+ $value = maybe_serialize( $value );
+ }
+
+ $stats['plugin_options_fields'][ $option ] = $value;
+ }
+ return $stats;
+ }
+
+ /**
+ * Get the gateway information to track.
+ *
+ * @since 2.8
+ *
+ * @return array The gateway information to track.
+ */
+ public function get_gateway_info() {
+ $stats = [];
+
+ // Gateway info.
+ $stats['pmpro_gateway'] = get_option( 'pmpro_gateway', 'No Value' );
+ $stats['pmpro_gateway_environment'] = get_option( 'pmpro_gateway_environment', 'No Value' );
+ $stats['pmpro_currency'] = get_option( 'pmpro_currency', 'No Value' );
+
+ // Get Stripe gateway info for other stats below.
+ $stripe_using_legacy_keys = PMProGateway_stripe::using_legacy_keys();
+ $stripe_has_connect_credentials = PMProGateway_stripe::has_connect_credentials( 'live' ) || PMProGateway_stripe::has_connect_credentials( 'sandbox' );
+
+ // Append the Stripe gateway qualifiers.
+ if ( 'stripe' === $stats['pmpro_gateway'] ) {
+ // Add Legacy Keys text if using Legacy Keys.
+ if ( $stripe_using_legacy_keys ) {
+ $stats['pmpro_gateway'] .= ' (' . __( 'Legacy Keys', 'paid-memberships-pro' ) . ')';
+ }
+
+ // Add Stripe Connect text if using Stripe Connect.
+ if ( $stripe_has_connect_credentials ) {
+ $stats['pmpro_gateway'] .= ' (' . __( 'Stripe Connect', 'paid-memberships-pro' ) . ')';
+ }
+
+ $stats['pmpro_gateway'] = strtolower( $stats['pmpro_gateway'] );
+ }
+
+ // Detect any gateway settings.
+ $gateway_settings_detected = [
+ 'authorizenet' => get_option( 'pmpro_loginname' ),
+ 'braintree' => get_option( 'pmpro_braintree_merchantid' ),
+ 'cybersource' => get_option( 'pmpro_cybersource_merchantid' ),
+ 'payflowpro' => get_option( 'pmpro_payflow_user' ),
+ 'paypal' => get_option( 'pmpro_apiusername' ),
+ 'paypalexpress' => get_option( 'paypalexpress_skip_confirmation' ),
+ 'paypalstandard' => get_option( 'gateway_email' ),
+ 'stripe' => $stripe_using_legacy_keys || $stripe_has_connect_credentials,
+ 'stripe_sandbox' => get_option( 'sandbox_stripe_connect_user_id' ),
+ 'twocheckout' => get_option( 'twocheckout_accountnumber' ),
+ ];
+
+ // Remove any gateway settings that are not set or are empty.
+ $gateway_settings_detected = array_map( static function( $value ) {
+ return false !== $value && '' !== $value;
+ }, $gateway_settings_detected );
+ $gateway_settings_detected = array_filter( $gateway_settings_detected );
+
+ // Fill in the gateway count/detected info.
+ $stats['pmpro_gateways_count'] = count( $gateway_settings_detected );
+ $stats['pmpro_gateways_detected'] = implode( ', ', array_keys( $gateway_settings_detected ) );
+
+ return $stats;
+ }
+
+ /**
+ * Get the level information for all levels to track.
+ *
+ * @since 2.8
+ *
+ * @return array The level information for all levels to track.
+ */
+ public function get_levels_info() {
+ global $wpdb;
+
+ $stats = array(
+ 'pmpro_level_setups' => array(),
+ 'pmpro_has_free_level' => 'no',
+ 'pmpro_has_paid_level' => 'no',
+ 'pmpro_level_count' => 0,
+ );
+
+ // Get the levels.
+ $levels = pmpro_getAllLevels( true );
+
+ // Update the level count.
+ $stats['pmpro_level_count'] = count( $levels );
+
+ // Loop through the levels.
+ foreach ( $levels as $level_id => $level_data ) {
+ // Remove sensitive info.
+ unset( $level_data->name );
+ unset( $level_data->description );
+ unset( $level_data->confirmation );
+
+ // Add Set Expiration Date/Subscription Delay info.
+ $level_data->set_expiration_date = get_option( 'pmprosed_' . $level_id , '' );
+ $level_data->subscription_delay = get_option( 'pmpro_subscription_delay_' . $level_id , '' );
+
+ // Add if a category is set.
+ $categories = $wpdb->get_col(
+ $wpdb->prepare(
+ "SELECT category_id
+ FROM $wpdb->pmpro_memberships_categories
+ WHERE membership_id = %d",
+ $level_id
+ )
+ );
+ $level_data->has_categories = ! empty( $categories ) ? 'yes' : 'no';
+
+ // Add level info.
+ $stats['pmpro_level_setups'][ $level_id ] = $level_data;
+
+ // Update whether or not we have a free/paid level yet.
+ if ( pmpro_isLevelFree( $level_data ) ) {
+ $stats['pmpro_has_free_level'] = 'yes';
+ } else {
+ $stats['pmpro_has_paid_level'] = 'yes';
+ }
+ }
+ return $stats;
+ }
+
+ /**
+ * Get the list of Add Ons categorized by active, inactive, and update available.
+ *
+ * @since 2.8
+ *
+ * @return array The list of Add Ons categorized by active, inactive, and update available.
+ */
+ public function get_addons_info() {
+ // Build the list of Add Ons data to track.
+ $addons = pmpro_getAddons();
+ $plugin_info = get_site_transient( 'update_plugins' );
+
+ // Split Add Ons into groups for filtering
+ $addons_active = [];
+ $addons_inactive = [];
+ $addons_update_available = [];
+
+ // Build array of Visible, Hidden, Active, Inactive, Installed, and Not Installed Add Ons.
+ foreach ( $addons as $addon ) {
+ $plugin_file = $addon['Slug'] . '/' . $addon['Slug'] . '.php';
+ $plugin_file_abs = WP_PLUGIN_DIR . '/' . $plugin_file;
+
+ if ( file_exists( $plugin_file_abs ) ) {
+ $plugin_data = get_plugin_data( $plugin_file_abs );
+ } else {
+ // Plugin is not on the site.
+ continue;
+ }
+
+ // Build Active and Inactive arrays - exclude hidden Add Ons that are not installed.
+ if ( is_plugin_active( $plugin_file ) ) {
+ $addons_active[ $addon['Slug'] ] = $plugin_data['Version'];
+ } else {
+ $addons_inactive[ $addon['Slug'] ] = $plugin_data['Version'];
+ }
+
+ // Build array of Add Ons that have an update available.
+ if ( isset( $plugin_info->response[ $plugin_file ] ) ) {
+ $addons_update_available[ $addon['Slug'] ] = $plugin_data['Version'];
+ }
+ }
+
+ return [
+ 'addons_active' => $addons_active,
+ 'addons_inactive' => $addons_inactive,
+ 'addons_update_available' => $addons_update_available,
+ ];
+ }
+
+}
diff --git a/classes/class-pmpro-wisdom-tracker.php b/classes/class-pmpro-wisdom-tracker.php
new file mode 100644
index 000000000..62ff3a3c7
--- /dev/null
+++ b/classes/class-pmpro-wisdom-tracker.php
@@ -0,0 +1,1107 @@
+plugin_file = $_plugin_file;
+ $this->home_url = trailingslashit( $_home_url );
+
+ // If the filename is 'functions' then we're tracking a theme
+ if ( basename( $this->plugin_file, '.php' ) != 'functions' ) {
+ $this->plugin_name = basename( $this->plugin_file, '.php' );
+
+ // PMPRO MODIFICATION.
+ if ( ! empty( $_plugin_slug ) ) {
+ $this->plugin_name = $_plugin_slug;
+ }
+ } else {
+ $this->what_am_i = 'theme';
+ $theme = wp_get_theme();
+ if ( $theme->Name ) {
+ $this->plugin_name = sanitize_text_field( $theme->Name );
+ }
+ }
+
+ $this->options = $_options;
+ $this->require_optin = $_require_optin;
+ $this->include_goodbye_form = $_include_goodbye_form;
+ $this->marketing = $_marketing;
+
+ // Only use this on switching theme
+ $this->theme_allows_tracking = get_theme_mod( 'wisdom-allow-tracking', 0 );
+
+ // Schedule / deschedule tracking when activated / deactivated
+ if ( $this->what_am_i == 'theme' ) {
+ // Need to think about scheduling for sites that have already activated the theme
+ add_action( 'after_switch_theme', [ $this, 'schedule_tracking' ] );
+ add_action( 'switch_theme', [ $this, 'deactivate_this_plugin' ] );
+ } else {
+ register_activation_hook( $this->plugin_file, [ $this, 'schedule_tracking' ] );
+ register_deactivation_hook( $this->plugin_file, [ $this, 'deactivate_this_plugin' ] );
+ }
+
+ // Get it going
+ $this->init();
+ }
+
+ public function init() {
+ // Check marketing
+ if ( $this->marketing == 3 ) {
+ $this->set_can_collect_email( true, $this->plugin_name );
+ }
+
+ // Check whether opt-in is required
+ // If not, then tracking is allowed
+ if ( ! $this->require_optin ) {
+ $this->set_can_collect_email( true, $this->plugin_name );
+ $this->set_is_tracking_allowed( true );
+ $this->update_block_notice();
+ $this->do_tracking();
+ }
+
+ // Hook our do_tracking function to the weekly action
+ add_filter( 'cron_schedules', [ $this, 'schedule_weekly_event' ] );
+ // It's called weekly, but in fact it could be daily, weekly or monthly
+ add_action( 'put_do_weekly_action', [ $this, 'do_tracking' ] );
+
+ // Use this action for local testing
+ // add_action( 'admin_init', array( $this, 'do_tracking' ) );
+
+ // Display the admin notice on activation
+ add_action( 'admin_init', [ $this, 'set_notification_time' ] );
+ add_action( 'admin_notices', [ $this, 'optin_notice' ] );
+ add_action( 'admin_notices', [ $this, 'marketing_notice' ] );
+
+ // Deactivation
+ add_filter( 'plugin_action_links_' . plugin_basename( $this->plugin_file ), [ $this, 'filter_action_links' ] );
+ add_action( 'admin_footer-plugins.php', [ $this, 'goodbye_ajax' ] );
+ add_action( 'wp_ajax_goodbye_form', [ $this, 'goodbye_form_callback' ] );
+ }
+
+ /**
+ * When the plugin is activated
+ * Create scheduled event
+ * And check if tracking is enabled - perhaps the plugin has been reactivated
+ *
+ * @since 1.0.0
+ */
+ public function schedule_tracking() {
+ if ( ! wp_next_scheduled( 'put_do_weekly_action' ) ) {
+ $schedule = $this->get_schedule();
+ wp_schedule_event( time(), $schedule, 'put_do_weekly_action' );
+ }
+ $this->do_tracking( true );
+ }
+
+ /**
+ * Create weekly schedule
+ *
+ * @since 1.2.3
+ */
+ public function schedule_weekly_event( $schedules ) {
+ $schedules['weekly'] = [
+ 'interval' => 604800,
+ 'display' => __( 'Once Weekly' ),
+ ];
+ $schedules['monthly'] = [
+ 'interval' => 2635200,
+ 'display' => __( 'Once Monthly' ),
+ ];
+
+ return $schedules;
+ }
+
+ /**
+ * Get how frequently data is tracked back
+ *
+ * @since 1.2.3
+ */
+ public function get_schedule() {
+ // Could be daily, weekly or monthly
+ $schedule = apply_filters( 'wisdom_filter_schedule_' . $this->plugin_name, 'monthly' );
+
+ return $schedule;
+ }
+
+ /**
+ * This is our function to get everything going
+ * Check that user has opted in
+ * Collect data
+ * Then send it back
+ *
+ * @since 1.0.0
+ *
+ * @param $force Force tracking if it's not time
+ */
+ public function do_tracking( $force = false ) {
+ // If the home site hasn't been defined, we just drop out. Nothing much we can do.
+ if ( ! $this->home_url ) {
+ return;
+ }
+
+ // Check to see if the user has opted in to tracking
+ $allow_tracking = $this->get_is_tracking_allowed();
+ if ( ! $allow_tracking ) {
+ return;
+ }
+
+ // Check to see if it's time to track
+ $track_time = $this->get_is_time_to_track();
+ if ( ! $track_time && ! $force ) {
+ return;
+ }
+
+ $this->set_admin_email();
+
+ // Get our data
+ $body = $this->get_data();
+
+ // Send the data
+ $this->send_data( $body );
+ }
+
+ /**
+ * We hook this to admin_init when the user accepts tracking
+ * Ensures the email address is available
+ *
+ * @since 1.2.1
+ */
+ public function force_tracking() {
+ $this->do_tracking( true ); // Run this straightaway
+ }
+
+ /**
+ * Send the data to the home site
+ *
+ * @since 1.0.0
+ */
+ public function send_data( $body ) {
+ $request = wp_remote_post( esc_url( $this->home_url . '?usage_tracker=hello' ), [
+ 'method' => 'POST',
+ 'timeout' => 20,
+ 'redirection' => 5,
+ 'httpversion' => '1.1',
+ 'blocking' => true,
+ 'body' => $body,
+ 'user-agent' => 'PUT/1.0.0; ' . home_url(),
+ ] );
+
+ $this->set_track_time();
+
+ if ( is_wp_error( $request ) ) {
+ return $request;
+ }
+ }
+
+ /**
+ * Here we collect most of the data
+ *
+ * @since 1.0.0
+ */
+ public function get_data() {
+ // Use this to pass error messages back if necessary
+ $body['message'] = '';
+
+ // Use this array to send data back
+ $body = [
+ 'plugin_slug' => sanitize_text_field( $this->plugin_name ),
+ 'url' => home_url(),
+ 'site_name' => get_bloginfo( 'name' ),
+ 'site_version' => get_bloginfo( 'version' ),
+ 'site_language' => get_bloginfo( 'language' ),
+ 'charset' => get_bloginfo( 'charset' ),
+ 'wisdom_version' => $this->wisdom_version,
+ 'php_version' => phpversion(),
+ 'multisite' => is_multisite(),
+ 'file_location' => __FILE__,
+ 'product_type' => esc_html( $this->what_am_i ),
+ ];
+
+ // Collect the email if the correct option has been set
+ if ( $this->get_can_collect_email() ) {
+ $body['email'] = $this->get_admin_email();
+ }
+ $body['marketing_method'] = $this->marketing;
+
+ $body['server'] = isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '';
+
+ // Extra PHP fields
+ $body['memory_limit'] = ini_get( 'memory_limit' );
+ $body['upload_max_size'] = ini_get( 'upload_max_size' );
+ $body['post_max_size'] = ini_get( 'post_max_size' );
+ $body['upload_max_filesize'] = ini_get( 'upload_max_filesize' );
+ $body['max_execution_time'] = ini_get( 'max_execution_time' );
+ $body['max_input_time'] = ini_get( 'max_input_time' );
+
+ // Retrieve current plugin information
+ if ( ! function_exists( 'get_plugins' ) ) {
+ include ABSPATH . '/wp-admin/includes/plugin.php';
+ }
+
+ $plugins = array_keys( get_plugins() );
+ $active_plugins = get_option( 'active_plugins', [] );
+
+ foreach ( $plugins as $key => $plugin ) {
+ if ( in_array( $plugin, $active_plugins, true ) ) {
+ // Remove active plugins from list so we can show active and inactive separately
+ unset( $plugins[ $key ] );
+ }
+ }
+
+ $body['active_plugins'] = $active_plugins;
+ $body['inactive_plugins'] = $plugins;
+
+ // Check text direction
+ $body['text_direction'] = 'LTR';
+ if ( function_exists( 'is_rtl' ) ) {
+ if ( is_rtl() ) {
+ $body['text_direction'] = 'RTL';
+ }
+ } else {
+ $body['text_direction'] = 'not set';
+ }
+
+ /**
+ * Get our plugin data
+ * Currently we grab plugin name and version
+ * Or, return a message if the plugin data is not available
+ *
+ * @since 1.0.0
+ */
+ $plugin = $this->plugin_data();
+ $body['status'] = 'Active'; // Never translated
+ if ( empty( $plugin ) ) {
+ // We can't find the plugin data
+ // Send a message back to our home site
+ $body['message'] .= __( 'We can\'t detect any product information. This is most probably because you have not included the code snippet.', 'paid-memberships-pro' );
+ $body['status'] = 'Data not found'; // Never translated
+ } else {
+ if ( isset( $plugin['Name'] ) ) {
+ $body['plugin'] = sanitize_text_field( $plugin['Name'] );
+ }
+ if ( isset( $plugin['Version'] ) ) {
+ $body['version'] = sanitize_text_field( $plugin['Version'] );
+ }
+ }
+
+ /**
+ * Get our plugin options
+ *
+ * @since 1.0.0
+ */
+ $options = $this->options;
+ $plugin_options = [];
+ if ( ! empty( $options ) && is_array( $options ) ) {
+ foreach ( $options as $option ) {
+ $fields = get_option( $option );
+ // Check for permission to send this option
+ if ( isset( $fields['wisdom_registered_setting'] ) ) {
+ foreach ( $fields as $key => $value ) {
+ $plugin_options[ $key ] = $value;
+ }
+ }
+ }
+ }
+ $body['plugin_options'] = $this->options; // Returns array
+ $body['plugin_options_fields'] = $plugin_options; // Returns object
+
+ /**
+ * Get our theme data
+ * Currently we grab theme name and version
+ *
+ * @since 1.0.0
+ */
+ $theme = wp_get_theme();
+ if ( $theme->Name ) {
+ $body['theme'] = sanitize_text_field( $theme->Name );
+ }
+ if ( $theme->Version ) {
+ $body['theme_version'] = sanitize_text_field( $theme->Version );
+ }
+ if ( $theme->Template ) {
+ $body['theme_parent'] = sanitize_text_field( $theme->Template );
+ }
+
+ // PMPRO MODIFICATION.
+ /**
+ * Allow filtering the data to be sent to the Wisdom plugin site.
+ *
+ * @since 2.8
+ *
+ * @param array $body The data to be sent to the Wisdom plugin site.
+ */
+ $body = apply_filters( "wisdom_tracker_data_{$this->plugin_name}", $body );
+
+ // Return the data
+ return $body;
+ }
+
+ /**
+ * Return plugin data
+ *
+ * @since 1.0.0
+ */
+ public function plugin_data() {
+ // Being cautious here
+ if ( ! function_exists( 'get_plugin_data' ) ) {
+ include ABSPATH . '/wp-admin/includes/plugin.php';
+ }
+ // Retrieve current plugin information
+ $plugin = get_plugin_data( $this->plugin_file );
+
+ return $plugin;
+ }
+
+ /**
+ * Deactivating plugin
+ *
+ * @since 1.0.0
+ */
+ public function deactivate_this_plugin() {
+ // Check to see if the user has opted in to tracking
+ if ( $this->what_am_i == 'theme' ) {
+ $allow_tracking = $this->theme_allows_tracking;
+ } else {
+ $allow_tracking = $this->get_is_tracking_allowed();
+ }
+
+ if ( ! $allow_tracking ) {
+ return;
+ }
+
+ $body = $this->get_data();
+ $body['status'] = 'Deactivated'; // Never translated
+ $body['deactivated_date'] = time();
+
+ // Add deactivation form data
+ if ( false !== get_option( 'wisdom_deactivation_reason_' . $this->plugin_name ) ) {
+ $body['deactivation_reason'] = get_option( 'wisdom_deactivation_reason_' . $this->plugin_name );
+ }
+ if ( false !== get_option( 'wisdom_deactivation_details_' . $this->plugin_name ) ) {
+ $body['deactivation_details'] = get_option( 'wisdom_deactivation_details_' . $this->plugin_name );
+ }
+
+ $this->send_data( $body );
+ // Clear scheduled update
+ wp_clear_scheduled_hook( 'put_do_weekly_action' );
+
+ // Clear the wisdom_last_track_time value for this plugin
+ // @since 1.2.2
+ $track_time = get_option( 'wisdom_last_track_time' );
+ if ( isset( $track_time[ $this->plugin_name ] ) ) {
+ unset( $track_time[ $this->plugin_name ] );
+ }
+ update_option( 'wisdom_last_track_time', $track_time );
+ }
+
+ /**
+ * Is tracking allowed?
+ *
+ * @since 1.0.0
+ */
+ public function get_is_tracking_allowed() {
+ // First, check if the user has changed their mind and opted out of tracking
+ if ( $this->has_user_opted_out() ) {
+ $this->set_is_tracking_allowed( false, $this->plugin_name );
+
+ // PMPRO MODIFICATION.
+ $this->set_can_collect_email( false, $this->plugin_name );
+
+ return false;
+ }
+
+ if ( $this->what_am_i == 'theme' ) {
+ $mod = get_theme_mod( 'wisdom-allow-tracking', 0 );
+ if ( $mod ) {
+ return true;
+ }
+ } else {
+ // The wisdom_allow_tracking option is an array of plugins that are being tracked
+ $allow_tracking = get_option( 'wisdom_allow_tracking' );
+ // If this plugin is in the array, then tracking is allowed
+ if ( isset( $allow_tracking[ $this->plugin_name ] ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Set if tracking is allowed
+ * Option is an array of all plugins with tracking permitted
+ * More than one plugin may be using the tracker
+ *
+ * @since 1.0.0
+ *
+ * @param $is_allowed Boolean true if tracking is allowed, false if not
+ */
+ public function set_is_tracking_allowed( $is_allowed, $plugin = null ) {
+ if ( empty( $plugin ) ) {
+ $plugin = $this->plugin_name;
+ }
+
+ // The wisdom_allow_tracking option is an array of plugins that are being tracked
+ $allow_tracking = get_option( 'wisdom_allow_tracking' );
+
+ // If the user has decided to opt out
+ if ( $this->has_user_opted_out() ) {
+ if ( $this->what_am_i == 'theme' ) {
+ set_theme_mod( 'wisdom-allow-tracking', 0 );
+ } else {
+ if ( isset( $allow_tracking[ $plugin ] ) ) {
+ unset( $allow_tracking[ $plugin ] );
+ }
+ }
+ } elseif ( $is_allowed || ! $this->require_optin ) {
+ // If the user has agreed to allow tracking or if opt-in is not required
+
+ if ( $this->what_am_i == 'theme' ) {
+ set_theme_mod( 'wisdom-allow-tracking', 1 );
+ } else {
+ if ( empty( $allow_tracking ) || ! is_array( $allow_tracking ) ) {
+ // If nothing exists in the option yet, start a new array with the plugin name
+ $allow_tracking = [ $plugin => $plugin ];
+ } else {
+ // Else add the plugin name to the array
+ $allow_tracking[ $plugin ] = $plugin;
+ }
+ }
+ } else {
+ if ( $this->what_am_i == 'theme' ) {
+ set_theme_mod( 'wisdom-allow-tracking', 0 );
+ } else {
+ if ( isset( $allow_tracking[ $plugin ] ) ) {
+ unset( $allow_tracking[ $plugin ] );
+ }
+ }
+ }
+
+ update_option( 'wisdom_allow_tracking', $allow_tracking );
+ }
+
+ /**
+ * Has the user opted out of allowing tracking?
+ * Note that themes are opt in / plugins are opt out
+ *
+ * @since 1.1.0
+ * @return Boolean
+ */
+ public function has_user_opted_out() {
+ // Different opt-out methods for plugins and themes
+ if ( $this->what_am_i == 'theme' ) {
+ // Look for the theme mod
+ $mod = get_theme_mod( 'wisdom-allow-tracking', 0 );
+ if ( false === $mod ) {
+ // If the theme mod is not set, then return true - the user has opted out
+ return true;
+ }
+ } else {
+ // Iterate through the options that are being tracked looking for wisdom_opt_out setting
+ if ( ! empty( $this->options ) ) {
+ foreach ( $this->options as $option_name ) {
+ // Check each option
+ $options = get_option( $option_name );
+ // If we find the setting, return true
+ if ( ! empty( $options['wisdom_opt_out'] ) ) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if it's time to track
+ *
+ * @since 1.1.1
+ */
+ public function get_is_time_to_track() {
+ // Let's see if we're due to track this plugin yet
+ $track_times = get_option( 'wisdom_last_track_time', [] );
+ if ( ! isset( $track_times[ $this->plugin_name ] ) ) {
+ // If we haven't set a time for this plugin yet, then we must track it
+ return true;
+ } else {
+ // If the time is set, let's get our schedule and check if it's time to track
+ $schedule = $this->get_schedule();
+ if ( $schedule == 'daily' ) {
+ $period = 'day';
+ } elseif ( $schedule == 'weekly' ) {
+ $period = 'week';
+ } else {
+ $period = 'month';
+ }
+ if ( $track_times[ $this->plugin_name ] < strtotime( '-1 ' . $period ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Record the time we send tracking data
+ *
+ * @since 1.1.1
+ */
+ public function set_track_time() {
+ // We've tracked, so record the time
+ $track_times = get_option( 'wisdom_last_track_time', [] );
+ // Set different times according to plugin, in case we are tracking multiple plugins
+ $track_times[ $this->plugin_name ] = time();
+ update_option( 'wisdom_last_track_time', $track_times );
+ }
+
+ /**
+ * Set the time when we can display the opt-in notification
+ * Will display now unless filtered
+ *
+ * @since 1.2.4
+ */
+ public function set_notification_time() {
+ $notification_times = get_option( 'wisdom_notification_times', [] );
+ // Set different times according to plugin, in case we are tracking multiple plugins
+ if ( ! isset( $notification_times[ $this->plugin_name ] ) ) {
+ $delay_notification = apply_filters( 'wisdom_delay_notification_' . $this->plugin_name, 0 );
+ // We can delay the notification time
+ $notification_time = time() + absint( $delay_notification );
+ $notification_times[ $this->plugin_name ] = $notification_time;
+ update_option( 'wisdom_notification_times', $notification_times );
+ }
+ }
+
+ /**
+ * Get whether it's time to display the notification
+ *
+ * @since 1.2.4
+ * @return Boolean
+ */
+ public function get_is_notification_time() {
+ $notification_times = get_option( 'wisdom_notification_times', [] );
+ $time = time();
+ // Set different times according to plugin, in case we are tracking multiple plugins
+ if ( isset( $notification_times[ $this->plugin_name ] ) ) {
+ $notification_time = $notification_times[ $this->plugin_name ];
+ if ( $time >= $notification_time ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Set if we should block the opt-in notice for this plugin
+ * Option is an array of all plugins that have received a response from the user
+ *
+ * @since 1.0.0
+ */
+ public function update_block_notice( $plugin = null ) {
+ if ( empty( $plugin ) ) {
+ $plugin = $this->plugin_name;
+ }
+ $block_notice = get_option( 'wisdom_block_notice' );
+ if ( empty( $block_notice ) || ! is_array( $block_notice ) ) {
+ // If nothing exists in the option yet, start a new array with the plugin name
+ $block_notice = [ $plugin => $plugin ];
+ } else {
+ // Else add the plugin name to the array
+ $block_notice[ $plugin ] = $plugin;
+ }
+ update_option( 'wisdom_block_notice', $block_notice );
+ }
+
+ /**
+ * Can we collect the email address?
+ *
+ * @since 1.0.0
+ */
+ public function get_can_collect_email() {
+ // The wisdom_collect_email option is an array of plugins that are being tracked
+ $collect_email = get_option( 'wisdom_collect_email' );
+ // If this plugin is in the array, then we can collect the email address
+ if ( isset( $collect_email[ $this->plugin_name ] ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Set if user has allowed us to collect their email address
+ * Option is an array of all plugins with email collection permitted
+ * More than one plugin may be using the tracker
+ *
+ * @since 1.0.0
+ *
+ * @param $can_collect Boolean true if collection is allowed, false if not
+ */
+ public function set_can_collect_email( $can_collect, $plugin = null ) {
+ if ( empty( $plugin ) ) {
+ $plugin = $this->plugin_name;
+ }
+ // The wisdom_collect_email option is an array of plugins that are being tracked
+ $collect_email = get_option( 'wisdom_collect_email' );
+ // If the user has agreed to allow tracking or if opt-in is not required
+ if ( $can_collect ) {
+ if ( empty( $collect_email ) || ! is_array( $collect_email ) ) {
+ // If nothing exists in the option yet, start a new array with the plugin name
+ $collect_email = [ $plugin => $plugin ];
+ } else {
+ // Else add the plugin name to the array
+ $collect_email[ $plugin ] = $plugin;
+ }
+ } else {
+ if ( isset( $collect_email[ $plugin ] ) ) {
+ unset( $collect_email[ $plugin ] );
+ }
+ }
+ update_option( 'wisdom_collect_email', $collect_email );
+ }
+
+ /**
+ * Get the correct email address to use
+ *
+ * @since 1.1.2
+ * @return Email address
+ */
+ public function get_admin_email() {
+ // The wisdom_collect_email option is an array of plugins that are being tracked
+ $email = get_option( 'wisdom_admin_emails' );
+ // If this plugin is in the array, then we can collect the email address
+ if ( isset( $email[ $this->plugin_name ] ) ) {
+ return $email[ $this->plugin_name ];
+ }
+
+ return false;
+ }
+
+ /**
+ * Set the correct email address to use
+ * There might be more than one admin on the site
+ * So we only use the first admin's email address
+ *
+ * @since 1.1.2
+ *
+ * @param $plugin Plugin name to set email address for
+ * @param $email Email address to set
+ */
+ public function set_admin_email( $email = null, $plugin = null ) {
+ if ( empty( $plugin ) ) {
+ $plugin = $this->plugin_name;
+ }
+ // If no email address passed, try to get the current user's email
+ if ( empty( $email ) ) {
+ // Have to check that current user object is available
+ if ( function_exists( 'wp_get_current_user' ) ) {
+ $current_user = wp_get_current_user();
+ $email = $current_user->user_email;
+ }
+ }
+ // The wisdom_admin_emails option is an array of admin email addresses
+ $admin_emails = get_option( 'wisdom_admin_emails' );
+ if ( empty( $admin_emails ) || ! is_array( $admin_emails ) ) {
+ // If nothing exists in the option yet, start a new array with the plugin name
+ $admin_emails = [ $plugin => sanitize_email( $email ) ];
+ } elseif ( empty( $admin_emails[ $plugin ] ) ) {
+ // Else add the email address to the array, if not already set
+ $admin_emails[ $plugin ] = sanitize_email( $email );
+ }
+ update_option( 'wisdom_admin_emails', $admin_emails );
+ }
+
+ /**
+ * Display the admin notice to users to allow them to opt in
+ *
+ * @since 1.0.0
+ */
+ public function optin_notice() {
+ // Check for plugin args
+ if ( isset( $_GET['plugin'] ) && isset( $_GET['plugin_action'] ) ) {
+ $plugin = sanitize_text_field( $_GET['plugin'] );
+ $action = sanitize_text_field( $_GET['plugin_action'] );
+ if ( $action == 'yes' ) {
+ $this->set_is_tracking_allowed( true, $plugin );
+ // Run this straightaway
+ add_action( 'admin_init', [ $this, 'force_tracking' ] );
+ } else {
+ $this->set_is_tracking_allowed( false, $plugin );
+ }
+ $this->update_block_notice( $plugin );
+ }
+
+ // Is it time to display the notification?
+ $is_time = $this->get_is_notification_time();
+ if ( ! $is_time ) {
+ return false;
+ }
+
+ // Check whether to block the notice, e.g. because we're in a local environment
+ // wisdom_block_notice works the same as wisdom_allow_tracking, an array of plugin names
+ $block_notice = get_option( 'wisdom_block_notice' );
+
+ if ( isset( $block_notice[ $this->plugin_name ] ) ) {
+ return;
+ }
+
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ // @credit EDD
+ // Don't bother asking user to opt in if they're in local dev
+ $is_local = false;
+ if ( stristr( network_site_url( '/' ), '.dev' ) !== false || stristr( network_site_url( '/' ), 'localhost' ) !== false || stristr( network_site_url( '/' ), ':8888' ) !== false ) {
+ $is_local = true;
+ }
+
+ // PMPRO MODIFICATION
+ if ( ! $is_local && stristr( network_site_url( '/' ), '.local' ) !== false ) {
+ //$is_local = true;
+ }
+
+ $is_local = apply_filters( 'wisdom_is_local_' . $this->plugin_name, $is_local );
+ if ( $is_local ) {
+ $this->update_block_notice();
+
+ // PMPRO MODIFICATION.
+ if ( $this->marketing ) {
+ $this->set_can_collect_email( false );
+ }
+ } else {
+ // Display the notice requesting permission to track
+ // Retrieve current plugin information
+ $plugin = $this->plugin_data();
+ $plugin_name = $plugin['Name'];
+
+ // Args to add to query if user opts in to tracking
+ $yes_args = [
+ 'plugin' => $this->plugin_name,
+ 'plugin_action' => 'yes',
+ ];
+
+ // Decide how to request permission to collect email addresses
+ if ( $this->marketing == 1 ) {
+ // Option 1 combines permissions to track and collect email
+ $yes_args['marketing_optin'] = 'yes';
+ } elseif ( $this->marketing == 2 ) {
+ // Option 2 enables a second notice that fires after the user opts in to tracking
+ $yes_args['marketing'] = 'yes';
+ }
+ $url_yes = add_query_arg( $yes_args );
+ $url_no = add_query_arg( [
+ 'plugin' => $this->plugin_name,
+ 'plugin_action' => 'no',
+ ] );
+
+ // Decide on notice text
+ if ( $this->marketing != 1 ) {
+ // Standard notice text
+ $notice_text = sprintf( __( 'Thank you for installing our %1$s. We would like to track its usage on your site. We don\'t record any sensitive data, only information regarding the WordPress environment and %1$s settings, which we will use to help us make improvements to the %1$s. Tracking is completely optional.', 'paid-memberships-pro' ), $this->what_am_i );
+ } else {
+ // If we have option 1 for marketing, we include reference to sending product information here
+ $notice_text = sprintf( __( 'Thank you for installing our %1$s. We\'d like your permission to track its usage on your site and subscribe you to our newsletter. We won\'t record any sensitive data, only information regarding the WordPress environment and %1$s settings, which we will use to help us make improvements to the %1$s. Tracking is completely optional.', 'paid-memberships-pro' ), $this->what_am_i );
+ }
+ // And we allow you to filter the text anyway
+ $notice_text = apply_filters( 'wisdom_notice_text_' . esc_attr( $this->plugin_name ), $notice_text ); ?>
+
+
+ set_can_collect_email( sanitize_text_field( $_GET['marketing_optin'] ), $this->plugin_name );
+ // Do tracking
+ $this->do_tracking( true );
+ } elseif ( isset( $_GET['marketing'] ) && $_GET['marketing'] == 'yes' ) {
+ // Display the notice requesting permission to collect email address
+ // Retrieve current plugin information
+ $plugin = $this->plugin_data();
+ $plugin_name = $plugin['Name'];
+
+ $url_yes = add_query_arg( [
+ 'plugin' => $this->plugin_name,
+ 'marketing_optin' => 'yes',
+ ] );
+ $url_no = add_query_arg( [
+ 'plugin' => $this->plugin_name,
+ 'marketing_optin' => 'no',
+ ] );
+
+ $marketing_text = sprintf( __( 'Thank you for opting in to tracking. Would you like to receive occasional news about this %s, including details of new features and special offers?', 'paid-memberships-pro' ), $this->what_am_i );
+ $marketing_text = apply_filters( 'wisdom_marketing_text_' . esc_attr( $this->plugin_name ), $marketing_text ); ?>
+
+
+
' . esc_html( $plugin_name ) . ''; ?>
+
+
+
+
+
+
+ get_is_tracking_allowed() ) {
+ return $links;
+ }
+ if ( isset( $links['deactivate'] ) && $this->include_goodbye_form ) {
+ $deactivation_link = $links['deactivate'];
+ // Insert an onClick action to allow form before deactivating
+ $deactivation_link = str_replace( ' form_default_text();
+
+ return apply_filters( 'wisdom_form_text_' . esc_attr( $this->plugin_name ), $form );
+ }
+
+ /**
+ * Form text strings
+ * These can be filtered
+ *
+ * @since 1.0.0
+ */
+ public function goodbye_ajax() {
+ // Get our strings for the form
+ $form = $this->form_filterable_text();
+ if ( ! isset( $form['heading'] ) || ! isset( $form['body'] ) || ! isset( $form['options'] ) || ! is_array( $form['options'] ) || ! isset( $form['details'] ) ) {
+ // If the form hasn't been filtered correctly, we revert to the default form
+ $form = $this->form_default_text();
+ }
+ // Build the HTML to go in the form
+ $html = '' . esc_html( $form['heading'] ) . '
';
+ $html .= '' . esc_html( $form['body'] ) . '
';
+ if ( is_array( $form['options'] ) ) {
+ $html .= '
';
+ }
+ $html .= '
';
+ $html .= ' ' . __( 'Submitting form', 'paid-memberships-pro' ) . '
';
+ ?>
+
+
+
+ plugin_name, $values );
+ }
+ if ( isset( $_POST['details'] ) ) {
+ $details = sanitize_text_field( $_POST['details'] );
+ update_option( 'wisdom_deactivation_details_' . $this->plugin_name, $details );
+ }
+ $this->do_tracking(); // Run this straightaway
+ echo 'success';
+ wp_die();
+ }
+
+}
diff --git a/classes/class.memberorder.php b/classes/class.memberorder.php
index fb4901c03..c9aeab07a 100644
--- a/classes/class.memberorder.php
+++ b/classes/class.memberorder.php
@@ -232,6 +232,12 @@ function is_renewal() {
return $this->is_renewal;
}
+ // Can't tell if this is a renewal without a timestamp.
+ if ( empty( $this->timestamp ) ) {
+ $this->is_renewal = false;
+ return $this->is_renewal;
+ }
+
// Check the DB.
$sqlQuery = "SELECT `id`
FROM $wpdb->pmpro_membership_orders
@@ -754,12 +760,15 @@ function saveOrder()
if(empty($this->gateway_environment))
$this->gateway_environment = pmpro_getOption("gateway_environment");
- if(empty($this->datetime) && empty($this->timestamp))
- $this->datetime = date("Y-m-d H:i:s", time());
- elseif(empty($this->datetime) && !empty($this->timestamp) && is_numeric($this->timestamp))
+ if( empty( $this->datetime ) && empty( $this->timestamp ) ) {
+ $this->timestamp = time();
+ $this->datetime = date("Y-m-d H:i:s", $this->timestamp);
+ } elseif( empty( $this->datetime ) && ! empty( $this->timestamp ) && is_numeric( $this->timestamp ) ) {
$this->datetime = date("Y-m-d H:i:s", $this->timestamp); //get datetime from timestamp
- elseif(empty($this->datetime) && !empty($this->timestamp))
+ } elseif( empty( $this->datetime ) && ! empty( $this->timestamp ) ) {
$this->datetime = $this->timestamp; //must have a datetime in it
+ $this->timestamp = strtotime( $this->datetime ); //fixing the timestamp
+ }
if(empty($this->notes))
$this->notes = "";
@@ -1184,4 +1193,35 @@ function get_test_order() {
return apply_filters( 'pmpro_test_order_data', $this );
}
+
+ /**
+ * Does this order have any billing address fields set?
+ * @since 2.8
+ * @return bool True if ANY billing address field is non-empty.
+ * False if ALL billing address fields are empty.
+ */
+ function has_billing_address() {
+ // This is sometimes set.
+ if ( ! empty( $this->Address1 ) ) {
+ return true;
+ }
+
+ // Avoid a warning if no billing object at all.
+ if ( empty( $this->billing ) ) {
+ return false;
+ }
+
+ // Check billing fields.
+ if ( ! empty( $this->billing->name )
+ || ! empty( $this->billing->street )
+ || ! empty( $this->billing->city )
+ || ! empty( $this->billing->state )
+ || ! empty( $this->billing->country )
+ || ! empty( $this->billing->zip )
+ || ! empty( $this->billing->phone ) ) {
+ return true;
+ }
+
+ return false;
+ }
} // End of Class
\ No newline at end of file
diff --git a/classes/class.pmproemail.php b/classes/class.pmproemail.php
index 6e0a2d94c..99d1371dc 100644
--- a/classes/class.pmproemail.php
+++ b/classes/class.pmproemail.php
@@ -241,6 +241,139 @@ function sendCancelAdminEmail($user = NULL, $old_level_id = NULL)
return $this->sendEmail();
}
+
+ function sendRefundedEmail( $user = NULL, $invoice = NULL ) {
+ global $wpdb, $current_user;
+ if ( ! $user ) {
+ $user = $current_user;
+ }
+
+ if ( ! $user ) {
+ return false;
+ }
+
+ $membership_level = pmpro_getSpecificMembershipLevelForUser( $user->ID, $invoice->membership_id );
+ if ( ! empty( $membership_level ) ) {
+ $membership_level_id = $membership_level->id;
+ $membership_level_name = $membership_level->name;
+ } else {
+ $membership_level_id = '';
+ $membership_level_name = __( 'N/A', 'paid-memberships-pro' );
+ }
+
+ $this->email = $user->user_email;
+ $this->subject = sprintf(__( 'Your invoice for order #%s at %s has been REFUNDED', 'paid-memberships-pro' ), $invoice->code, get_option( 'blogname' ) );
+
+ $this->data = array(
+ 'user_login' => $user->user_login,
+ 'user_email' => $user->user_email,
+ 'display_name' => $user->display_name,
+ 'sitename' => get_option('blogname'),
+ 'siteemail' => pmpro_getOption('from_email'),
+ 'login_link' => pmpro_login_url(),
+ 'login_url' => pmpro_login_url(),
+ 'membership_id' => $membership_level_id,
+ 'membership_level_name' => $membership_level_name,
+ 'invoice_id' => $invoice->code,
+ 'invoice_total' => pmpro_formatPrice($invoice->total),
+ 'invoice_date' => date_i18n(get_option('date_format'), $invoice->getTimestamp()),
+ 'billing_name' => $invoice->billing->name,
+ 'billing_street' => $invoice->billing->street,
+ 'billing_city' => $invoice->billing->city,
+ 'billing_state' => $invoice->billing->state,
+ 'billing_zip' => $invoice->billing->zip,
+ 'billing_country' => $invoice->billing->country,
+ 'billing_phone' => $invoice->billing->phone,
+ 'cardtype' => $invoice->cardtype,
+ 'accountnumber' => hideCardNumber($invoice->accountnumber),
+ 'expirationmonth' => $invoice->expirationmonth,
+ 'expirationyear' => $invoice->expirationyear,
+ 'login_link' => pmpro_login_url(),
+ 'login_url' => pmpro_login_url(),
+ 'invoice_link' => pmpro_login_url( pmpro_url( 'invoice', '?invoice=' . $invoice->code ) ),
+ 'invoice_url' => pmpro_login_url( pmpro_url( 'invoice', '?invoice=' . $invoice->code ) )
+ );
+ $this->data['billing_address'] = pmpro_formatAddress(
+ $invoice->billing->name,
+ $invoice->billing->street,
+ "", //address 2
+ $invoice->billing->city,
+ $invoice->billing->state,
+ $invoice->billing->zip,
+ $invoice->billing->country,
+ $invoice->billing->phone
+ );
+
+ $this->template = apply_filters( 'pmpro_email_template', 'refund', $this );
+ return $this->sendEmail();
+ }
+
+ function sendRefundedAdminEmail( $user = NULL, $invoice = NULL ) {
+ global $wpdb, $current_user;
+ if ( ! $user ) {
+ $user = $current_user;
+ }
+
+ if ( ! $user ) {
+ return false;
+ }
+
+ $membership_level = pmpro_getSpecificMembershipLevelForUser( $user->ID, $invoice->membership_id );
+ if ( ! empty( $membership_level ) ) {
+ $membership_level_id = $membership_level->id;
+ $membership_level_name = $membership_level->name;
+ } else {
+ $membership_level_id = '';
+ $membership_level_name = __( 'N/A', 'paid-memberships-pro' );
+ }
+
+ $this->email = get_bloginfo( 'admin_email' );
+ $this->subject = sprintf(__( 'Invoice for order #%s at %s has been REFUNDED', 'paid-memberships-pro' ), $invoice->code, get_option( 'blogname' ) );
+
+ $this->data = array(
+ 'user_login' => $user->user_login,
+ 'user_email' => $user->user_email,
+ 'display_name' => $user->display_name,
+ 'sitename' => get_option('blogname'),
+ 'siteemail' => pmpro_getOption('from_email'),
+ 'login_link' => pmpro_login_url(),
+ 'login_url' => pmpro_login_url(),
+ 'membership_id' => $membership_level_id,
+ 'membership_level_name' => $membership_level_name,
+ 'invoice_id' => $invoice->code,
+ 'invoice_total' => pmpro_formatPrice($invoice->total),
+ 'invoice_date' => date_i18n(get_option('date_format'), $invoice->getTimestamp()),
+ 'billing_name' => $invoice->billing->name,
+ 'billing_street' => $invoice->billing->street,
+ 'billing_city' => $invoice->billing->city,
+ 'billing_state' => $invoice->billing->state,
+ 'billing_zip' => $invoice->billing->zip,
+ 'billing_country' => $invoice->billing->country,
+ 'billing_phone' => $invoice->billing->phone,
+ 'cardtype' => $invoice->cardtype,
+ 'accountnumber' => hideCardNumber($invoice->accountnumber),
+ 'expirationmonth' => $invoice->expirationmonth,
+ 'expirationyear' => $invoice->expirationyear,
+ 'login_link' => pmpro_login_url(),
+ 'login_url' => pmpro_login_url(),
+ 'invoice_link' => pmpro_login_url( pmpro_url( 'invoice', '?invoice=' . $invoice->code ) ),
+ 'invoice_url' => pmpro_login_url( pmpro_url( 'invoice', '?invoice=' . $invoice->code ) )
+ );
+ $this->data['billing_address'] = pmpro_formatAddress(
+ $invoice->billing->name,
+ $invoice->billing->street,
+ "", //address 2
+ $invoice->billing->city,
+ $invoice->billing->state,
+ $invoice->billing->zip,
+ $invoice->billing->country,
+ $invoice->billing->phone
+ );
+
+ $this->template = apply_filters( 'pmpro_email_template', 'refund_admin', $this );
+
+ return $this->sendEmail();
+ }
function sendCheckoutEmail($user = NULL, $invoice = NULL)
{
diff --git a/classes/gateways/class.pmprogateway_braintree.php b/classes/gateways/class.pmprogateway_braintree.php
index 3724e3049..cffd0bbd4 100644
--- a/classes/gateways/class.pmprogateway_braintree.php
+++ b/classes/gateways/class.pmprogateway_braintree.php
@@ -109,8 +109,15 @@ public static function dependencies()
function loadBraintreeLibrary()
{
//load Braintree library if it hasn't been loaded already (usually by another plugin using Braintree)
- if(!class_exists("\Braintree"))
+ if ( ! class_exists( "\Braintree" ) ) {
require_once( PMPRO_DIR . "/includes/lib/Braintree/lib/Braintree.php");
+ } else {
+ // Another plugin may have loaded the Braintree library already.
+ // Let's log the current Braintree Library info so that we know
+ // where to look if we need to troubleshoot library conflicts.
+ $previously_loaded_class = new \ReflectionClass( '\Braintree' );
+ pmpro_track_library_conflict( 'braintree', $previously_loaded_class->getFileName(), Braintree\Version::get() );
+ }
}
/**
diff --git a/classes/gateways/class.pmprogateway_paypal.php b/classes/gateways/class.pmprogateway_paypal.php
index 3ba461c09..d28150a48 100644
--- a/classes/gateways/class.pmprogateway_paypal.php
+++ b/classes/gateways/class.pmprogateway_paypal.php
@@ -902,7 +902,7 @@ function PPHttpPost($methodName_, $nvpStr_) {
/**
* Allow performing actions using the http post request's response.
*
- * @since TBD
+ * @since 2.8
*
* @param array $httpParsedResponseAr The parsed response.
* @param string $methodName_ The NVP API name.
diff --git a/classes/gateways/class.pmprogateway_paypalexpress.php b/classes/gateways/class.pmprogateway_paypalexpress.php
index b167372d4..ee9368bf1 100644
--- a/classes/gateways/class.pmprogateway_paypalexpress.php
+++ b/classes/gateways/class.pmprogateway_paypalexpress.php
@@ -71,6 +71,7 @@ static function init()
add_filter('pmpro_checkout_default_submit_button', array('PMProGateway_paypalexpress', 'pmpro_checkout_default_submit_button'));
add_action('http_api_curl', array('PMProGateway_paypalexpress', 'http_api_curl'), 10, 3);
}
+ add_filter( 'pmpro_process_refund_paypalexpress', array('PMProGateway_paypalexpress', 'process_refund' ), 10, 2 );
}
/**
@@ -1080,7 +1081,7 @@ function PPHttpPost( $methodName_, $nvpStr_ ) {
/**
* Allow performing actions using the http post request's response.
*
- * @since TBD
+ * @since 2.8
*
* @param array $httpParsedResponseAr The parsed response.
* @param string $methodName_ The NVP API name.
@@ -1090,7 +1091,65 @@ function PPHttpPost( $methodName_, $nvpStr_ ) {
return $httpParsedResponseAr;
}
- /**
+ /**
+ * Refunds an order (only supports full amounts)
+ *
+ * @param bool $succes Status of the refund (default: false)
+ * @param object $morder The Member Order Object
+ * @since 2.8
+ *
+ * @return bool Status of the processed refund
+ */
+ public static function process_refund( $success, $morder ){
+
+ //need a transaction id
+ if ( empty( $morder->payment_transaction_id ) ) {
+ return false;
+ }
+
+ $transaction_id = $morder->payment_transaction_id;
+
+ //Get the real transaction ID
+ if ( $transaction_id === $morder->subscription_transaction_id ) {
+ $transaction_id = $morder->Gateway->getRealPaymentTransactionId( $morder );
+ }
+
+ $httpParsedResponseAr = $morder->Gateway->PPHttpPost( 'RefundTransaction', '&TRANSACTIONID='.$transaction_id );
+
+ if ( 'success' === strtolower( $httpParsedResponseAr['ACK'] ) ) {
+
+ $success = true;
+
+ $morder->status = 'refunded';
+
+ global $current_user;
+
+ // translators: %1$s is the Transaction ID. %2$s is the user display name that initiated the refund.
+ $morder->notes = trim( $morder->notes . ' ' . sprintf( __('Admin: Order successfully refunded on %1$s for transaction ID %2$s by %3$s.', 'paid-memberships-pro' ), date_i18n('Y-m-d H:i:s'), $transaction_id, $current_user->display_name ) );
+
+ $user = get_user_by( 'id', $morder->user_id );
+ //send an email to the member
+ $myemail = new PMProEmail();
+ $myemail->sendRefundedEmail( $user, $morder );
+
+ //send an email to the admin
+ $myemail = new PMProEmail();
+ $myemail->sendRefundedAdminEmail( $user, $morder );
+
+ } else {
+ //The refund failed, so lets return the gateway message
+
+ // translators: %1$s is the Transaction ID. %1$s is the Gateway Error
+ $morder->notes = trim( $morder->notes .' '. sprintf( __( 'Admin: There was a problem processing a refund for transaction ID %1$s. Gateway Error: %2$s.', 'paid-memberships-pro' ), $transaction_id, $httpParsedResponseAr['L_LONGMESSAGE0'] ) );
+ }
+
+ $morder->SaveOrder();
+
+ return $success;
+
+ }
+
+ /**
* PAYPAL Function
* Send HTTP POST Request with uuid
*
diff --git a/classes/gateways/class.pmprogateway_paypalstandard.php b/classes/gateways/class.pmprogateway_paypalstandard.php
index 4d5be9507..432fff83e 100644
--- a/classes/gateways/class.pmprogateway_paypalstandard.php
+++ b/classes/gateways/class.pmprogateway_paypalstandard.php
@@ -659,7 +659,7 @@ function PPHttpPost($methodName_, $nvpStr_) {
/**
* Allow performing actions using the http post request's response.
*
- * @since TBD
+ * @since 2.8
*
* @param array $httpParsedResponseAr The parsed response.
* @param string $methodName_ The NVP API name.
diff --git a/classes/gateways/class.pmprogateway_stripe.php b/classes/gateways/class.pmprogateway_stripe.php
index 6c3f7d554..cdb91463d 100644
--- a/classes/gateways/class.pmprogateway_stripe.php
+++ b/classes/gateways/class.pmprogateway_stripe.php
@@ -14,6 +14,7 @@
use Stripe\WebhookEndpoint as Stripe_Webhook;
use Stripe\StripeClient as Stripe_Client; // Used for deleting webhook as of 2.4
use Stripe\Account as Stripe_Account;
+use Stripe\Checkout\Session as Stripe_Checkout_Session;
define( "PMPRO_STRIPE_API_VERSION", "2020-03-02" );
@@ -26,6 +27,7 @@
// loading plugin activation actions
add_action( 'activate_paid-memberships-pro', array( 'PMProGateway_stripe', 'pmpro_activation' ) );
add_action( 'deactivate_paid-memberships-pro', array( 'PMProGateway_stripe', 'pmpro_deactivation' ) );
+add_filter( 'pmpro_registered_crons', array( 'PMProGateway_stripe', 'register_cron' ) );
/**
* PMProGateway_stripe Class
@@ -78,6 +80,12 @@ public static function loadStripeLibrary() {
//load Stripe library if it hasn't been loaded already (usually by another plugin using Stripe)
if ( ! class_exists( "Stripe\Stripe" ) ) {
require_once( PMPRO_DIR . "/includes/lib/Stripe/init.php" );
+ } else {
+ // Another plugin may have loaded the Stripe library already.
+ // Let's log the current Stripe Library info so that we know
+ // where to look if we need to troubleshoot library conflicts.
+ $previously_loaded_class = new \ReflectionClass( 'Stripe\Stripe' );
+ pmpro_track_library_conflict( 'stripe', $previously_loaded_class->getFileName(), Stripe\Stripe::VERSION );
}
}
@@ -140,34 +148,42 @@ public static function init() {
$current_gateway = pmpro_getGateway();
// $_REQUEST['review'] here means the PayPal Express review pag
- if ( ( $default_gateway == "stripe" || $current_gateway == "stripe" ) && empty( $_REQUEST['review'] ) )
- {
- add_action( 'pmpro_after_checkout_preheader', array(
- 'PMProGateway_stripe',
- 'pmpro_checkout_after_preheader'
- ) );
-
- add_action( 'pmpro_billing_preheader', array( 'PMProGateway_stripe', 'pmpro_checkout_after_preheader' ) );
- add_filter( 'pmpro_checkout_order', array( 'PMProGateway_stripe', 'pmpro_checkout_order' ) );
- add_filter( 'pmpro_billing_order', array( 'PMProGateway_stripe', 'pmpro_checkout_order' ) );
+ if ( ( $default_gateway == "stripe" || $current_gateway == "stripe" ) && empty( $_REQUEST['review'] ) ) {
add_filter( 'pmpro_include_billing_address_fields', array(
'PMProGateway_stripe',
'pmpro_include_billing_address_fields'
) );
- add_filter( 'pmpro_include_cardtype_field', array(
- 'PMProGateway_stripe',
- 'pmpro_include_billing_address_fields'
- ) );
- add_filter( 'pmpro_include_payment_information_fields', array(
- 'PMProGateway_stripe',
- 'pmpro_include_payment_information_fields'
- ) );
- //make sure we clean up subs we will be cancelling after checkout before processing
- add_action( 'pmpro_checkout_before_processing', array(
- 'PMProGateway_stripe',
- 'pmpro_checkout_before_processing'
- ) );
+ if ( ! self::using_stripe_checkout() ) {
+ // On-site checkout flow.
+ add_action( 'pmpro_after_checkout_preheader', array(
+ 'PMProGateway_stripe',
+ 'pmpro_checkout_after_preheader'
+ ) );
+ add_filter( 'pmpro_include_cardtype_field', array(
+ 'PMProGateway_stripe',
+ 'pmpro_include_billing_address_fields'
+ ) );
+ add_action( 'pmpro_billing_preheader', array( 'PMProGateway_stripe', 'pmpro_checkout_after_preheader' ) );
+ add_filter( 'pmpro_checkout_order', array( 'PMProGateway_stripe', 'pmpro_checkout_order' ) );
+ add_filter( 'pmpro_billing_order', array( 'PMProGateway_stripe', 'pmpro_checkout_order' ) );
+ add_filter( 'pmpro_include_payment_information_fields', array(
+ 'PMProGateway_stripe',
+ 'pmpro_include_payment_information_fields'
+ ) );
+
+ //make sure we clean up subs we will be cancelling after checkout before processing
+ add_action( 'pmpro_checkout_before_processing', array(
+ 'PMProGateway_stripe',
+ 'pmpro_checkout_before_processing'
+ ) );
+ } else {
+ // Checkout flow for Stripe Checkout.
+ add_filter('pmpro_include_payment_information_fields', array('PMProGateway_stripe', 'show_stripe_checkout_pending_warning'));
+ add_filter('pmpro_checkout_before_change_membership_level', array('PMProGateway_stripe', 'pmpro_checkout_before_change_membership_level'), 10, 2);
+ add_filter('pmprommpu_gateway_supports_multiple_level_checkout', '__return_false', 10, 2);
+ add_action( 'pmpro_billing_preheader', array( 'PMProGateway_stripe', 'pmpro_billing_preheader_stripe_checkout' ) );
+ }
}
add_action( 'pmpro_payment_option_fields', array( 'PMProGateway_stripe', 'pmpro_set_up_apple_pay' ), 10, 2 );
@@ -177,6 +193,8 @@ public static function init() {
add_action( 'admin_init', array( 'PMProGateway_stripe', 'stripe_connect_save_options' ) );
add_action( 'admin_notices', array( 'PMProGateway_stripe', 'stripe_connect_show_errors' ) );
add_action( 'admin_notices', array( 'PMProGateway_stripe', 'stripe_connect_deauthorize' ) );
+
+ add_filter( 'pmpro_process_refund_stripe', array( 'PMProGateway_stripe', 'process_refund' ), 10, 2 );
}
/**
@@ -247,6 +265,14 @@ public static function getGatewayOptions() {
'stripe_payment_request_button',
);
+ if ( self::stripe_checkout_beta_enabled() ) {
+ $options[] = 'stripe_payment_flow'; // 'onsite' or 'checkout'
+ $options[] = 'stripe_update_billing_flow'; // 'onsite' or 'portal'
+ $options[] = 'stripe_checkout_billing_address'; //'auto' or 'required'
+ $options[] = 'stripe_tax'; // 'no', 'inclusive', 'exclusive'
+ $options[] = 'stripe_tax_id_collection_enabled'; // '0', '1'
+ }
+
return $options;
}
@@ -473,7 +499,93 @@ public static function pmpro_payment_option_fields( $values, $gateway ) {
echo ' style="display: none;"';
}
echo '> ' . sprintf( wp_kses( __( 'Optional: Offer PayPal Express as an option at checkout using the Add PayPal Express Add On .', 'paid-memberships-pro' ), $allowed_appe_html ), 'https://www.paidmembershipspro.com/add-ons/pmpro-add-paypal-express-option-checkout/?utm_source=plugin&utm_medium=pmpro-paymentsettings&utm_campaign=add-ons&utm_content=pmpro-add-paypal-express-option-checkout' ) . '
';
- } ?>
+ }
+ if ( ! self::stripe_checkout_beta_enabled() ) {
+ // Don't show Stripe Checkout settings if the beta is not enabled.
+ return;
+ }
+ ?>
+ style="display: none;">
+
+
+
+
+
+
+ style="display: none;">
+
+ :
+
+
+
+ >
+ >
+
+
+
+
+ style="display: none;">
+
+ :
+
+
+
+
+ selected="selected">
+
+
+
+
+ style="display: none;">
+
+ :
+
+
+
+
+ selected="selected">
+
+
+
+ style="display: none;">
+
+ :
+
+
+
+
+ selected="selected">
+ selected="selected">
+
+ array (
+ 'href' => array(),
+ 'target' => array(),
+ 'title' => array(),
+ ),
+ );
+ ?>
+ activate Stripe Tax in your Stripe dashboard. More information about Stripe Tax » ', 'paid-memberships-pro' ), $allowed_stripe_tax_description_html ), 'https://stripe.com/tax', 'https://dashboard.stripe.com/settings/tax/activate' ); ?>
+
+
+ style="display: none;">
+
+ :
+
+
+
+
+ selected="selected">
+
+
+
+
user_email ) ) {
- $remove[] = 'bemail';
- $bemail = $current_user->user_email;
- $bconfirmemail = $bemail;
- }
- //remove the fields
- foreach ( $remove as $field ) {
- unset( $fields[ $field ] );
- }
+ $remove = array_merge( $remove, [ 'bfirstname', 'blastname', 'baddress', 'bcity', 'bstate', 'bzipcode', 'bphone', 'bcountry', 'CardType' ] );
+ }
+
+ // If a user is logged in, don't require bemail either
+ if ( ! empty( $current_user->user_email ) ) {
+ $remove = array_merge( $remove, [ 'bemail' ] );
+ $bemail = $current_user->user_email;
+ $bconfirmemail = $bemail;
+ }
+
+ // If using Stripe Checkout, don't require card information.
+ if ( self::using_stripe_checkout() ) {
+ $remove = array_merge( $remove, [ 'CardType', 'AccountNumber', 'ExpirationMonth', 'ExpirationYear', 'CVV' ] );
+ }
+
+ // Remove the fields.
+ foreach ( $remove as $field ) {
+ unset( $fields[ $field ] );
}
return $fields;
@@ -955,6 +1063,23 @@ public static function pmpro_deactivation() {
wp_clear_scheduled_hook( 'pmpro_cron_stripe_subscription_updates' );
}
+ /**
+ * Register the cron we need for Stripe subscription updates.
+ *
+ * @since 2.8
+ *
+ * @param array $crons The list of registered crons for Paid Memberships Pro.
+ *
+ * @return array The list of registered crons for Paid Memberships Pro.
+ */
+ public static function register_cron( $crons ) {
+ $crons['pmpro_cron_stripe_subscription_updates'] = [
+ 'interval' => 'daily',
+ ];
+
+ return $crons;
+ }
+
/**
* Cron job for subscription updates.
*
@@ -1366,6 +1491,295 @@ public static function dependencies() {
return true;
}
+ /**
+ * Check if the user has opted into the Stripe Checkout beta.
+ *
+ * @return bool
+ */
+ public static function stripe_checkout_beta_enabled() {
+ return ( defined( 'PMPRO_STRIPE_CHECKOUT_BETA_ENABLED' ) && PMPRO_STRIPE_CHECKOUT_BETA_ENABLED );
+ }
+
+ /**
+ * Check if Stripe Checkout is being used.
+ *
+ * @return bool
+ */
+ public static function using_stripe_checkout() {
+ // While Stripe Checkout is in beta, only enable it if the constant is set.
+ if ( ! self::stripe_checkout_beta_enabled() ) {
+ return false;
+ }
+
+ return 'checkout' === pmpro_getOption( 'stripe_payment_flow' );
+ }
+
+ /**
+ * Show warning at checkout if Stripe Checkout is being used and
+ * the last order is pending.
+ *
+ * @since 2.8
+ *
+ * @param bool $show Whether to show the default payment information fields.
+ * @return bool
+ */
+ static function show_stripe_checkout_pending_warning($show)
+ {
+ global $gateway;
+
+ //show our submit buttons
+ ?>
+ style="display: none;">
+ getLastMemberOrder( get_current_user_id(), null, null, 'stripe' );
+ if ( ! empty( $last_order->id ) && $last_order->status === 'pending' ) {
+ ?>
+
+
+
+ gateway != 'stripe' ) {
+ return;
+ }
+
+ $morder->user_id = $user_id;
+ $morder->status = 'token';
+ $morder->saveOrder();
+
+ // Save some checkout information in the order so that we can access it when the payment is complete.
+ // Save the request variables.
+ $request_vars = $_REQUEST;
+ unset( $request_vars['password'] );
+ unset( $request_vars['password2'] );
+ unset( $request_vars['password2_copy'] );
+ update_pmpro_membership_order_meta( $morder->id, 'checkout_request_vars', $request_vars );
+
+ // Save the checkout level.
+ $pmpro_level_arr = (array) $pmpro_level;
+ update_pmpro_membership_order_meta( $morder->id, 'checkout_level', $pmpro_level_arr );
+
+ // Save the discount code.
+ $pmpro_discount_code_arr = (array) $discount_code;
+ update_pmpro_membership_order_meta( $morder->id, 'checkout_discount_code', $pmpro_discount_code_arr );
+
+ // Time to send the user to pay with Stripe!
+ $stripe = new PMProGateway_stripe();
+
+ // Let's first get the customer to charge.
+ $customer = $stripe->update_customer_at_checkout( $morder );
+ if ( empty( $customer ) ) {
+ // There was an issue creating/updating the Stripe customer.
+ // $order will have an error message.
+ pmpro_setMessage( __( 'Could not get customer. ', 'paid-memberships-pro' ) . $morder->error, 'pmpro_error', true );
+ return;
+ }
+
+ // Next, let's get the product being purchased.
+ $product_id = $stripe->get_product_id_for_level( $morder->membership_id );
+ if ( empty( $product_id ) ) {
+ // Something went wrong getting the product ID or creating the product.
+ // Show the user a general error message.
+ pmpro_setMessage( __( 'Could not get product ID.', 'paid-memberships-pro' ), 'pmpro_error', true );
+ return;
+ }
+
+ // Then, we need to build the line items array to charge.
+ $line_items = array();
+
+ // Used to calculate Stripe Connect fees.
+ $application_fee_percentage = self::get_application_fee_percentage();
+
+ // First, let's handle the initial payment.
+ if ( ! empty( $morder->InitialPayment ) ) {
+ $initial_subtotal = $morder->InitialPayment;
+ $initial_tax = $morder->getTaxForPrice( $initial_subtotal );
+ $initial_payment_amount = pmpro_round_price( (float) $initial_subtotal + (float) $initial_tax );
+ $initial_payment_price = $stripe->get_price_for_product( $product_id, $initial_payment_amount );
+ if ( is_string( $initial_payment_price ) ) {
+ // There was an error getting the price.
+ pmpro_setMessage( __( 'Could not get price for initial payment. ', 'paid-memberships-pro' ) . $initial_payment_price, 'pmpro_error', true );
+ return;
+ }
+ $line_items[] = array(
+ 'price' => $initial_payment_price->id,
+ 'quantity' => 1,
+ );
+ if ( ! empty( $application_fee_percentage ) ) {
+ $application_fee = floor( $initial_payment_price->unit_amount * $application_fee_percentage / 100 );
+ if ( ! empty( $application_fee ) ) {
+ $payment_intent_data = array(
+ 'application_fee_amount' => $application_fee,
+ );
+ }
+ }
+ }
+
+ // Now, let's handle the recurring payments.
+ if ( pmpro_isLevelRecurring( $morder->membership_level ) ) {
+ $recurring_subtotal = $morder->PaymentAmount;
+ $recurring_tax = $morder->getTaxForPrice( $recurring_subtotal );
+ $recurring_payment_amount = pmpro_round_price( (float) $recurring_subtotal + (float) $recurring_tax );
+ $recurring_payment_price = $stripe->get_price_for_product( $product_id, $recurring_payment_amount, $morder->BillingPeriod, $morder->BillingFrequency );
+ if ( is_string( $recurring_payment_price ) ) {
+ // There was an error getting the price.
+ pmpro_setMessage( __( 'Could not get price for recurring payment. ', 'paid-memberships-pro' ) . $recurring_payment_price, 'pmpro_error', true );
+ return;
+ }
+ $line_items[] = array(
+ 'price' => $recurring_payment_price->id,
+ 'quantity' => 1,
+ );
+ $subscription_data = array();
+
+ // Check if we can combine initial and recurring payments.
+ $filtered_trial_period_days = $stripe->calculate_trial_period_days( $morder );
+ if (
+ empty( $order->TrialBillingCycles ) && // Check if there is a trial period.
+ $filtered_trial_period_days === $stripe->calculate_trial_period_days( $morder, false ) && // Check if the trial period is the same as the filtered trial period.
+ ( ! empty( $initial_payment_amount ) && $initial_payment_amount === $recurring_payment_amount ) // Check if the initial payment and recurring payment prices are the same.
+ ) {
+ // We can combine the initial payment and the recurring payment.
+ array_shift( $line_items );
+ $payment_intent_data = null;
+ } else {
+ // We need to set the trial period days and send initial and recurring payments as separate line items.
+ $subscription_data['trial_period_days'] = $filtered_trial_period_days;
+ }
+
+ // Add application fee for Stripe Connect.
+ $application_fee_percentage = self::get_application_fee_percentage();
+ if ( ! empty( $application_fee_percentage ) ) {
+ $subscription_data['application_fee_percent'] = $application_fee_percentage;
+ }
+ }
+
+ // Set up tax and billing addres collection.
+ $automatic_tax = ( ! empty( pmpro_getOption( 'stripe_tax' ) ) && 'no' !== pmpro_getOption( 'stripe_tax' ) ) ? array(
+ 'enabled' => true,
+ ) : array(
+ 'enabled' => false,
+ );
+ $tax_id_collection = ! empty( pmpro_getOption( 'stripe_tax_id_collection_enabled' ) ) ? array(
+ 'enabled' => true,
+ ) : array(
+ 'enabled' => false,
+ );
+ $billing_address_collection = pmpro_getOption( 'stripe_checkout_billing_address' ) ?: 'auto';
+
+ // And let's send 'em to Stripe!
+ $checkout_session_params = array(
+ 'customer' => $customer->id,
+ 'line_items' => $line_items,
+ 'mode' => isset( $subscription_data ) ? 'subscription' : 'payment',
+ 'automatic_tax' => $automatic_tax,
+ 'tax_id_collection' => $tax_id_collection,
+ 'billing_address_collection' => $billing_address_collection,
+ 'customer_update' => array(
+ 'address' => 'auto',
+ 'name' => 'auto'
+ ),
+ 'success_url' => add_query_arg( 'level', $morder->membership_level->id, pmpro_url("confirmation" ) ),
+ 'cancel_url' => add_query_arg( 'level', $morder->membership_level->id, pmpro_url("checkout" ) ),
+ );
+ if ( ! empty( $subscription_data ) ) {
+ $checkout_session_params['subscription_data'] = $subscription_data;
+ } elseif ( ! empty( $payment_intent_data ) ) {
+ $checkout_session_params['payment_intent_data'] = $payment_intent_data;
+ }
+
+ try {
+ $checkout_session = Stripe_Checkout_Session::create( $checkout_session_params );
+ } catch ( Throwable $th ) {
+ // Error creating checkout session.
+ pmpro_setMessage( __( 'Could not create checkout session. ', 'paid-memberships-pro' ) . $th->getMessage(), 'pmpro_error', true );
+ return;
+ } catch ( Exception $e ) {
+ // Error creating checkout session.
+ pmpro_setMessage( __( 'Could not create checkout session. ', 'paid-memberships-pro' ) . $e->getMessage(), 'pmpro_error', true );
+ return;
+ }
+
+ // Save so that we can confirm the payment later.
+ update_pmpro_membership_order_meta( $morder->id, 'stripe_checkout_session_id', $checkout_session->id );
+ wp_redirect( $checkout_session->url );
+ exit;
+ }
+
+ /**
+ * If using Stripe Checkout, either redirect the user to the Stripe Customer
+ * portal or set up our update billing page with the onsite payment fields.
+ *
+ * @since 2.8
+ */
+ public static function pmpro_billing_preheader_stripe_checkout() {
+ if ( 'portal' === pmpro_getOption( 'stripe_update_billing_flow' ) ) {
+ // Send user to Stripe Customer Portal.
+ $user_order = new MemberOrder();
+ $user_order->getLastMemberOrder( null, array( 'success', 'pending' ) );
+
+ // Check whether the user's most recent order is a Stripe subscription.
+ if ( empty( $user_order->gateway ) || 'stripe' !== $user_order->gateway ) {
+ $error = __( 'Last order was not charged with Stripe.', 'paid-memberships-pro' );
+ }
+
+ if ( empty( $error ) ) {
+ $stripe = new PMProGateway_stripe();
+ $customer = $stripe->get_customer_for_user( $user_order->user_id );
+ if ( empty( $customer->id ) ) {
+ $error = __( 'Could not get Stripe customer for user.', 'paid-memberships-pro' );
+ }
+ }
+
+ if ( empty( $error ) ) {
+ $customer_portal_url = $stripe->get_customer_portal_url( $customer->id );
+ if ( ! empty( $customer_portal_url ) ) {
+ wp_redirect( $customer_portal_url );
+ exit;
+ }
+ $error = __( 'Could not get Customer Portal URL. This feature may not be set up in Stripe.', 'paid-memberships-pro' );
+ }
+
+ // There must have been an error while getting the customer portal URL. Show an error and let user update
+ // their billing info onsite.
+ pmpro_setMessage( $error . ' ' . __( 'Please contact the site administrator.', 'paid-memberships-pro' ), 'pmpro_alert', true );
+ }
+ // Disable Stripe Checkout functionality for the rest of this page load.
+ add_filter( 'pmpro_include_cardtype_field', array(
+ 'PMProGateway_stripe',
+ 'pmpro_include_billing_address_fields'
+ ), 15 );
+ add_action( 'pmpro_billing_preheader', array( 'PMProGateway_stripe', 'pmpro_checkout_after_preheader' ), 15 );
+ add_filter( 'pmpro_billing_order', array( 'PMProGateway_stripe', 'pmpro_checkout_order' ), 15 );
+ add_filter( 'pmpro_include_payment_information_fields', array(
+ 'PMProGateway_stripe',
+ 'pmpro_include_payment_information_fields'
+ ), 15 );
+ add_filter( 'option_pmpro_stripe_payment_flow', '__return_false' ); // Disable Stripe Checkout for rest of page load.
+ }
+
/****************************************
************ PUBLIC METHODS ************
****************************************/
@@ -1375,6 +1789,11 @@ public static function dependencies() {
* @since 1.4
*/
public function process( &$order ) {
+ if ( self::using_stripe_checkout() ) {
+ // If using Stripe Checkout, we will try to collect the payment later.
+ return true;
+ }
+
$payment_transaction_id = '';
$subscription_transaction_id = '';
@@ -1421,7 +1840,7 @@ public function process( &$order ) {
$payment_method = $this->get_payment_method( $order );
if ( empty( $payment_method ) ) {
// There was an issue getting the payment method.
- $order->error = __( 'Error retrieving payment method.', 'paid-memberships-pro' );
+ $order->error = __( 'Error retrieving payment method.', 'paid-memberships-pro' ) . empty( $order->error ) ? '' : ' ' . $order->error;
$order->shorterror = $order->error;
return false;
}
@@ -1674,7 +2093,7 @@ public function update( &$order ) {
$payment_method = $this->get_payment_method( $order );
if ( empty( $payment_method ) ) {
// There was an issue getting the payment method.
- $order->error = __( "Error retrieving payment method.", 'paid-memberships-pro' );
+ $order->error = __( 'Error retrieving payment method.', 'paid-memberships-pro' ) . empty( $order->error ) ? '' : ' ' . $order->error;
$order->shorterror = $order->error;
return false;
}
@@ -1749,6 +2168,26 @@ public function cancel( &$order, $update_status = true ) {
}
}
+ /**
+ * Get the URL for a customer's Stripe Customer Portal.
+ *
+ * @since 2.8
+ *
+ * @param string $customer_id Customer to get the URL for.
+ * @return string URL for customer portal, or empty String if not found.
+ */
+ public function get_customer_portal_url( $customer_id ) {
+ try {
+ $session = \Stripe\BillingPortal\Session::create([
+ 'customer' => $customer_id,
+ 'return_url' => pmpro_url( 'account' ),
+ ]);
+ return $session->url;
+ } catch ( Exception $e ) {
+ return '';
+ }
+ }
+
/****************************************
*********** PRIVATE METHODS ************
@@ -2111,6 +2550,9 @@ function pmpro_user_register_stripe_customerid( $user_id ) {
}
add_action( "user_register", "pmpro_user_register_stripe_customerid" );
}
+ } else {
+ // User already exists. Update their Stripe customer ID.
+ update_user_meta( $user_id, 'pmpro_stripe_customerid', $customer->id );
}
/**
@@ -2301,6 +2743,12 @@ private function get_price_for_product( $product_id, $amount, $cycle_period = nu
$cycle_period = strtolower( $cycle_period );
+ // Only for use with Stripe Checkout.
+ $tax_behavior = pmpro_getOption( 'stripe_tax' );
+ if ( ! self::using_stripe_checkout() || empty( $tax_behavior ) ) {
+ $tax_behavior = 'no';
+ }
+
$price_search_args = array(
'product' => $product_id,
'type' => $is_recurring ? 'recurring' : 'one_time',
@@ -2313,31 +2761,34 @@ private function get_price_for_product( $product_id, $amount, $cycle_period = nu
try {
$prices = Stripe_Price::all( $price_search_args );
-
- foreach ( $prices as $price ) {
- // Check whether price is the same. If not, continue.
- if ( (int) $price->unit_amount !== $unit_amount ) {
- continue;
- }
- // Check if recurring structure is the same. If not, continue.
- if ( $is_recurring && ( empty( $price->recurring->interval_count ) || (int) $price->recurring->interval_count !== (int) $cycle_number ) ) {
- continue;
- }
- return $price;
- }
} catch (\Throwable $th) {
// There was an error listing prices.
- return $th->getMessage();
+ return $th->getMessage();;
} catch (\Exception $e) {
// There was an error listing prices.
return $e->getMessage();
}
+ foreach ( $prices as $price ) {
+ // Check whether price is the same. If not, continue.
+ if ( intval( $price->unit_amount ) !== intval( $unit_amount ) ) {
+ continue;
+ }
+ // Check if recurring structure is the same. If not, continue.
+ if ( $is_recurring && ( empty( $price->recurring->interval_count ) || intval( $price->recurring->interval_count ) !== intval( $cycle_number ) ) ) {
+ continue;
+ }
+ // Check if tax is enabled and set up correctly. If not, continue.
+ if ( 'no' !== $tax_behavior && $price->tax_behavior !== $tax_behavior ) {
+ continue;
+ }
+ return $price;
+ }
// Create a new Price.
$price_args = array(
'product' => $product_id,
'currency' => strtolower( $pmpro_currency ),
- 'unit_amount' => $unit_amount
+ 'unit_amount' => $unit_amount,
);
if ( $is_recurring ) {
$price_args['recurring'] = array(
@@ -2345,6 +2796,9 @@ private function get_price_for_product( $product_id, $amount, $cycle_period = nu
'interval_count' => $cycle_number
);
}
+ if ( 'no' !== $tax_behavior ) {
+ $price_args['tax_behavior'] = $tax_behavior;
+ }
try {
$price = Stripe_Price::create( $price_args );
@@ -2368,9 +2822,10 @@ private function get_price_for_product( $product_id, $amount, $cycle_period = nu
* @since 2.7.0.
*
* @param MemberOrder $order to calculate trial period days for.
+ * @param bool $filtered whether to filter the result.
* @return int trial period days.
*/
- private function calculate_trial_period_days( $order ) {
+ private function calculate_trial_period_days( $order, $filtered = true ) {
// Use a trial period to set the first recurring payment date.
if ( $order->BillingPeriod == "Year" ) {
$trial_period_days = $order->BillingFrequency * 365; //annual
@@ -2391,7 +2846,9 @@ private function calculate_trial_period_days( $order ) {
$order->ProfileStartDate = date_i18n( "Y-m-d\TH:i:s", strtotime( "+ " . $trial_period_days . " Day", current_time( "timestamp" ) ) );
//filter the start date
- $order->ProfileStartDate = apply_filters( "pmpro_profile_start_date", $order->ProfileStartDate, $order );
+ if ( $filtered ) {
+ $order->ProfileStartDate = apply_filters( "pmpro_profile_start_date", $order->ProfileStartDate, $order );
+ }
//convert back to days
$trial_period_days = ceil( abs( strtotime( date_i18n( "Y-m-d\TH:i:s" ), current_time( "timestamp" ) ) - strtotime( $order->ProfileStartDate, current_time( "timestamp" ) ) ) / 86400 );
@@ -2431,7 +2888,7 @@ private function create_subscription_for_customer_from_order( $customer_id, $ord
$subscription_params = array(
'customer' => $customer_id,
'items' => array(
- array( 'price' => $price ),
+ array( 'price' => $price->id ),
),
'trial_period_days' => $trial_period_days,
'expand' => array(
@@ -2731,8 +3188,6 @@ private function user_profile_fields_subscription_updates( $user, $customer ) {
refund( $order, $transaction_id );
}
+ /**
+ * Refunds an order (only supports full amounts)
+ *
+ * @param bool $success Status of the refund (default: false)
+ * @param object $order The Member Order Object
+ * @since 2.8
+ *
+ * @return bool Status of the processed refund
+ */
+ public static function process_refund( $success, $order ) {
+
+ //default to using the payment id from the order
+ if ( !empty( $order->payment_transaction_id ) ) {
+ $transaction_id = $order->payment_transaction_id;
+ }
+
+ //need a transaction id
+ if ( empty( $transaction_id ) ) {
+ return false;
+ }
+
+ //if an invoice ID is passed, get the charge/payment id
+ if ( strpos( $transaction_id, "in_" ) !== false ) {
+ $invoice = Stripe_Invoice::retrieve( $transaction_id );
+
+ if ( ! empty( $invoice ) && ! empty( $invoice->charge ) ) {
+ $transaction_id = $invoice->charge;
+ }
+ }
+
+ $success = false;
+
+ //attempt refund
+ try {
+
+ $secretkey = pmpro_getOption( 'stripe_secretkey' );
+
+ // If they are not using legacy keys, get Stripe Connect keys for the relevant environment.
+ if ( ! self::using_legacy_keys() && empty( $secretkey ) ) {
+ if ( pmpro_getOption( 'gateway_environment' ) === 'live' ) {
+ $secretkey = pmpro_getOption( 'live_stripe_connect_secretkey' );
+ } else {
+ $secretkey = pmpro_getOption( 'sandbox_stripe_connect_secretkey' );
+ }
+ }
+
+ $client = new Stripe_Client( $secretkey );
+ $refund = $client->refunds->create( [
+ 'charge' => $transaction_id,
+ ] );
+
+ //Make sure we're refunding an order that was successful
+ if ( $refund->status != 'failed' ) {
+ $order->status = 'refunded';
+
+ $success = true;
+
+ global $current_user;
+
+ $order->notes = trim( $order->notes.' '.sprintf( __('Admin: Order successfully refunded on %1$s for transaction ID %2$s by %3$s.', 'paid-memberships-pro' ), date_i18n('Y-m-d H:i:s'), $transaction_id, $current_user->display_name ) );
+
+ $user = get_user_by( 'id', $order->user_id );
+ //send an email to the member
+ $myemail = new PMProEmail();
+ $myemail->sendRefundedEmail( $user, $order );
+
+ //send an email to the admin
+ $myemail = new PMProEmail();
+ $myemail->sendRefundedAdminEmail( $user, $order );
+
+ } else {
+ $order->notes = trim( $order->notes . ' ' . __('Admin: An error occured while attempting to process this refund.', 'paid-memberships-pro' ) );
+ }
+
+ } catch ( \Throwable $e ) {
+ $order->notes = trim( $order->notes . ' ' . __( 'Admin: There was a problem processing the refund', 'paid-memberships-pro' ) . ' ' . $e->getMessage() );
+ } catch ( \Exception $e ) {
+ $order->notes = trim( $order->notes . ' ' . __( 'Admin: There was a problem processing the refund', 'paid-memberships-pro' ) . ' ' . $e->getMessage() );
+ }
+
+ $order->saveOrder();
+
+ return $success;
+ }
+
/**
* Refund a payment or invoice
*
@@ -4477,6 +5026,4 @@ public function create_plan( &$order ) {
return $order->plan;
}
-
-
}
diff --git a/classes/gateways/class.pmprogateway_twocheckout.php b/classes/gateways/class.pmprogateway_twocheckout.php
index 10dfac579..99ddc3907 100644
--- a/classes/gateways/class.pmprogateway_twocheckout.php
+++ b/classes/gateways/class.pmprogateway_twocheckout.php
@@ -9,8 +9,15 @@ class PMProGateway_Twocheckout extends PMProGateway
{
function __construct($gateway = NULL)
{
- if(!class_exists("Twocheckout"))
- require_once(dirname(__FILE__) . "/../../includes/lib/Twocheckout/Twocheckout.php");
+ if ( ! class_exists( "Twocheckout" ) ) {
+ require_once( dirname(__FILE__) . "/../../includes/lib/Twocheckout/Twocheckout.php" );
+ } else {
+ // Another plugin may have loaded the 2Checkout library already.
+ // Let's log the current 2Checkout Library info so that we know
+ // where to look if we need to troubleshoot library conflicts.
+ $previously_loaded_class = new \ReflectionClass( 'Twocheckout' );
+ pmpro_track_library_conflict( 'twocheckout', $previously_loaded_class->getFileName(), Twocheckout::VERSION );
+ }
//set API connection vars
Twocheckout::sellerId(pmpro_getOption('twocheckout_accountnumber'));
diff --git a/css/admin-rtl.css b/css/admin-rtl.css
index 6a8c2c893..b6912615e 100644
--- a/css/admin-rtl.css
+++ b/css/admin-rtl.css
@@ -1,7 +1,3 @@
-.pmpro_admin {
- background: url(../images/Paid-Memberships-Pro_watermark.png) bottom left no-repeat !important;
-}
-
.pmpro_admin .pmpro_banner h2 {
float: right;
}
@@ -29,7 +25,7 @@
/* discount levels */
/* pagination */
-div.pmpro_pagination {
+.pmpro_pagination {
float: left;
}
diff --git a/css/admin.css b/css/admin.css
index b3319a1fe..497dfb80b 100644
--- a/css/admin.css
+++ b/css/admin.css
@@ -6,7 +6,7 @@
border-bottom-width: 1px;
}
-/* icon */
+/* icons */
#wp-admin-bar-paid-memberships-pro .ab-item .ab-icon:before {
font-family: "dashicons";
content: "\f307";
@@ -14,63 +14,87 @@
.pmpro_admin tr td .dashicons {
padding-top: 5px;
}
+.pmpro_admin .pmpro-has-icon:before {
+ font: normal 16px/1 dashicons;
+ margin-right: 8px;
+ speak: never;
+ vertical-align: middle;
+ position: relative;
+ top: -2px;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+.pmpro_admin .pmpro-has-icon-printer:before {
+ content: "\f193";
+}
+.pmpro_admin .pmpro-has-icon-email:before {
+ content: "\f465";
+}
+.pmpro_admin .pmpro-has-icon-admin-users:before {
+ content: "\f110";
+}
+.pmpro_admin .pmpro-has-icon-image-rotate:before {
+ content: "\f531";
+}
+.pmpro_admin .pmpro-has-icon-download:before {
+ content: "\f316";
+ top: 0;
+}
+.pmpro_admin .pmpro-has-icon-plus:before {
+ content: "\f132";
+ top: 0;
+}
/* header/etc */
.pmpro_admin {
- background-image: url(../images/Paid-Memberships-Pro_watermark.png);
- background-position: bottom right;
- background-repeat: no-repeat;
- background-size: 290px 40px;
- padding: 1em 0 70px 0;
+ padding: 1em 0 0 0;
}
-.pmpro_admin .pmpro_banner {
- display: grid;
- grid-template-areas: "logo meta";
- grid-template-columns: 350px auto;
+.pmpro_banner {
+ display: flex;
+ padding: .5em 0;
}
-.pmpro_admin .pmpro_banner .pmpro_logo {
- grid-area: logo;
+.pmpro_banner .pmpro_logo {
+ margin-right: 10px;
}
-.pmpro_admin .pmpro_banner .pmpro_meta {
+.pmpro_banner .pmpro_meta {
align-self: center;
- grid-area: meta;
font-size: 16px;
line-height: 1.5;
}
-.pmpro_admin .pmpro_banner .pmpro_meta .pmpro_version {
- color: #333333;
- display: inline-block;
+.pmpro_banner .pmpro_meta .pmpro_version {
+ background: #DCDCDE;
+ color: #555;
+ border-radius: 999px;
+ font-size: 14px;
font-weight: bold;
- padding: 5px 10px 5px 5px;
+ padding: 6px 12px;
}
-.pmpro_admin .pmpro_banner .pmpro_meta a {
- border-left: 1px solid #CCC;
- padding: 5px 10px;
+.pmpro_banner .pmpro_meta a {
+ display: inline-block;
+ margin: 0 0 0 10px;
}
-.pmpro_admin .pmpro_banner .pmpro_meta a.pmpro_license_tag {
+.pmpro_banner .pmpro_meta a.pmpro_license_tag {
font-weight: bold;
- padding: 5px 10px 5px 5px;
text-decoration: none;
}
-.pmpro_admin .pmpro_banner .pmpro_meta a.pmpro_license_tag:before {
- bottom: 7px;
+.pmpro_banner .pmpro_meta a.pmpro_license_tag:before {
display: inline-block;
font: 400 20px/1 dashicons;
left: 0;
position: relative;
text-decoration: none;
- vertical-align: bottom;
+ vertical-align: middle;
}
-.pmpro_admin .pmpro_banner .pmpro_meta a.pmpro_license_tag.pmpro_license_tag-valid {
+.pmpro_banner .pmpro_meta a.pmpro_license_tag.pmpro_license_tag-valid {
color: rgb( 70, 180, 80 );
}
-.pmpro_admin .pmpro_banner .pmpro_meta a.pmpro_license_tag.pmpro_license_tag-valid:before {
+.pmpro_banner .pmpro_meta a.pmpro_license_tag.pmpro_license_tag-valid:before {
content: "\f147";
}
-.pmpro_admin .pmpro_banner .pmpro_meta a.pmpro_license_tag.pmpro_license_tag-invalid {
+.pmpro_banner .pmpro_meta a.pmpro_license_tag.pmpro_license_tag-invalid {
color: #AAA;
}
-.pmpro_admin .pmpro_banner .pmpro_meta a.pmpro_license_tag.pmpro_license_tag-invalid:before {
+.pmpro_banner .pmpro_meta a.pmpro_license_tag.pmpro_license_tag-invalid:before {
content: "\f335";
}
@@ -92,6 +116,10 @@
max-width: 200px;
height: auto;
}
+.pmpro_admin h2,
+.pmpro_admin h3 {
+ font-size: 18px;
+}
/* Scollable Boxes */
.pmpro_scrollable {
@@ -170,6 +198,18 @@
background-color: #f6f7f7;
}
+/* General admin-area tables styles */
+.pmpro_admin table.wp-list-table thead th {
+ padding-left: 1.25em;
+ padding-right: 1.25em;
+}
+.pmpro_admin table.wp-list-table tbody td {
+ padding: 1.25em;
+}
+.pmpro_admin table.wp-list-table .row-actions {
+ color: #999;
+}
+
/* levels */
.memberships_page_pmpro-membershiplevels .pmpro_admin #posts-filter p.search-box {
margin: 2em 0 1em 0;
@@ -217,36 +257,37 @@
}
.admin_page_pmpro-paymentsettings span.pmpro_gateway-mode {
- border: 1px solid;
+ border: 1px solid transparent;
border-radius: 3px;
display: inline-block;
- font-size: 75%;
+ font-size: 14px;
+ font-weight: 500;
margin-left: 5px;
- padding: 3px 5px;
+ padding: .25em .5em;
}
.admin_page_pmpro-paymentsettings span.pmpro_gateway-mode-live {
- background-color: #edfaef;
- border-color: #00a32a;
- color: #00a32a;
+ background-color: #d4edda;
+ border-color: #c3e6cb;
+ color: #0F441C;
}
.admin_page_pmpro-paymentsettings span.pmpro_gateway-mode-live.pmpro_gateway-mode-not-connected {
- background-color: #fcf0f1;
- border-color: #d63638;
- color: #d63638;
+ background-color: #f8d7da;
+ border-color: #f5c6cb;
+ color: #721c24;
}
.admin_page_pmpro-paymentsettings span.pmpro_gateway-mode-test {
- background-color: #fcf9e8;
- border-color: #dba617;
- color: #dba617;
+ background-color: #FFF2E0;
+ border-color: #F2E5D3;
+ color: #6B4201;
}
.admin_page_pmpro-paymentsettings span.pmpro_gateway-mode-test.pmpro_gateway-mode-not-connected {
- background-color: #f9f3d3;
- border-color: #d59a13;
- color: #d59a13;
+ background-color: #FFF8E0;
+ border-color: #ffeeba;
+ color: #6C5101;
}
.admin_page_pmpro-paymentsettings .pmpro-stripe-connect {
@@ -369,6 +410,62 @@
.memberships_page_pmpro-orders .pmpro_admin .nav-tab-wrapper {
margin-bottom: 10px;
}
+.pmpro_admin .wp-list-table .column-username,
+.pmpro_admin .wp-list-table .column-billing,
+.pmpro_admin .wp-list-table .column-transaction-ids {
+ word-break: break-word;
+}
+.pmpro_order-status {
+ border: 1px solid transparent;
+ border-radius: 5px;
+ display: block;
+ font-weight: 500;
+ padding: .25em .5em;
+ text-align: center;
+}
+.pmpro_order-status-success,
+.pmpro_order-status-cancelled {
+ background-color: #d4edda;
+ border-color: #c3e6cb;
+ color: #0F441C;
+}
+.pmpro_order-status-error,
+.pmpro_order-status-refunded {
+ background-color: #f8d7da;
+ border-color: #f5c6cb;
+ color: #721c24;
+}
+.pmpro_order-status-pending,
+.pmpro_order-status-review,
+.pmpro_order-status-token {
+ background-color: #FFF8E0;
+ border-color: #ffeeba;
+ color: #6C5101;
+}
+.wp-list-table .pmpro_order-renewal {
+ display: block;
+ margin-top: 8px;
+ text-align: center;
+}
+a.pmpro_order-renewal {
+ color: inherit;
+ text-decoration: none;
+}
+.pmpro_order-renewal:before {
+ content: "\f113";
+ font: normal 12px/1 dashicons;
+ margin-right: 3px;
+ vertical-align: middle;
+ position: relative;
+ top: -1px;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+.memberships_page_pmpro-orders #show_billing_action {
+ font-size: 14px;
+ font-weight: normal;
+ margin-left: 10px;
+}
/* members list */
.wp-list-table.members .column-ID {
@@ -385,42 +482,41 @@ tr.pmpro_settings_divider td {
}
/* admin pages */
-.pmpro_admin_section-email-templates-content .pmpro-email-templates-variable-reference .form-table td {
- padding: 5px;
-}
-.pmpro_admin_section-email-templates-content .pmpro-email-templates-variable-reference .form-table .widefat {
- margin-bottom: 20px;
+.pmpro_admin_section-email-templates-content .pmpro-email-templates-variable-reference table {
+ margin-bottom: 3em;
+ max-width: 900px;
}
/* messages */
.pmpro_admin .pmpro_message {
background: #FFF;
border-left: 4px solid #FFF;
+ color: #3c434a;
margin-right: 15px;
padding: 15px;
}
.pmpro_admin .pmpro_success {
- background-color: rgba( 70, 180, 80, 0.1 );
- border-left-color: rgb( 70, 180, 80 );
+ background-color: #d4edda;
+ border-left-color: #c3e6cb;
}
.pmpro_admin .pmpro_error,
.pmpro_admin tr.pmpro_error td {
- background-color: rgba( 220, 50, 50, 0.1 );
- border-left-color: rgb( 220, 50, 50 );
+ background-color: #f8d7da;
+ border-left-color: #f5c6cb;
}
.pmpro_admin .pmpro_alert {
- background-color: rgba( 255, 185, 0, 0.1 );
- border-left-color: rgb( 255, 185, 0 );
+ background-color: #FFF8E0;
+ border-left-color: #ffeeba;
}
.pmpro_admin .pmpro_success a {
- color: #208A1B;
+ color: #0F441C;
}
.pmpro_admin .pmpro_error a,
.pmpro_admin tr.pmpro_error td {
- color: #CC0000;
+ color: #721c24;
}
.pmpro_admin .pmpro_alert a {
- color: #CF8516;
+ color: #6C5101;
}
/* notifications */
@@ -487,37 +583,37 @@ tr.pmpro_alert {
}
/* pagination */
-div.pmpro_pagination {
- padding: 3px;
- margin: 5px 0px 5px 0px;
- font-size: 10px;
- float: right;
-}
-div.pmpro_pagination a {
- padding: 2px 5px 2px 5px;
- margin: 1px;
- border: 1px solid #666;
+.pmpro_pagination a,
+.pmpro_pagination span.current,
+.pmpro_pagination span.disabled {
+ display: inline-block;
+ vertical-align: baseline;
+ margin: 0 2px;
+ padding: 0 8px;
+ font-size: 14px;
+ line-height: 1.625;
+ text-align: center;
+ color: #2271b1;
+ border: 1px solid #2271b1;
+ background: #f6f7f7;
text-decoration: none;
- /* no underline */ color: #666;
- background: #EEE;
+ border-radius: 3px;
}
-div.pmpro_pagination a:hover, div.pmpro_pagination a:active {
- background: #FFF;
+.pmpro_pagination a:hover,
+.pmpro_pagination a:active {
+ background: #f0f0f1;
+ border-color: #0a4b78;
+ color: #0a4b78;
}
-div.pmpro_pagination span.current {
- border: 1px solid #FFF;
+.pmpro_pagination span.current {
+ background: #0a4b78;
+ border-color: #0a4b78;
color: #FFF;
- background: #666;
- padding: 2px 5px 2px 5px;
- margin: 1px;
- font-weight: bold;
}
-div.pmpro_pagination span.disabled {
- padding: 2px 5px 2px 5px;
- margin: 2px;
- border: 1px solid #BBB;
- color: #BBB;
- background: #EFEFEF;
+.pmpro_pagination span.disabled {
+ background: #f6f7f7;
+ border-color: #dcdcde;
+ color: #a7aaad;
}
p.pmpro_meta_notice {
@@ -616,6 +712,9 @@ p.pmpro_meta_notice {
.memberships_page_pmpro-license .pmpro_icon {
margin-top: 20px;
}
+.memberships_page_pmpro-license .about-wrap .about-text {
+ color: #3c434a;
+}
/* misc */
.pmpro_lite {
@@ -644,13 +743,9 @@ h2.nav-tab-wrapper {
.memberships_page_pmpro-reports .pmpro_admin .nav-tab-wrapper {
margin-bottom: 10px;
}
-.pmpro_reports-holder { }
-.pmpro_reports-holder .postbox h2 {
- border-bottom: 1px solid #eee;
- font-size: 14px;
- padding: 8px 12px;
- margin: 0;
- line-height: 1.4;
+.pmpro_report-holder table.wp-list-table thead th,
+.pmpro_report-holder table.wp-list-table tbody td {
+ padding: 8px 10px;
}
.pmpro_clickable {
cursor: pointer;
@@ -717,6 +812,7 @@ button.pmpro_report_th_closed:before {
background-color: #f6f7f7;
}
.pmpro_report-holder .pmpro_report-button {
+ margin-bottom: 0;
text-align: center;
}
.pmpro_chart_area {
diff --git a/css/blocks.editor.css b/css/blocks.editor.css
index b184c9487..59203ccc0 100644
--- a/css/blocks.editor.css
+++ b/css/blocks.editor.css
@@ -21,23 +21,27 @@
position: relative;
text-transform: uppercase;
}
+.pmpro-block-element hr {
+ margin-bottom: 16px;
+ margin-top: 16px;
+}
.pmpro-block-element .components-base-control {
margin-bottom: 16px;
}
.pmpro-block-element .components-base-control .components-select-control {
height: auto;
}
-.pmpro-block-element .components-base-control .components-base-control__label {
+..pmpro-block-element .components-base-control .components-base-control__label {
display: block;
}
/* Require Membership Block */
.pmpro-block-require-membership-element {
+ background: rgba(237, 239, 240, 0.8);
border-bottom: 5px solid #8d96a0;
border-top: 5px solid #8d96a0;
}
.pmpro-block-require-membership-element .pmpro-block-title {
- background: rgba(237, 239, 240, 0.8);
color: #555d66;
display: block;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
@@ -46,24 +50,36 @@
position: relative;
text-transform: uppercase;
}
-.pmpro-block-require-membership-element .components-panel__body {
- background: rgba(237, 239, 240, 0);
- border-bottom: 0;
+.pmpro-block-require-membership-element .components-panel__body,
+.pmpro-block-require-membership-element .components-panel__body.is-opened {
+ background: rgba(237, 239, 240, 0.8);
+ border-bottom: none;
border-top: none;
- border-left: 5px solid rgba(237, 239, 240, 0.8);
- border-bottom: 5px solid rgba(237, 239, 240, 0.8);
- padding-top: 0;
- height: 200px;
- overflow: auto;
+}
+.pmpro-block-require-membership-element .pmpro-block-inspector-scrollable {
+ border: 1px solid #CCC;
+ margin: 0px 16px 16px 16px;
}
.pmpro-block-require-membership-element .block-editor-inner-blocks {
- padding-left: 16px;
- padding-right: 16px;
+ background-color: #FFF;
+ padding: 16px;
}
.pmpro-block-inspector-scrollable {
- height: 200px;
+ height: 170px;
overflow: auto;
}
+.pmpro-block-inspector-scrollable::-webkit-scrollbar {
+ background-color: #c3c4c7;
+ width: 8px;
+ height: 8px;
+}
+.pmpro-block-inspector-scrollable::-webkit-scrollbar-thumb {
+ background: #2c3338;
+ border-radius: 5px;
+}
+.pmpro-block-inspector-scrollable .components-base-control.components-checkbox-control {
+ margin-bottom: 5px;
+}
/* Checkout Button Block */
.wp-block-pmpro-checkout-button {
diff --git a/includes/compatibility.php b/includes/compatibility.php
index ba040a2ef..6b6b01e2e 100644
--- a/includes/compatibility.php
+++ b/includes/compatibility.php
@@ -113,4 +113,49 @@ function pmpro_compatibility_checker_themes(){
}
-add_action( 'after_setup_theme', 'pmpro_compatibility_checker_themes' );
\ No newline at end of file
+add_action( 'after_setup_theme', 'pmpro_compatibility_checker_themes' );
+
+/**
+ * Keep track of plugins that load libraries before PMPro loads its version.
+ *
+ * @param string $name The name of the library.
+ * @param string $path The path of the loaded library.
+ * @param string $version The version of the loaded library.
+ *
+ * @since 2.8
+ */
+function pmpro_track_library_conflict( $name, $path, $version ) {
+ // Ignore when PMPro is trying to load.
+ if ( strpos( $path, '/plugins/paid-memberships-pro/' ) !== false ) {
+ return;
+ }
+
+ // Use a static var for timestamp so we can avoid multiple updates per pageload.
+ static $now = null;
+ if ( empty( $now ) ) {
+ $now = current_time( 'Y-m-d H:i:s' );
+ }
+
+ // Get the current list of library conflicts.
+ $library_conflicts = get_option( 'pmpro_library_conflicts', array() );
+
+ // Make sure we have an entry for this library.
+ if ( ! isset( $library_conflicts[ $name ] ) ) {
+ $library_conflicts[ $name ] = array();
+ }
+
+ // Make sure we have an entry for this path.
+ if ( ! isset( $library_conflicts[ $name ][ $path ] ) ) {
+ $library_conflicts[ $name ][ $path ] = array();
+ }
+
+ // Don't save conflict if no time has passed.
+ if ( ! empty( $library_conflicts[ $name ][ $path ]['timestamp'] ) && $library_conflicts[ $name ][ $path ]['timestamp'] === $now ) {
+ return;
+ }
+
+ // Update the library conflict information.
+ $library_conflicts[ $name ][ $path ]['version'] = $version;
+ $library_conflicts[ $name ][ $path ]['timestamp'] = $now;
+ update_option( 'pmpro_library_conflicts', $library_conflicts, false );
+}
\ No newline at end of file
diff --git a/includes/crons.php b/includes/crons.php
new file mode 100644
index 000000000..3ba60964a
--- /dev/null
+++ b/includes/crons.php
@@ -0,0 +1,124 @@
+ [
+ 'interval' => 'hourly',
+ ],
+ 'pmpro_cron_expiration_warnings' => [
+ 'interval' => 'hourly',
+ 'timestamp' => current_time( 'timestamp' ) + 1,
+ ],
+ 'pmpro_cron_credit_card_expiring_warnings' => [
+ 'interval' => 'monthly',
+ ],
+ 'pmpro_cron_admin_activity_email' => [
+ 'interval' => 'daily',
+ 'timestamp' => strtotime( '10:30:00' ) - ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ),
+ ],
+ 'pmpro_license_check_key' => [
+ 'interval' => 'monthly',
+ ],
+ ];
+
+ /**
+ * Allow filtering the list of registered crons for Paid Memberships Pro.
+ *
+ * @since 2.8
+ *
+ * @param array $crons The list of registered crons for Paid Memberships Pro.
+ */
+ $crons = (array) apply_filters( 'pmpro_registered_crons', $crons );
+
+ // Set up the default information for each cron if not set.
+ foreach ( $crons as $hook => $cron ) {
+ if ( empty( $cron['timestamp'] ) ) {
+ $cron['timestamp'] = current_time( 'timestamp' );
+ }
+
+ if ( empty( $cron['recurrence'] ) ) {
+ $cron['recurrence'] = 'hourly';
+ }
+
+ if ( empty( $cron['args'] ) ) {
+ $cron['args'] = [];
+ }
+
+ $crons[ $hook ] = $cron;
+ }
+
+ return $crons;
+}
+
+/**
+ * Maybe schedule our registered crons.
+ *
+ * @since 2.8
+ */
+function pmpro_maybe_schedule_crons() {
+ $crons = pmpro_get_crons();
+
+ foreach ( $crons as $hook => $cron ) {
+ pmpro_maybe_schedule_event( $cron['timestamp'], $cron['recurrence'], $hook, $cron['args'] );
+ }
+}
+
+/**
+ * Handle rescheduling Paid Memberships Pro crons when checking for ready cron tasks.
+ *
+ * @since 2.8
+ *
+ * @param null|array[] $pre Array of ready cron tasks to return instead. Default null
+ * to continue using results from _get_cron_array().
+ *
+ * @return null|array[] Array of ready cron tasks to return instead. Default null
+ * to continue using results from _get_cron_array().
+ */
+function pmpro_handle_schedule_crons_on_cron_ready_check( $pre ) {
+ pmpro_maybe_schedule_crons();
+
+ return $pre;
+}
+
+add_filter( 'pre_get_ready_cron_jobs', 'pmpro_handle_schedule_crons_on_cron_ready_check' );
+
+/**
+ * Schedule a periodic event unless one with the same hook is already scheduled.
+ *
+ * @since 2.8
+ *
+ * @see wp_schedule_event()
+ * @link https://developer.wordpress.org/reference/functions/wp_schedule_event/
+ *
+ * @param int $timestamp Unix timestamp (UTC) for when to next run the event.
+ * @param string $recurrence How often the event should subsequently recur.
+ * See wp_get_schedules() for accepted values.
+ * @param string $hook Action hook to execute when the event is run.
+ * @param array $args Optional. Array containing arguments to pass to the
+ * hook's callback function. Each value in the array
+ * is passed to the callback as an individual parameter.
+ * The array keys are ignored. Default empty array.
+ *
+ * @return bool|WP_Error True when an event is scheduled, WP_Error on failure, and false if the event was already scheduled.
+ */
+function pmpro_maybe_schedule_event( $timestamp, $recurrence, $hook, $args = [] ) {
+ $next = wp_next_scheduled( $hook, $args );
+
+ if ( empty( $next ) ) {
+ return wp_schedule_event( $timestamp, $recurrence, $hook, $args, true );
+ }
+
+ return false;
+}
diff --git a/includes/email-templates.php b/includes/email-templates.php
index b828beb1f..7880f4aee 100644
--- a/includes/email-templates.php
+++ b/includes/email-templates.php
@@ -498,7 +498,37 @@
Log in to your membership account here: !!login_url!!
', 'paid-memberships-pro' ),
'help_text' => __( 'This email is sent to the member when the trial portion of their membership level is approaching, at an interval based on the term of the trial.', 'paid-memberships-pro' )
),
+ 'refund' => array(
+ 'subject' => __( 'Your invoice for order #!!invoice_id!! at !!sitename!! has been REFUNDED', 'paid-memberships-pro' ),
+ 'description' => __('Refund', 'paid-memberships-pro'),
+ 'body' => __( 'Your invoice for order #!!invoice_id!! at !!sitename!! has been refunded.
+Account: !!display_name!! (!!user_email!!)
+
+ Invoice #!!invoice_id!! on !!invoice_date!!
+ Total Refunded: !!invoice_total!!
+
+
+Log in to your membership account here: !!login_url!!
+To view an online version of this invoice, click here: !!invoice_url!!
+
+If you did not request this refund and would like more information please contact us at !!siteemail!!
', 'paid-memberships-pro' ),
+ 'help_text' => __( 'This email is sent to the member as confirmation of a refunded payment. The email is sent after your membership site receives notification of a successful payment refund through your gateway.', 'paid-memberships-pro' )
+ ),
+ 'refund_admin' => array(
+ 'subject' => __( 'Invoice for order #!!invoice_id!! at !!sitename!! has been REFUNDED', 'paid-memberships-pro' ),
+ 'description' => __('Refund (admin)', 'paid-memberships-pro'),
+ 'body' => __( 'The invoice for order #!!invoice_id!! at !!sitename!! has been refunded.
+
+Account: !!display_name!! (!!user_email!!)
+
+ Invoice #!!invoice_id!! on !!invoice_date!!
+ Total Refunded: !!invoice_total!!
+
+
+Log in to your WordPress admin here: !!login_url!!
', 'paid-memberships-pro' ),
+ 'help_text' => __( 'This email is sent to the admin as confirmation of a refunded payment. The email is sent after your membership site receives notification of a successful payment refund through your gateway.', 'paid-memberships-pro' )
+ ),
);
// add SCA payment action required emails if we're using PMPro 2.1 or later
diff --git a/includes/functions.php b/includes/functions.php
index c7ab7c0fa..c34a77121 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -1510,7 +1510,7 @@ function pmpro_getPaginationString( $page = 1, $totalitems = 0, $limit = 15, $ad
*/
$pagination = '';
if ( $lastpage > 1 ) {
- $pagination .= '