Skip to content

Instantly share code, notes, and snippets.

@ara303
Created September 1, 2025 23:04
Show Gist options
  • Save ara303/b8b9e308accc4939368bb23c421b368f to your computer and use it in GitHub Desktop.
Save ara303/b8b9e308accc4939368bb23c421b368f to your computer and use it in GitHub Desktop.
dogshit gpt5 subscription system
<?php
/**
* Plugin Name: Woo Subscriptions: Shared Access + Renewal Notices
* Description: Extend WooCommerce Subscriptions so multiple users share one subscription’s benefits; includes admin UI to assign members and a renewal notice hook framework.
* Author: GPT
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* License: GPLv2 or later
*
* Assumptions:
* - WooCommerce and WooCommerce Subscriptions are active.
* - Relationship storage: subscription post meta `_wcss_shared_members` holds an array of user IDs.
* - “Owner” = the subscription’s customer user. “Members” = additional linked users. The UI avoids the words “Parent/Child.”
*/
if( ! defined( 'ABSPATH' ) ){ exit; }
if( ! class_exists( 'WCSS_Shared_Subscriptions' ) ){
final class WCSS_Shared_Subscriptions {
const META_KEY_MEMBERS = '_wcss_shared_members';
const CRON_HOOK_BEFORE = 'wcss_subscription_renewal_notice_before';
const CRON_HOOK_AFTER = 'wcss_subscription_renewal_notice_after';
const ACTION_NOTICE = 'wcss_subscription_renewal_notice'; // ($subscription_id, $when) where $when = 'before'|'after'
const NONCE_ACTION = 'wcss_assign_members';
const CAPABILITY = 'manage_options';
private static $instance = null;
public static function instance(){
if( self::$instance === null ){
self::$instance = new self();
}
return self::$instance;
}
private function __construct(){
add_action( 'plugins_loaded', array( $this, 'maybe_boot' ), 5 );
}
public function maybe_boot(){
if( ! function_exists( 'wcs_get_subscription' ) ){
add_action( 'admin_notices', array( $this, 'admin_notice_missing_wcs' ) );
return;
}
// Public API: helper functions
require_once __FILE__; // self-contained single file; helpers defined at bottom guarded by function_exists.
// Admin UI
add_action( 'admin_menu', array( $this, 'register_admin_ui' ) );
add_action( 'admin_post_wcss_save_members', array( $this, 'handle_save_members' ) );
// Ensure members can "benefit" in common checks
add_filter( 'woocommerce_can_subscription_be_updated_to', array( $this, 'allow_members_to_view_update' ), 10, 3 );
add_filter( 'woocommerce_current_user_can_view_subscription', array( $this, 'extend_view_caps' ), 10, 2 );
// Schedule notices on relevant changes
add_action( 'woocommerce_subscription_status_updated', array( $this, 'reschedule_notices_on_status_change' ), 10, 3 );
add_action( 'woocommerce_subscription_date_updated', array( $this, 'reschedule_notices_on_date_change' ), 10, 3 );
add_action( 'woocommerce_scheduled_subscription_payment', array( $this, 'reschedule_notices_generic' ), 10, 1 );
// Cron handlers -> unify through a single action for reusability
add_action( self::CRON_HOOK_BEFORE, array( $this, 'emit_notice_before' ), 10, 1 );
add_action( self::CRON_HOOK_AFTER, array( $this, 'emit_notice_after' ), 10, 1 );
// Example listener wiring (no-op email framework; developers can hook their own)
add_action( self::ACTION_NOTICE, array( $this, 'default_email_listener' ), 10, 2 );
// Activation/Deactivation
register_activation_hook( __FILE__, array( __CLASS__, 'activate' ) );
register_deactivation_hook( __FILE__, array( __CLASS__, 'deactivate' ) );
}
public function admin_notice_missing_wcs(){
if( current_user_can( self::CAPABILITY ) ){
echo '<div class="notice notice-error"><p><strong>Woo Subscriptions: Shared Access + Renewal Notices</strong> requires WooCommerce Subscriptions. Activate it.</p></div>';
}
}
/* =========================
* Admin UI
* ========================= */
public function register_admin_ui(){
add_submenu_page(
'woocommerce',
'Shared Subscriptions',
'Shared Subscriptions',
self::CAPABILITY,
'wcss-shared-subscriptions',
array( $this, 'render_admin_page' )
);
}
public function render_admin_page(){
if( ! current_user_can( self::CAPABILITY ) ){ wp_die( 'Access denied.' ); }
$sub_id = isset( $_GET['subscription_id'] ) ? absint( $_GET['subscription_id'] ) : 0;
$subscription = $sub_id ? wcs_get_subscription( $sub_id ) : false;
$members = array();
$owner_id = 0;
if( $subscription ){
$owner_id = (int) $subscription->get_user_id();
$members = get_post_meta( $subscription->get_id(), self::META_KEY_MEMBERS, true );
if( ! is_array( $members ) ){ $members = array(); }
}
?>
<div class="wrap">
<h1>Shared Subscriptions</h1>
<form method="get" action="">
<input type="hidden" name="page" value="wcss-shared-subscriptions" />
<p>
<label for="wcss-sub-id">Subscription ID</label>
<input type="number" min="1" id="wcss-sub-id" name="subscription_id" value="<?php echo esc_attr( $sub_id ); ?>" />
<?php submit_button( 'Load', 'secondary', '', false ); ?>
</p>
</form>
<?php if( $subscription ): ?>
<hr />
<h2>Subscription #<?php echo esc_html( $subscription->get_id() ); ?></h2>
<p><strong>Owner account:</strong> <?php echo esc_html( $this->user_display( $owner_id ) ); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<?php wp_nonce_field( self::NONCE_ACTION ); ?>
<input type="hidden" name="action" value="wcss_save_members" />
<input type="hidden" name="subscription_id" value="<?php echo esc_attr( $subscription->get_id() ); ?>" />
<table class="widefat striped">
<thead>
<tr>
<th style="width:40%">Members (user IDs or emails)</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<textarea name="wcss_members" rows="6" style="width:100%" placeholder="Enter one per line"><?php
$prefill = array();
foreach( $members as $uid ){
$u = get_user_by( 'id', (int) $uid );
$prefill[] = $u ? $u->user_email : (string) $uid;
}
echo esc_textarea( implode( "\n", $prefill ) );
?></textarea>
</td>
<td>
<p>Owner is included automatically. Owner cannot be removed here.</p>
<p>Lines can be user IDs or emails. Non-existent emails are ignored.</p>
</td>
</tr>
</tbody>
</table>
<?php submit_button( 'Save Members' ); ?>
</form>
<?php elseif( $sub_id ): ?>
<p>No subscription found.</p>
<?php endif; ?>
</div>
<?php
}
private function user_display( $user_id ){
$u = get_user_by( 'id', (int) $user_id );
if( ! $u ){ return 'Unknown user'; }
return sprintf( '%s (#%d, %s)', $u->display_name, $u->ID, $u->user_email );
}
public function handle_save_members(){
if( ! current_user_can( self::CAPABILITY ) ){ wp_die( 'Access denied.' ); }
check_admin_referer( self::NONCE_ACTION );
$sub_id = isset( $_POST['subscription_id'] ) ? absint( $_POST['subscription_id'] ) : 0;
$subscription = $sub_id ? wcs_get_subscription( $sub_id ) : false;
if( ! $subscription ){ wp_redirect( add_query_arg( array( 'page' => 'wcss-shared-subscriptions', 'subscription_id' => $sub_id, 'updated' => 0 ), admin_url( 'admin.php' ) ) ); exit; }
$lines = isset( $_POST['wcss_members'] ) ? explode( "\n", wp_unslash( $_POST['wcss_members'] ) ) : array();
$clean_ids = array();
foreach( $lines as $raw ){
$raw = trim( $raw );
if( $raw === '' ){ continue; }
if( is_numeric( $raw ) ){
$uid = absint( $raw );
if( $uid && get_user_by( 'id', $uid ) ){ $clean_ids[] = $uid; }
continue;
}
if( is_email( $raw ) ){
$u = get_user_by( 'email', $raw );
if( $u ){ $clean_ids[] = (int) $u->ID; }
}
}
$clean_ids = array_values( array_unique( array_filter( array_map( 'absint', $clean_ids ) ) ) );
// Ensure owner is not duplicated in members list
$owner_id = (int) $subscription->get_user_id();
$clean_ids = array_values( array_diff( $clean_ids, array( $owner_id ) ) );
update_post_meta( $subscription->get_id(), self::META_KEY_MEMBERS, $clean_ids );
$this->reschedule_notices_generic( $subscription->get_id() );
wp_redirect( add_query_arg( array( 'page' => 'wcss-shared-subscriptions', 'subscription_id' => $subscription->get_id(), 'updated' => 1 ), admin_url( 'admin.php' ) ) );
exit;
}
/* =========================
* Permission Extensions
* ========================= */
public function extend_view_caps( $can_view, $subscription_id ){
if( $can_view ){ return true; }
$user_id = get_current_user_id();
if( ! $user_id ){ return $can_view; }
return wcss_user_is_linked_to_subscription( $subscription_id, $user_id );
}
public function allow_members_to_view_update( $can, $new_status, $subscription ){
// Keep default capability checks intact for updates; this ensures we don't over-grant mutating powers.
return $can;
}
/* =========================
* Renewal Notice Scheduling
* ========================= */
public static function activate(){
// Nothing mandatory; developers may bulk schedule via WP-CLI if needed.
}
public static function deactivate(){
// Clear all scheduled hooks for cleanliness.
$crons = _get_cron_array();
if( is_array( $crons ) ){
foreach( $crons as $timestamp => $hooks ){
foreach( $hooks as $hook => $events ){
if( in_array( $hook, array( self::CRON_HOOK_BEFORE, self::CRON_HOOK_AFTER ), true ) ){
foreach( $events as $sig => $data ){
wp_unschedule_event( $timestamp, $hook, $data['args'] );
}
}
}
}
}
}
public function reschedule_notices_on_status_change( $subscription, $old_status, $new_status ){
$this->reschedule_notices_generic( $subscription->get_id() );
}
public function reschedule_notices_on_date_change( $subscription, $date_type, $datetime ){
$this->reschedule_notices_generic( $subscription->get_id() );
}
public function reschedule_notices_generic( $subscription_id ){
$subscription = wcs_get_subscription( $subscription_id );
if( ! $subscription ){ return; }
// Clear existing schedules for this subscription
$this->clear_schedules_for( $subscription_id );
// Only schedule for active subscriptions with a next payment date
if( $subscription->has_status( array( 'active', 'on-hold' ) ) ){
$next = $subscription->get_date( 'next_payment' );
if( $next ){
$ts_next = strtotime( $next );
if( $ts_next && $ts_next > 0 ){
$before = $ts_next - WEEK_IN_SECONDS;
$after = $ts_next + WEEK_IN_SECONDS;
if( $before > time() ){
wp_schedule_single_event( $before, self::CRON_HOOK_BEFORE, array( $subscription_id ) );
}
if( $after > time() ){
wp_schedule_single_event( $after, self::CRON_HOOK_AFTER, array( $subscription_id ) );
}
}
}
}
}
private function clear_schedules_for( $subscription_id ){
// Unschedule any pending events matching our hooks for this subscription
$hooks = array( self::CRON_HOOK_BEFORE, self::CRON_HOOK_AFTER );
foreach( $hooks as $hook ){
$next = wp_next_scheduled( $hook, array( $subscription_id ) );
while( $next ){
wp_unschedule_event( $next, $hook, array( $subscription_id ) );
$next = wp_next_scheduled( $hook, array( $subscription_id ) );
}
}
}
public function emit_notice_before( $subscription_id ){
/**
* Fires when a subscription is 1 week before renewal.
* @param int $subscription_id
* @param string $when 'before'
*/
do_action( self::ACTION_NOTICE, $subscription_id, 'before' );
}
public function emit_notice_after( $subscription_id ){
/**
* Fires when a subscription is 1 week after the scheduled renewal date.
* @param int $subscription_id
* @param string $when 'after'
*/
do_action( self::ACTION_NOTICE, $subscription_id, 'after' );
}
/* =========================
* Default Listener (Example)
* ========================= */
public function default_email_listener( $subscription_id, $when ){
$subscription = wcs_get_subscription( $subscription_id );
if( ! $subscription ){ return; }
$owner_id = (int) $subscription->get_user_id();
$owner = get_user_by( 'id', $owner_id );
$members = get_post_meta( $subscription_id, self::META_KEY_MEMBERS, true );
if( ! is_array( $members ) ){ $members = array(); }
$emails = array();
if( $owner && is_email( $owner->user_email ) ){ $emails[] = $owner->user_email; }
foreach( $members as $uid ){
$u = get_user_by( 'id', (int) $uid );
if( $u && is_email( $u->user_email ) ){ $emails[] = $u->user_email; }
}
$emails = array_values( array_unique( array_filter( $emails ) ) );
if( empty( $emails ) ){ return; }
$product_titles = array();
foreach( $subscription->get_items() as $item ){
$product_titles[] = $item->get_name();
}
$products_str = implode( ', ', $product_titles );
$when_label = $when === 'before' ? 'Upcoming renewal' : 'Recent renewal';
$subject = apply_filters( 'wcss_email_subject', sprintf( '%s: Subscription #%d', $when_label, $subscription_id ), $subscription_id, $when );
$body = apply_filters(
'wcss_email_body',
sprintf(
"%s\n\nSubscription: #%d\nItems: %s\nNext payment: %s\nStatus: %s\n",
$when_label,
$subscription_id,
$products_str,
(string) $subscription->get_date( 'next_payment' ),
$subscription->get_status()
),
$subscription_id,
$when
);
/**
* Filter recipients list for the default email listener.
* @param array $emails
* @param int $subscription_id
* @param string $when
*/
$emails = apply_filters( 'wcss_email_recipients', $emails, $subscription_id, $when );
// Basic mail send. Replace or remove this listener and wire your own if you need HTML or WC mailer templates.
foreach( $emails as $email ){
wp_mail( $email, $subject, $body );
}
}
}
WCSS_Shared_Subscriptions::instance();
}
/* ============================================================
* PUBLIC HELPERS (callable anywhere)
* ============================================================ */
/**
* Check if a user is linked to a subscription as owner or member.
* Returns true/false. Strict existence check; does not check active status.
*/
if( ! function_exists( 'wcss_user_is_linked_to_subscription' ) ){
function wcss_user_is_linked_to_subscription( $subscription_id, $user_id = 0 ){
$user_id = $user_id ? (int) $user_id : get_current_user_id();
if( ! $user_id || ! function_exists( 'wcs_get_subscription' ) ){ return false; }
$subscription = wcs_get_subscription( $subscription_id );
if( ! $subscription ){ return false; }
if( (int) $subscription->get_user_id() === (int) $user_id ){ return true; }
$members = get_post_meta( $subscription_id, WCSS_Shared_Subscriptions::META_KEY_MEMBERS, true );
if( ! is_array( $members ) ){ $members = array(); }
return in_array( (int) $user_id, array_map( 'intval', $members ), true );
}
}
/**
* Helper: is the current user linked (owner or member) AND is the subscription active/on-hold?
* Use in theme/plugin code to gate benefits.
*/
if( ! function_exists( 'wcss_current_user_has_active_link' ) ){
function wcss_current_user_has_active_link( $subscription_id ){
if( ! function_exists( 'wcs_get_subscription' ) ){ return false; }
$user_id = get_current_user_id();
if( ! $user_id ){ return false; }
$linked = wcss_user_is_linked_to_subscription( $subscription_id, $user_id );
if( ! $linked ){ return false; }
$subscription = wcs_get_subscription( $subscription_id );
return $subscription && $subscription->has_status( array( 'active', 'on-hold' ) );
}
}
/**
* Helper: does the user have an active link to any subscription that contains a given product?
* Optional utility if you gate access by product ownership.
*/
if( ! function_exists( 'wcss_user_has_access_to_product' ) ){
function wcss_user_has_access_to_product( $user_id, $product_id ){
if( ! function_exists( 'wcs_get_subscriptions' ) ){ return false; }
$args = array(
'customer_id' => (int) $user_id,
'status' => array( 'active', 'on-hold' ),
);
$subs = wcs_get_subscriptions( $args );
// Extend with subscriptions where $user_id is a member
$member_subs = get_posts( array(
'post_type' => 'shop_subscription',
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => WCSS_Shared_Subscriptions::META_KEY_MEMBERS,
'value' => '"' . (int) $user_id . '"',
'compare' => 'LIKE',
),
),
'fields' => 'ids',
) );
foreach( $member_subs as $sid ){
$s = wcs_get_subscription( $sid );
if( $s && $s->has_status( array( 'active', 'on-hold' ) ) ){
$subs[ $sid ] = $s;
}
}
if( empty( $subs ) ){ return false; }
foreach( $subs as $sub ){
foreach( $sub->get_items() as $item ){
if( (int) $item->get_product_id() === (int) $product_id || (int) $item->get_variation_id() === (int) $product_id ){
return true;
}
}
}
return false;
}
}
/**
* Utility: get member user IDs array for a subscription.
*/
if( ! function_exists( 'wcss_get_subscription_members' ) ){
function wcss_get_subscription_members( $subscription_id ){
$members = get_post_meta( $subscription_id, WCSS_Shared_Subscriptions::META_KEY_MEMBERS, true );
return is_array( $members ) ? array_values( array_map( 'intval', $members ) ) : array();
}
}
/* ============================================================
* DEVELOPER NOTES
*
* - Assign linked users in WP-Admin: WooCommerce → Shared Subscriptions.
* - Use wcss_user_is_linked_to_subscription( $subscription_id, $user_id ) to check linkage.
* - Use wcss_current_user_has_active_link( $subscription_id ) to gate benefits.
* - Renewal notice framework:
* * Schedules two wp-cron events relative to next payment: -7 days and +7 days.
* * Emits a single reusable action: do_action( 'wcss_subscription_renewal_notice', $subscription_id, 'before'|'after' );
* * Replace or augment the default listener if you prefer WooCommerce Mailer templates.
* - Hooks you can use:
* * add_action( 'wcss_subscription_renewal_notice', function( $subscription_id, $when ){ ... }, 10, 2 );
* * add_filter( 'wcss_email_subject', function( $subject, $subscription_id, $when ){ ... }, 10, 3 );
* * add_filter( 'wcss_email_body', function( $body, $subscription_id, $when ){ ... }, 10, 3 );
* * add_filter( 'wcss_email_recipients', function( $emails, $subscription_id, $when ){ ... }, 10, 3 );
*
* - Safety: UI vocabulary avoids “Parent/Child.” It uses “Owner account” and “Members.”
* ============================================================ */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment