This commit is contained in:
Jeremy Rangel
2025-02-06 09:05:18 -08:00
commit 60006e6188
11 changed files with 20590 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
node_modules

132
api.php Normal file
View File

@ -0,0 +1,132 @@
<?php
function get_all_user_roles() {
global $wp_roles;
if (!isset($wp_roles)) {
$wp_roles = new WP_Roles();
}
$roles = $wp_roles->roles;
$role_names = [];
foreach ($roles as $role_key => $role_data) {
$role_names[] = [
'key' => $role_key,
'name' => $role_data['name'],
];
}
return rest_ensure_response($role_names);
}
function is_admin_user() {
return true;
/* return current_user_can('manage_options'); */
}
function register_custom_user_roles_endpoint() {
register_rest_route('lcp-paywall/v1', '/user-roles', [
'methods' => 'GET',
'callback' => 'get_all_user_roles',
'permission_callback' => 'is_admin_user',
]);
}
add_action('rest_api_init', 'register_custom_user_roles_endpoint');
/* GET TAXONOMIES */
function get_taxonomies_by_post_type( $request ) {
$post_type = $request->get_param('post_type');
if (empty($post_type)) {
return new WP_Error('missing_post_type', 'Post type parameter is required', ['status' => 400]);
}
// Get taxonomies associated with the post type
$taxonomies = get_object_taxonomies($post_type, 'names'); // Use 'names' instead of 'objects' for simple list
// If no taxonomies found, return an error message
if (empty($taxonomies)) {
return new WP_Error('no_taxonomies', 'No taxonomies found for the given post type', ['status' => 404]);
}
// Fetch taxonomy objects for the specific taxonomies
$taxonomy_objects = [];
foreach ($taxonomies as $taxonomy) {
$taxonomy_objects[] = get_taxonomy($taxonomy); // Retrieve taxonomy object for each taxonomy
}
return rest_ensure_response($taxonomy_objects);
}
function register_taxonomies_endpoint() {
register_rest_route('lcp-paywall/v1', '/taxonomies', [
'methods' => 'GET',
'callback' => 'get_taxonomies_by_post_type',
'args' => [
'post_type' => [
'required' => true,
'type' => 'string',
],
],
'permission_callback' => '__return_true',
]);
}
add_action('rest_api_init', 'register_taxonomies_endpoint');
function get_terms_by_taxonomy_slug( $request ) {
// Get the taxonomy slug from the request parameters
$taxonomy_slug = $request->get_param( 'taxonomy' );
// Check if the taxonomy slug is provided
if ( empty( $taxonomy_slug ) ) {
return new WP_Error( 'missing_taxonomy', 'Taxonomy slug parameter is required', array( 'status' => 400 ) );
}
// Check if the taxonomy exists
if ( ! taxonomy_exists( $taxonomy_slug ) ) {
return new WP_Error( 'invalid_taxonomy', 'The provided taxonomy does not exist', array( 'status' => 404 ) );
}
// Get the terms for the taxonomy
$terms = get_terms( array(
'taxonomy' => $taxonomy_slug,
'hide_empty' => false, // Set to true to only get terms that have posts assigned
) );
// Check if terms were found
if ( is_wp_error( $terms ) ) {
return $terms;
}
if ( empty( $terms ) ) {
return new WP_Error( 'no_terms', 'No terms found for the given taxonomy', array( 'status' => 404 ) );
}
// Return the terms as a response
return rest_ensure_response( $terms );
}
function register_custom_taxonomy_terms_endpoint() {
// Register the custom REST route
register_rest_route( 'lcp-paywall/v1','/terms', array(
'methods' => 'GET',
'callback' => 'get_terms_by_taxonomy_slug',
'args' => array(
'taxonomy' => array(
'required' => true,
'validate_callback' => function( $param, $request, $key ) {
// Validate that the taxonomy exists
return taxonomy_exists( $param );
},
),
),
) );
}
add_action( 'rest_api_init', 'register_custom_taxonomy_terms_endpoint' );

1
build/index.asset.php Normal file
View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'wp-components', 'wp-element'), 'version' => '33ab959b55f1e87d13f0');

1
build/index.js Normal file

File diff suppressed because one or more lines are too long

2
index.php Normal file
View File

@ -0,0 +1,2 @@
<?php
//Silence is golden

149
lcp-paywall.php Normal file
View File

@ -0,0 +1,149 @@
<?php
/*
Plugin Name: LCP Paywall
Plugin URI: https://localcontentpro.com/paywall
Description: Flexible and customizable paywall solution for WordPress.
Author: Local Content Pro
Version: 0.0.1
Author URI: https://localcontentpro.com/
Tags: paywall, subscriptions, metered, membership, pay wall, content monetization, metered access, metered pay wall, paid content
Text Domain: lcp
*/
// If this file is called directly, abort.
if (!defined('WPINC')) {
die;
}
require_once plugin_dir_path(__FILE__) . 'api.php';
// Add menu page to the admin dashboard
add_action('admin_menu', 'lcp_paywall_add_admin_menu');
add_action('admin_enqueue_scripts', 'lcp_paywall_enqueue_admin_scripts');
function lcp_paywall_add_admin_menu() {
add_menu_page(
'LCP Paywall', // Page title
'LCP Paywall', // Menu title
'manage_options', // Capability required
'lcp-paywall', // Menu slug
'lcp_paywall_admin_page', // Function to output content
'dashicons-lock', // Icon (lock icon)
30 // Position in menu
);
}
function lcp_paywall_admin_page() {
// Check user capabilities
if (!current_user_can('manage_options')) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<div id="lcp-paywall-app-container">
<!-- React app will render here -->
</div>
</div>
<?php
}
function lcp_paywall_enqueue_admin_scripts($hook) {
// Only load on our admin page
if ('toplevel_page_lcp-paywall' !== $hook) {
return;
}
// Enqueue the built js file
wp_enqueue_script(
'lcp-paywall-admin',
plugins_url('build/index.js', __FILE__),
['wp-element', 'wp-components'], // Added wp-components dependency
filemtime(plugin_dir_path(__FILE__) . 'build/index.js'),
true
);
// Enqueue WordPress components styles
wp_enqueue_style('wp-components');
// Get all registered post types
$post_types = get_post_types(['public' => true], 'objects');
$post_types_data = [];
foreach ($post_types as $post_type) {
$post_types_data[] = [
'name' => $post_type->name,
'label' => $post_type->label,
'singular_label' => $post_type->labels->singular_name
];
}
// Pass data to JavaScript
wp_localize_script(
'lcp-paywall-admin',
'lcpPaywallData',
[
'postTypes' => $post_types_data,
'nonce' => wp_create_nonce('lcp_paywall_nonce'),
'ajaxUrl' => admin_url('admin-ajax.php')
]
);
// Add some basic styles
wp_add_inline_style(
'wp-admin',
'
.lcp-paywall-post-type {
margin: 20px 0;
padding: 20px;
background: #fff;
border: 1px solid #ccd0d4;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.lcp-paywall-post-type.is-over {
background: #f0f0f1;
}
.lcp-paywall-draggable-item {
padding: 15px;
margin: 5px 0;
background: #f8f9fa;
border: 1px dashed #ccd0d4;
cursor: move;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.lcp-paywall-draggable-item:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.lcp-paywall-draggable-item.is-dragging {
opacity: 0.5;
box-shadow: 0 5px 10px rgba(0,0,0,0.1);
}
.lcp-paywall-rule-controls {
display: flex;
gap: 10px;
align-items: flex-start;
}
.lcp-paywall-rule-controls .components-select-control {
margin-bottom: 0;
min-width: 120px;
}
.lcp-paywall-rule-controls .components-select-control__input {
height: 36px;
}
.lcp-paywall-add-rule {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #f0f0f1;
}
.lcp-paywall-add-rule-button.is-secondary {
margin-left: 0;
}
.lcp-paywall-rules-container {
min-height: 50px;
padding: 5px 0;
}
'
);
}

19903
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "lcp-paywall",
"version": "1.0.0",
"description": "LCP Paywall WordPress Plugin",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"check-engines": "wp-scripts check-engines"
},
"dependencies": {
"@wordpress/scripts": "^26.0.0",
"@wordpress/components": "^25.0.0",
"@wordpress/element": "^5.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1"
}
}

View File

@ -0,0 +1,237 @@
import { useDrag, useDrop } from 'react-dnd';
import { DateTimePicker,SelectControl, TextControl, __experimentalNumberControl as NumberControl } from '@wordpress/components';
import { useState, useEffect, useCallback } from '@wordpress/element';
const DraggableItem = ({ id, index, moveItem, postType, children }) => {
// Drag-and-drop hooks with memoization
const [{ isDragging }, drag] = useDrag(
useCallback(() => ({
type: 'rule',
item: { id, index },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}), [id, index])
);
const [, drop] = useDrop(
useCallback(() => ({
accept: 'rule',
hover: (item) => {
if (item.index !== index) {
moveItem(item.index, index);
item.index = index; // Keep track of the moved index
}
},
}), [index, moveItem])
);
// State management
const [ruleType, setRuleType] = useState('post');
const [lockType, setLockType] = useState('has_subscription');
const [operator, setOperator] = useState('=');
const [metaKey, setMetaKey] = useState('');
const [roles, setRoles] = useState([]); // State to store user roles
const [taxonomies, setTaxonomies] = useState([]); // State to store taxonomies
const [terms, setTerms] = useState([]); // State to store terms of a selected taxonomy
const [postContentLengthAmount, setPostContentLengthAmount] = useState(0);
const [postContentLengthUnit, setPostContentLengthUnit] = useState('characters');
const [postContentLengthOperator, setPostContentLengthOperator] = useState('=');
const [postDate,setPostDate] = useState('')
const [postDateOperator, setPostDateOperator] = useState('=');
const [metaKeyOperator, setMetaKeyOperator] = useState('=');
const [metaValue, setMetaValue] = useState('');
// Fetch user roles from the WordPress REST API
useEffect(() => {
if (lockType === 'has_role') {
fetch('/localcontentpro/wp-json/lcp-paywall/v1/user-roles') // Your custom REST API endpoint
.then((response) => response.json())
.then((data) => {
setRoles(data.map(role => ({ label: role.name, value: role.key })));
})
.catch((error) => console.error('Error fetching roles:', error));
}
}, [lockType]); // Fetch only when lockType is 'has_role'
// Fetch taxonomies for the current post type
useEffect(() => {
if ((lockType === 'in_term' || lockType === 'not_in_term') && postType) {
fetch(`/localcontentpro/wp-json/lcp-paywall/v1/taxonomies?post_type=${postType}`)
.then(response => response.json())
.then(data => {
const taxonomyOptions = Object.keys(data).map(taxonomy => ({
label: data[taxonomy].name,
value: taxonomy,
}));
setTaxonomies(taxonomyOptions);
})
.catch(error => console.error('Error fetching taxonomies:', error));
}
}, [lockType, postType]); // Fetch taxonomies when lockType is 'in_term' and postType changes
// Fetch terms when lockType is in_term or not_in_term and a taxonomy is selected
useEffect(() => {
if ((lockType === 'in_term' || lockType === 'not_in_term') && metaKey) {
fetch(`/localcontentpro/wp-json/lcp-paywall/v1/terms?taxonomy=${metaKey}`)
.then(response => response.json())
.then(data => {
const termOptions = data.map(term => ({
label: term.name,
value: term.id,
}));
setTerms(termOptions);
})
.catch(error => console.error('Error fetching terms:', error));
}
}, [lockType, metaKey]); // Fetch terms when lockType is 'in_term' or 'not_in_term' and metaKey (taxonomy) is selected
const postLockTypeOptions = [
{ value: 'post_content_length', label: 'Post Content Length' },
{ value: 'post_in', label: 'Post In' },
{value: 'post_not_in', label: 'Post Not In' },
{ value: 'meta_value', label: 'Meta Value' },
{ value: 'in_term', label: 'In Term' },
{ value: 'not_in_term', label: 'Not In Term' },
{ value: 'older_than', label: 'Older Than' },
{ value: 'newer_than', label: 'Newer Than' },
{ value: 'author_in', label: 'Author In' },
{ value: 'author_not_in', label: 'Author Not In' },
{ value: 'post_date', label: 'Post Date' }
];
const userLockTypeOptions = [
{ label: 'Has Subscription', value: 'has_subscription' },
{ label: 'Is Logged In', value: 'is_logged_in' },
{ label: 'Role In', value: 'role_in' },
{ label: 'Role Not In', value: 'role_not_in' },
{ label: 'Meta Value', value: 'meta_value' }
];
const userDeviceLockTypeOptions = [
{ label: 'Browser In', value: 'browser_in' },
{ label: 'Browser Not In', value: 'browser_not_in' },
{ label: 'Operating System In', value: 'os_in' },
{ label: 'Operating System Not In', value: 'os_not_in' }
];
const comparisonOperators = [
{ label: '=', value: '=' },
{ label: '!=', value: '!=' },
{ label: '>', value: '>' },
{ label: '<', value: '<' },
{ label: '>=', value: '>=' },
{ label: '<=', value: '<=' },
];
return (
<div
ref={(node) => drag(drop(node))}
className={`lcp-paywall-draggable-item ${isDragging ? 'is-dragging' : ''}`}
style={{ opacity: isDragging ? 0.5 : 1 }}
>
<div className="lcp-paywall-rule-controls">
<SelectControl
value={ruleType}
options={[
{ label: 'Current Post', value: 'post' },
{ label: 'Current User', value: 'user' },
{ label: 'Current User Device', value: 'user_device' },
]}
onChange={setRuleType}
/>
<SelectControl
value={lockType}
options={
ruleType === 'post' ? postLockTypeOptions : ruleType === 'user' ? userLockTypeOptions : userDeviceLockTypeOptions
}
onChange={setLockType}
/>
{lockType === 'meta_value' && (
<>
<TextControl
value={metaKey}
onChange={setMetaKey}
placeholder="Meta Key"
/>
<SelectControl
value={metaKeyOperator}
options={comparisonOperators}
onChange={setMetaKeyOperator}
/>
<TextControl
value={metaValue}
onChange={setMetaValue}
placeholder="Meta Value"
/>
</>
)}
{lockType === 'has_role' && (
<SelectControl
value={metaKey}
options={roles}
onChange={setMetaKey}
/>
)}
{(lockType === 'in_term' || lockType === 'not_in_term') && (
<>
<SelectControl
value={metaKey}
options={taxonomies}
onChange={setMetaKey}
/>
<SelectControl
value={metaKey}
options={terms}
onChange={setMetaKey}
/>
</>
)}
{lockType === 'post_content_length' && (
<>
<SelectControl
value={postContentLengthOperator}
options={comparisonOperators}
onChange={setPostContentLengthOperator}
/>
<NumberControl
value={postContentLengthAmount}
onChange={setPostContentLengthAmount}
/>
<SelectControl
value={postContentLengthUnit}
options={[
{ label: 'Characters', value: 'characters' },
{ label: 'Words', value: 'words' } ]}
onChange={setPostContentLengthUnit}
/>
</>
)}
{lockType === 'post_date' && (
<>
<SelectControl
value={postDateOperator}
options={comparisonOperators}
onChange={setPostDateOperator}
/>
<DateTimePicker
value={postDate}
onChange={setPostDate}
/>
</>
)}
</div>
{children}
</div>
);
};
export default DraggableItem;

View File

@ -0,0 +1,120 @@
import { useDrop } from 'react-dnd';
import { Button, ToggleControl, __experimentalNumberControl as NumberControl, SelectControl } from '@wordpress/components';
import { useState, useEffect, useCallback } from '@wordpress/element';
import DraggableItem from './DraggableItem';
const PostTypeContainer = ({ postType }) => {
const [rules, setRules] = useState([{ id: Date.now() }]);
const [lockDefault, setLockDefault] = useState(false);
const [enableMeter, setEnableMeter] = useState(false);
const [meterAmount, setMeterAmount] = useState(0);
const [meterDuration, setMeterDuration] = useState(1);
const [meterCookieUnits, setMeterCookieUnits] = useState('hour');
const [totalSeconds, setTotalSeconds] = useState(3600);
const [{ isOver }, drop] = useDrop(() => ({
accept: 'rule',
drop: () => ({ postType: postType.name }),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
}));
const moveItem = useCallback((dragIndex, hoverIndex) => {
setRules((prevRules) => {
const newRules = [...prevRules];
const dragItem = newRules[dragIndex];
// Remove the dragged item
newRules.splice(dragIndex, 1);
// Insert it at the new position
newRules.splice(hoverIndex, 0, dragItem);
return newRules;
});
}, []);
const addRule = () => {
setRules([...rules, { id: Date.now() }]);
};
const calculateTotalSeconds = (duration, unit) => {
const unitToSeconds = {
second: 1,
minute: 60,
hour: 3600,
day: 86400,
week: 604800,
};
return duration * (unitToSeconds[unit] || 0);
};
// Update total seconds whenever duration or unit changes
useEffect(() => {
setTotalSeconds(calculateTotalSeconds(meterDuration, meterCookieUnits));
}, [meterDuration, meterCookieUnits]);
return (
<div className={`lcp-paywall-post-type ${isOver ? 'is-over' : ''}`} ref={drop}>
<h3>{postType.label}</h3>
<ToggleControl
label="Lock Posts By Default"
checked={lockDefault}
onChange={setLockDefault}
/>
<ToggleControl
label="Enable Meter"
checked={enableMeter}
onChange={setEnableMeter}
/>
{enableMeter && (
<>
<NumberControl
label="Meter Amount"
value={meterAmount}
onChange={setMeterAmount}
/>
<NumberControl
label="Meter Cookie Duration"
value={meterDuration}
onChange={setMeterDuration}
/>
<SelectControl
label="Meter Cookie Units"
value={meterCookieUnits}
options={[
{ label: 'Minutes', value: 'minute' },
{ label: 'Hours', value: 'hour' },
{ label: 'Days', value: 'day' },
{ label: 'Weeks', value: 'week' },
]}
onChange={setMeterCookieUnits}
/>
<p><strong>Total Seconds:</strong> {totalSeconds}</p>
</>
)}
<div className="lcp-paywall-rules-container">
{rules.map((rule, index) => (
<DraggableItem
key={rule.id}
id={rule.id}
index={index}
moveItem={moveItem}
postType={postType.name}
/>
))}
</div>
<div className="lcp-paywall-add-rule">
<Button
variant="secondary"
onClick={addRule}
className="lcp-paywall-add-rule-button"
>
Add Rule
</Button>
</div>
</div>
);
};
export default PostTypeContainer;

24
src/index.js Normal file
View File

@ -0,0 +1,24 @@
import { render } from '@wordpress/element';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import PostTypeContainer from './components/PostTypeContainer';
const App = () => {
return (
<DndProvider backend={HTML5Backend}>
<div id="lcp-paywall-app">
{window.lcpPaywallData.postTypes.map(postType => (
<PostTypeContainer key={postType.name} postType={postType} />
))}
</div>
</DndProvider>
);
};
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('lcp-paywall-app-container');
if (container) {
render(<App />, container);
}
});