Jake Vanderwerf
3 days ago ba1e1ccf869b818f7a7a897264dfea05563a7796
=Major overhaul of Integrations. Playing around with adding fields to post types through Registrar from an integrations' class file.
51 files modified
472 ■■■■■ changed files
JVBase.php 8 ●●●● patch | view | raw | blame | history
activate.php 4 ●●●● patch | view | raw | blame | history
assets/js/concise/AuthManager.js 1 ●●●● patch | view | raw | blame | history
assets/js/concise/FrontendFavourites.js 3 ●●●●● patch | view | raw | blame | history
assets/js/min/auth.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/favourites.min.js 2 ●●● patch | view | raw | blame | history
base/Site.php 4 ●●● patch | view | raw | blame | history
checks.php 2 ●●● patch | view | raw | blame | history
inc/admin/ContentTaxonomy.php 4 ●●●● patch | view | raw | blame | history
inc/admin/_setup.php 2 ●●● patch | view | raw | blame | history
inc/blocks/FAQBlock.php 2 ●●● patch | view | raw | blame | history
inc/blocks/RegisterBlocks.php 4 ●●●● patch | view | raw | blame | history
inc/blocks/_setup.php 8 ●●●● patch | view | raw | blame | history
inc/helpers/dashboard.php 4 ●●●● patch | view | raw | blame | history
inc/integrations/Facebook.php 9 ●●●●● patch | view | raw | blame | history
inc/integrations/GoogleMaps.php 2 ●●● patch | view | raw | blame | history
inc/integrations/GoogleMyBusiness.php 5 ●●●●● patch | view | raw | blame | history
inc/integrations/Instagram.php 3 ●●●●● patch | view | raw | blame | history
inc/integrations/Integrations.php 11 ●●●●● patch | view | raw | blame | history
inc/integrations/Square.php 85 ●●●●● patch | view | raw | blame | history
inc/managers/ApprovalManager.php 2 ●●● patch | view | raw | blame | history
inc/managers/CRUDManager.php 7 ●●●● patch | view | raw | blame | history
inc/managers/DashboardManager.php 15 ●●●●● patch | view | raw | blame | history
inc/managers/DirectoryManager.php 6 ●●●● patch | view | raw | blame | history
inc/managers/InvitationsManager.php 8 ●●●● patch | view | raw | blame | history
inc/managers/LoginManager.php 3 ●●●● patch | view | raw | blame | history
inc/managers/Notifications/EmailDigests.php 8 ●●●● patch | view | raw | blame | history
inc/managers/Notifications/Notifications.php 10 ●●●● patch | view | raw | blame | history
inc/managers/RoleManager.php 8 ●●●● patch | view | raw | blame | history
inc/managers/SEO/render/SchemaOutput.php 2 ●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Product/Product.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/VerifyEntryManager.php 2 ●●● patch | view | raw | blame | history
inc/managers/_setup.php 8 ●●●● patch | view | raw | blame | history
inc/registrar/Fields.php 2 ●●● patch | view | raw | blame | history
inc/registrar/Registrar.php 66 ●●●● patch | view | raw | blame | history
inc/registrar/config/Integration.php 8 ●●●● patch | view | raw | blame | history
inc/registrar/config/Section.php 13 ●●●●● patch | view | raw | blame | history
inc/registrar/fields/Field.php 17 ●●●●● patch | view | raw | blame | history
inc/registrar/helpers/AddIntegrationFields.php 6 ●●●●● patch | view | raw | blame | history
inc/registrar/helpers/HideSingle.php 7 ●●●●● patch | view | raw | blame | history
inc/registrar/helpers/MakeCalendarType.php 9 ●●●● patch | view | raw | blame | history
inc/registrar/helpers/MakeTimelineType.php 4 ●●● patch | view | raw | blame | history
inc/rest/Rest.php 4 ●●●● patch | view | raw | blame | history
inc/rest/RestRouteManager.php 4 ●●●● patch | view | raw | blame | history
inc/rest/_setup.php 4 ●●●● patch | view | raw | blame | history
inc/rest/routes/ApprovalRoutes.php 4 ●●●● patch | view | raw | blame | history
inc/rest/routes/ContentTermsRoutes.php 2 ●●● patch | view | raw | blame | history
inc/rest/routes/FeedRoutes.php 8 ●●●● patch | view | raw | blame | history
inc/rest/routes/LoginRoutes.php 11 ●●●●● patch | view | raw | blame | history
inc/rest/routes/NotificationsRoutes.php 2 ●●● patch | view | raw | blame | history
jvb.php 50 ●●●●● patch | view | raw | blame | history
JVBase.php
@@ -151,7 +151,7 @@
            $this->managers['notifications'] = new NotificationManager();
            $this->routes['notifications'] = new NotificationsRoutes();
        }
        if (!empty(Registrar::getFeatured('approve_new'))) {
        if (!empty(Registrar::withFeature('approve_new'))) {
            $this->managers['approvals'] = new ApprovalManager();
        }
        if (Site::has('feed_block') || Site::has('dashboard')) {
@@ -183,13 +183,13 @@
        if ($membership && $membership->has('invitable')) {
            $this->managers['invitations'] = new InvitationsManager();
        }
        if (!empty(Registrar::getFeatured('has_responses'))) {
        if (!empty(Registrar::withFeature('has_responses'))) {
            $this->routes['comments'] = new ResponseRoutes();
        }
        if (!empty(Registrar::getFeatured('karma'))) {
        if (!empty(Registrar::withFeature('karma'))) {
            $this->routes['vote'] = new VoteRoutes();
        }
        if (!empty(Registrar::getFeatured('karma'))
        if (!empty(Registrar::withFeature('karma'))
            || ($membership && $membership->has('member_verified')) ||
            ($membership && $membership->has('term_approval'))) {
            $this->routes['approvals'] = new ApprovalRoutes();
activate.php
@@ -64,7 +64,7 @@
    $role = get_role('administrator');
    $users = get_users(['role' => 'administrator']);
    foreach (array_merge(Registrar::getRegistered('post'), Registrar::getFeatured('is_content')) as $slug) {
    foreach (array_merge(Registrar::getRegistered('post'), Registrar::withFeature('is_content')) as $slug) {
        error_log('Adding administrative roles to '.$slug);
        $plural = $roleManager->getContentPlural($slug);
        $capabilities = [
@@ -105,7 +105,7 @@
    $roleManager = new RoleManager();
    $users = get_users(['role' => 'administrator']);
    foreach (array_merge(Registrar::getRegistered('post'), Registrar::getFeatured('is_content', 'term')) as $slug) {
    foreach (array_merge(Registrar::getRegistered('post'), Registrar::withFeature('is_content', 'term')) as $slug) {
        foreach ($users as $user) {
            // These methods should check if post type exists before adding caps
assets/js/concise/AuthManager.js
@@ -127,6 +127,7 @@
        this.authenticated = authData.authenticated || false;
        this.user = authData.user || false;
        this.nonces = authData.nonces || {};
        console.log(this.nonces);
        // Session expired â€” was logged in, now isn't
        if (wasAuthenticated && !this.authenticated) {
assets/js/concise/FrontendFavourites.js
@@ -7,6 +7,9 @@
                storeName: 'favourites',
                keyPath: 'id',
                endpoint: 'favourites',
                headers: {
                    'X-Action-Nonce': window.auth.getNonce('favourites')
                },
                indexes: [
                    {name: 'content', keyPath: 'content'},
                    {name: 'listId', keyPath: 'listId'},
assets/js/min/auth.min.js
@@ -1 +1 @@
window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.cacheExpiry=3e5,this.init()}async init(){if(!this.isAuthenticating){this.isAuthenticating=!0;try{if("undefined"!=typeof jvbAuth)return this.setAuthData(jvbAuth),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1});await this.fetchAuth()}catch(t){this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,i={}){const e=async(s=0)=>{const h={...!(i.body instanceof FormData)&&{"Content-Type":"application/json"},...i.headers,"X-WP-Nonce":this.getNonce()},n=await fetch(t,{...i,credentials:"same-origin",headers:h});if((403===n.status||401===n.status)&&0===s){const t=await n.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),e(s+1)}return n};return e()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const i=await t.json();this.setAuthData(i),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){const i=this.initialized&&this.authenticated;this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{},i&&!this.authenticated&&(window.location.href=`/login?redirect_to=${encodeURIComponent(window.location.href)}`)}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={}}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,i){this.subscribers.forEach((e=>{try{e(t,i)}catch(t){console.error("Subscriber error:",t)}}))}};
window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.cacheExpiry=3e5,this.init()}async init(){if(!this.isAuthenticating){this.isAuthenticating=!0;try{if("undefined"!=typeof jvbAuth)return this.setAuthData(jvbAuth),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1});await this.fetchAuth()}catch(t){this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,i={}){const e=async(s=0)=>{const n={...!(i.body instanceof FormData)&&{"Content-Type":"application/json"},...i.headers,"X-WP-Nonce":this.getNonce()},h=await fetch(t,{...i,credentials:"same-origin",headers:n});if((403===h.status||401===h.status)&&0===s){const t=await h.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),e(s+1)}return h};return e()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const i=await t.json();this.setAuthData(i),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){const i=this.initialized&&this.authenticated;this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{},console.log(this.nonces),i&&!this.authenticated&&(window.location.href=`/login?redirect_to=${encodeURIComponent(window.location.href)}`)}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={}}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,i){this.subscribers.forEach((e=>{try{e(t,i)}catch(t){console.error("Subscriber error:",t)}}))}};
assets/js/min/favourites.min.js
@@ -1 +1 @@
(()=>{class t{constructor(){let t=window.jvbStore.register("favourites",{storeName:"favourites",keyPath:"id",endpoint:"favourites",indexes:[{name:"content",keyPath:"content"},{name:"listId",keyPath:"listId"}],TTL:36e4,showLoading:!1,filters:{user:window.auth.getUser(),content:"all",order:"desc",orderby:"date",page:1,all:!0}});this.store=t.favourites,this.store.subscribe(((t,e)=>{t}))}toggleFavourite(t){if(!window.auth.getUser())return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",o=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(o),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],this.store.setItem(t.dataset.id,{target_id:t.dataset.id,action:e,type:t.dataset.type,artist:t.dataset.artist})}isFavourited(t,e){const o=`${this.userId}_${t}_${e}`;return void 0!==this.store.get(o)}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,window.auth.subscribe((e=>{"auth-loaded"===e&&(window.jvbFavourites=new t)}))})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
(()=>{class t{constructor(){let t=window.jvbStore.register("favourites",{storeName:"favourites",keyPath:"id",endpoint:"favourites",headers:{"X-Action-Nonce":window.auth.getNonce("favourites")},indexes:[{name:"content",keyPath:"content"},{name:"listId",keyPath:"listId"}],TTL:36e4,showLoading:!1,filters:{user:window.auth.getUser(),content:"all",order:"desc",orderby:"date",page:1,all:!0}});this.store=t.favourites,this.store.subscribe(((t,e)=>{t}))}toggleFavourite(t){if(!window.auth.getUser())return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",o=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(o),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],this.store.setItem(t.dataset.id,{target_id:t.dataset.id,action:e,type:t.dataset.type,artist:t.dataset.artist})}isFavourited(t,e){const o=`${this.userId}_${t}_${e}`;return void 0!==this.store.get(o)}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,window.auth.subscribe((e=>{"auth-loaded"===e&&(window.jvbFavourites=new t)}))})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
base/Site.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\base;
use JVBase\base\Login;
if (!defined('ABSPATH')) {
    exit;
}
@@ -92,7 +90,7 @@
    public static function getInstance():Site {
        if (!isset(self::$instance)) {
            self::$instance = new self();
            do_action('jvbLoadDefinitions');
            do_action('jvb_define_site');
        }
        return self::$instance;
    }
checks.php
@@ -14,7 +14,7 @@
function jvbUserTypes():array
{
    return  Registrar::getFeatured('profile_link', 'user');
    return  Registrar::withFeature('profile_link', 'user');
}
inc/admin/ContentTaxonomy.php
@@ -17,7 +17,7 @@
{
    public function __construct()
    {
        if (empty(Registrar::getFeatured('is_content', 'term'))){
        if (empty(Registrar::withFeature('is_content', 'term'))){
            return;
        }
        add_filter(BASE.'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3);
@@ -124,7 +124,7 @@
                    </thead>
                    <tbody>
                    <?php
                    $taxonomies = Registrar::getFeatured('is_content', 'term');
                    $taxonomies = Registrar::withFeature('is_content', 'term');
                    foreach ($taxonomies as $slug):
                        $registrar = Registrar::getInstance($slug);
                        $taxonomy = BASE . $slug;
inc/admin/_setup.php
@@ -8,7 +8,7 @@
add_action('init', 'jvb_maybe_setup_content_taxonomy', 1);
function jvb_maybe_setup_content_taxonomy(): void
{
    if (!empty(Registrar::getFeatured('is_content', 'term'))) {
    if (!empty(Registrar::withFeature('is_content', 'term'))) {
        require(JVB_DIR . '/inc/admin/ContentTaxonomy.php');
    }
}
inc/blocks/FAQBlock.php
@@ -32,7 +32,7 @@
        );
        $faq = array_values(Registrar::getFeatured('is_faq','post'));
        $faq = array_values(Registrar::withFeature('is_faq','post'));
        $registrar = Registrar::getInstance($faq[0]);
        $this->section = array_map('jvbCheckBase', $registrar->registrar->taxonomies)[0];
        $this->postType = $registrar->getBased();
inc/blocks/RegisterBlocks.php
@@ -14,7 +14,7 @@
require(JVB_DIR . '/build/summary/render.php');
require(JVB_DIR . '/build/forms/render.php');
require(JVB_DIR . '/build/menu/render.php');
if (!empty(Registrar::getFeatured('is_glossary'))) {
if (!empty(Registrar::withFeature('is_glossary'))) {
    error_log('Has Glossary Type');
    require(JVB_DIR . '/build/glossary/render.php');
}
@@ -39,7 +39,7 @@
//            ]
//        );
//    }
    if (!empty(Registrar::getFeatured('show_directory'))) {
    if (!empty(Registrar::withFeature('show_directory'))) {
        register_block_type(
            JVB_DIR . '/build/list',
            [
inc/blocks/_setup.php
@@ -20,17 +20,17 @@
        new JVBase\blocks\MenuBlock();
    }
    if (!empty(Registrar::getFeatured('is_faq'))) {
    if (!empty(Registrar::withFeature('is_faq'))) {
        require('FAQBlock.php');
        new JVBase\blocks\FAQBlock();
    }
    if (!empty(Registrar::getFeatured('is_glossary'))) {
    if (!empty(Registrar::withFeature('is_glossary'))) {
        require('GlossaryBlock.php');
        new JVBase\blocks\GlossaryBlock();
    }
    if (!empty(Registrar::getFeatured('is_timeline'))) {
    if (!empty(Registrar::withFeature('is_timeline'))) {
        require('TimelineBlock.php');
        new JVBase\blocks\TimelineBlock();
    }
@@ -47,7 +47,7 @@
//            ]
//        );
//    }
//  if (!empty(Registrar::getFeatured('show_directory'))) {
//  if (!empty(Registrar::withFeature('show_directory'))) {
//      error_log('Registering Directory List Block');
//      register_block_type(
//          JVB_DIR . '/build/list',
inc/helpers/dashboard.php
@@ -62,7 +62,7 @@
            array_map(function ($role) {
                return BASE.$role;
            },
               Registrar::getFeatured('has_dashboard', 'user'))
               Registrar::withFeature('has_dashboard', 'user'))
        )
    )>0;
}
@@ -109,7 +109,7 @@
            }
        }
        $types = Registrar::getFeatured('show_feed');
        $types = Registrar::withFeature('show_feed');
        $types = array_filter($temp, function ($type) use ($types) {
            return in_array($type, $types);
        });
inc/integrations/Facebook.php
@@ -17,6 +17,15 @@
class Facebook extends Integrations
{
    // Facebook-specific properties
    protected array $allowedContent = [
        'post',
        'photo',
        'video',
        'event',
        'offer',
        'note',
        'milestone'
    ];
    private string $page_id = '';
    private string $page_access_token = '';
    private array $permissions = [];
inc/integrations/GoogleMaps.php
@@ -542,7 +542,7 @@
            return true;
        }
        if (!empty(Registrar::getFeatured('is_content', 'term')) && get_term_meta(get_queried_object_id(), BASE . 'has_map', true) === true) {
        if (!empty(Registrar::withFeature('is_content', 'term')) && get_term_meta(get_queried_object_id(), BASE . 'has_map', true) === true) {
            return true;
        }
inc/integrations/GoogleMyBusiness.php
@@ -9,6 +9,11 @@
class GoogleMyBusiness extends Integrations
{
    protected array $allowedContent = [
        'menu_item',
        'post',
        'event',
    ];
    private ?string $access_token = null;
    protected string $readMask = 'name,title,storefrontAddress,metadata,openInfo,storeCode,categories,phoneNumbers,labels,specialHours';
    private ?string $location = null;
inc/integrations/Instagram.php
@@ -17,6 +17,9 @@
class Instagram extends Integrations
{
    protected array $allowedContent = [
        'post'
    ];
    private string $ig_user_id = '';
    private string $page_id = '';
    private string $page_access_token = '';
inc/integrations/Integrations.php
@@ -29,6 +29,15 @@
abstract class Integrations
{
    /**
     * Queue types
     * These types match with IntegrationExecutor
     */
    protected static string $syncTo = 'sync_to';
    protected static string $deleteFrom = 'delete_from';
    protected static string $syncFrom = 'sync_from';
    protected static string $syncCustomer = 'sync_customer';
    protected static string $import = 'import';
    /**
     * API Configuration
     * These properties define how the integration connects to external services
     */
@@ -319,7 +328,7 @@
        if (!$taxonomies) {
            // Combine both content and taxonomy filtering
            $taxonomies = [];
            foreach (Registrar::getFeatured('is_content', 'term') as $type) {
            foreach (Registrar::withFeature('is_content', 'term') as $type) {
                $registrar = Registrar::getInstance($type);
                if ($registrar->hasIntegration($this->service_name)) {
                    $taxonomies[] = $registrar->getSlug();
inc/integrations/Square.php
@@ -26,6 +26,14 @@
 */
class Square extends Integrations
{
    protected array $allowedContent = [
        'REGULAR',
        'FOOD_AND_BEV',
        'APPOINTMENTS_SERVICE',
        'DIGITAL',
        'EVENT',
        'DONATION'
    ];
    /**
     * Square API Configuration
     */
@@ -45,6 +53,9 @@
     * OAuth Configuration
     */
    protected bool $isOAuthService = true;
    protected string $orderPostType = '_sq_order';
    protected array $newOrder = [];
    protected array $oauth = [
        'authorize' => '',
        'token' => '',
@@ -80,6 +91,11 @@
        $this->refresh_interval = 7 * DAY_IN_SECONDS;
        $this->newOrder = [
            'post_type'     => $this->orderPostType,
            'post_status'   => 'PROPOSED',
        ];
        // Define credential fields
        $this->fields = [
            'environment'   => [
@@ -180,8 +196,7 @@
                'sync_to_square' => 'Sync Site to Square',
            ]
        );
        add_action('init', [$this, 'registerSquarePostTypes']);
        add_action('init', [$this, 'registerSquarePostTypes'], 5);
    }
    /**
@@ -228,24 +243,20 @@
                    'square_order_id' => [
                        'type' => 'text',
                        'label' => 'Square Order ID',
                        'readonly' => true
                    ],
                    'square_payment_id' => [
                        'type' => 'text',
                        'label' => 'Square Payment ID',
                        'readonly' => true
                    ],
                    'square_customer_id' => [
                        'type' => 'text',
                        'label' => 'Square Customer ID',
                        'readonly' => true
                    ],
                    'amount' => [
                        'type' => 'number',
                        'label' => 'Total Amount (cents)',
                        'readonly' => true
                    ],
                    'status' => [
                    'post_status' => [
                        'type' => 'select',
                        'label' => 'Order Status',
                        'options' => [
@@ -255,7 +266,6 @@
                            'COMPLETED' => 'Completed',
                            'CANCELED' => 'Canceled'
                        ],
                        'readonly' => true
                    ],
                    'fulfillment_status' => [
                        'type' => 'select',
@@ -268,7 +278,6 @@
                            'CANCELED' => 'Canceled',
                            'FAILED' => 'Failed'
                        ],
                        'readonly' => true
                    ],
                    'pickup_time' => [
                        'type' => 'datetime',
@@ -277,27 +286,22 @@
                    'customer_email' => [
                        'type' => 'email',
                        'label' => 'Customer Email',
                        'readonly' => true
                    ],
                    'customer_name' => [
                        'type' => 'text',
                        'label' => 'Customer Name',
                        'readonly' => true
                    ],
                    'customer_phone' => [
                        'type' => 'tel',
                        'type' => 'phone',
                        'label' => 'Customer Phone',
                        'readonly' => true
                    ],
                    'special_instructions' => [
                        'type' => 'textarea',
                        'label' => 'Special Instructions',
                        'readonly' => true
                    ],
                    'items' => [
                        'type' => 'repeater',
                        'label' => 'Order Items',
                        'readonly' => true,
                        'fields' => [
                            'name' => ['type' => 'text', 'label' => 'Item Name'],
                            'quantity' => ['type' => 'number', 'label' => 'Quantity'],
@@ -308,17 +312,14 @@
                    'receipt_url' => [
                        'type' => 'url',
                        'label' => 'Receipt URL',
                        'readonly' => true
                    ],
                    'created_at' => [
                        'type' => 'datetime',
                        'label' => 'Created At',
                        'readonly' => true
                    ],
                    'updated_at' => [
                        'type' => 'datetime',
                        'label' => 'Last Updated',
                        'readonly' => true
                    ]
                ];
    }
@@ -327,16 +328,14 @@
    {
        $orders = Registrar::forPost('_sq_orders', 'Square Order', 'Square Orders');
        $orders->make([
            'public'    => false
            ]
        );
            'public'    => true
        ]);
        $orders->setAll(['system']);
        $fields = $orders->fields();
        foreach ($this->getOrderFields() as $fieldName => $config) {
            $fields->addField($fieldName, $config);
        }
    }
    /**
@@ -845,10 +844,9 @@
        add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
        // Shared checkout UI (replaces outputCheckout)
        add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
        // Square-specific checkout description
        add_filter('jvb_checkout_description', function (string $desc, string $provider) {
            if ($provider === 'square') {
                return 'Securely checkout with your name, email, and payments processed by Square.';
@@ -902,31 +900,31 @@
        $queue    = JVB()->queue();
        $executor = new IntegrationExecutor();
        $queue->registry()->register('square_sync_to', new TypeConfig(
        $queue->registry()->register(self::$syncTo, new TypeConfig(
            executor:   $executor,
            chunkKey:   'items',
            chunkSize:  50,
            maxRetries: 3
        ));
        $queue->registry()->register('square_delete_from', new TypeConfig(
        $queue->registry()->register(self::$deleteFrom, new TypeConfig(
            executor:   $executor,
            chunkKey:   'external_ids',
            chunkSize:  200,
            maxRetries: 2
        ));
        $queue->registry()->register('square_sync_from', new TypeConfig(
        $queue->registry()->register(self::$syncFrom, new TypeConfig(
            executor:   $executor,
            maxRetries: 3
        ));
        $queue->registry()->register('square_sync_customer', new TypeConfig(
        $queue->registry()->register(self::$syncCustomer, new TypeConfig(
            executor:   $executor,
            maxRetries: 2
        ));
        $queue->registry()->register('square_import', new TypeConfig(
        $queue->registry()->register(self::$import, new TypeConfig(
            executor:   $executor,
            maxRetries: 3
        ));
@@ -941,7 +939,7 @@
     */
    protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
    {
        $this->queueOperation('sync_to', [
        $this->queueOperation(self::$syncTo, [
            'items'   => [$postID],
            'user_id' => $this->userID,
        ], [
@@ -960,7 +958,7 @@
        $square_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
        if ($square_id) {
            $this->queueOperation('delete_from', [
            $this->queueOperation(self::$deleteFrom, [
                'external_ids' => [$square_id],
                'post_id'      => $postID,
            ], [
@@ -970,26 +968,6 @@
    }
    /**
     * @deprecated IntegrationExecutor handles new operations via registerQueueTypes().
     * Kept for legacy-typed operations ('square_sync_to_square') already queued.
     * Safe to remove once all legacy operations have been processed.
     */
    public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
    {
        $base   = strtolower($this->service_name) . '_';
        $square = array_key_exists('user', $data) ? new self((int) $data['user']) : $this;
        return match ($operation->type) {
            $base . 'sync_to_square'     => $square->processSyncToSquare($data),
            $base . 'delete_from_square' => $square->processDeleteFromSquare($data),
            $base . 'sync_from_square'   => $square->processSyncFromSquare($data),
            $base . 'sync_customer'      => $square->processSyncCustomer($data),
            default                      => $result,
        };
    }
    /**
     * Process sync to Square
     */
    private function processSyncToSquare(array $data): array
@@ -2764,17 +2742,18 @@
            return [];
        }
        $array = $this->setBaseFields();
        return array_combine(
        $return = array_combine(
            array_map(fn($k) => 'sq_' . $k, array_keys($array)),
            $array
        );
        return $return;
    }
    protected function setBaseFields():array
    {
        return [
            'price' => [
                'type'        => 'number',
                'bulkEdit'    => true,
                'label'       => 'Price',
                'step'        => 0.01,
                'max'         => 99999,
inc/managers/ApprovalManager.php
@@ -32,7 +32,7 @@
    protected function defineTables():void
    {
        $types = Registrar::getFeatured('approve_new');
        $types = Registrar::withFeature('approve_new');
        foreach ($types as $type) {
            $requests = CustomTable::for("approval_{$type}_requests");
            $registrar = Registrar::getInstance($type);
inc/managers/CRUDManager.php
@@ -64,9 +64,14 @@
        // Fields and sections
        $this->skeleton->setFields($this->registrar->getFields());
        foreach ($this->registrar->getSections() as $config) {
        jvbDump($this->registrar->getSections());
        $sections = $this->registrar->getSections();
        if (count($sections) > 1) {
            foreach ($sections as $config) {
                jvbDump($config);
            $this->skeleton->addSection($config['id'], $config);
        }
        }
        // Taxonomies
        $this->initTaxonomies();
inc/managers/DashboardManager.php
@@ -641,7 +641,10 @@
                }
            }
            return $icon;
            return match($icon) {
                'favourites'    => 'heart',
                default => $icon
            };
        });
    }
    protected function getSlug(string $slug, string $page):string
@@ -744,7 +747,7 @@
            //content types
        $all = array_merge(
            Registrar::getRegistered('post'),
            Registrar::getFeatured('is_content', 'term')
            Registrar::withFeature('is_content', 'term')
        );
        $availableContent = array_filter($pages, function($page, $key) use($all) {
            return !is_numeric($key) && in_array($key, $all) && JVB()->roles()->checkRole($this->user, $key);
@@ -1091,7 +1094,7 @@
        <?php
        $i=1;
        $content = Registrar::getRegistered('post');
        $contentTax = Registrar::getFeatured('is_content', 'term');
        $contentTax = Registrar::withFeature('is_content', 'term');
        $taxonomies = Registrar::getRegistered('term');
        foreach($contentTax as $index => $tax) {
            unset($taxonomies[$index]);
@@ -1284,7 +1287,7 @@
                $pages[] = 'Favourites';
            }
            if (!empty(Registrar::getFeatured('karma'))) {
            if (!empty(Registrar::withFeature('karma'))) {
                $pages[] = 'Karmic Score';
            }
@@ -1451,7 +1454,7 @@
                            foreach ($roles as $role) {
                                $contents = Registrar::getInstance($role)?->getCreatable();
                                if (!empty($contents)) {
                                    $hasKarma = Registrar::getFeatured('karma');
                                    $hasKarma = Registrar::withFeature('karma');
                                    $remove = empty(array_intersect($contents, $hasKarma));
                                }
                            }
@@ -1512,7 +1515,7 @@
     */
    protected function getRolesWithDashboard():array
    {
        return Registrar::getFeatured('has_dashboard', 'user');
        return Registrar::withFeature('has_dashboard', 'user');
    }
    /**
inc/managers/DirectoryManager.php
@@ -90,21 +90,21 @@
            $directories = [];
            //content
            $content = Registrar::getFeatured('show_directory', 'post');
            $content = Registrar::withFeature('show_directory', 'post');
            if(!empty($content)) {
                foreach ($content as $key) {
                    $directories[$key] = 'content';
                }
            }
            $taxonomies = Registrar::getFeatured('show_directory', 'term');
            $taxonomies = Registrar::withFeature('show_directory', 'term');
            if(!empty($taxonomies)) {
                foreach ($taxonomies as $key) {
                    $directories[$key] = 'taxonomy';
                }
            }
            $users = Registrar::getFeatured('show_directory', 'user');
            $users = Registrar::withFeature('show_directory', 'user');
            if(!empty($users)) {
                foreach ($users as $key) {
                    $directories[$key] = 'user';
inc/managers/InvitationsManager.php
@@ -36,7 +36,7 @@
    public function defineTable():void
    {
        $terms = Registrar::getFeatured('invitable', 'term');
        $terms = Registrar::withFeature('invitable', 'term');
        $membership = Site::membership();
        $roles = ($membership) ? Site::membership()->has('can_invite') :[];
        if (empty($terms) && empty($roles)) {
@@ -163,9 +163,9 @@
                }
                // Term invitations from invitable content taxonomies
                $invitable = Registrar::getFeatured('invitable', 'term');
                $content = Registrar::getFeatured('is_content', 'term');
                $ownable = Registrar::getFeatured('is_ownable', 'term');
                $invitable = Registrar::withFeature('invitable', 'term');
                $content = Registrar::withFeature('is_content', 'term');
                $ownable = Registrar::withFeature('is_ownable', 'term');
                $taxonomies = array_intersect($invitable, $content, $ownable);
                if (!empty($taxonomies)) {
                    $users = Registrar::getRegistered('user');
inc/managers/LoginManager.php
@@ -143,7 +143,7 @@
                    'hint'  => 'Have a referral code? Paste it here!'
                ];
            }
            $canRegister = Registrar::getFeatured('can_register', 'user');
            $canRegister = Registrar::withFeature('can_register', 'user');
            if (!empty($canRegister)) {
                foreach ($canRegister as $role) {
                    $registrar = Registrar::getInstance($role);
@@ -456,6 +456,7 @@
    protected function customStyles():void
    {
        $logo = get_theme_mod('custom_logo');
        $small = $large = '';
        if ($logo) {
            $small = wp_get_attachment_image_src($logo, 'medium')[0]??'';
            $large = wp_get_attachment_image_src($logo, 'large')[0]??'';
inc/managers/Notifications/EmailDigests.php
@@ -43,7 +43,7 @@
        protected function registerUserIndex():void
        {
            $table = CustomTable::for('user_notification_email_digest');
//      $types = implode(',',array_map(function($item) { return "'{$item}'"; }, Registrar::getFeatured('favouritable')));
//      $types = implode(',',array_map(function($item) { return "'{$item}'"; }, Registrar::withFeature('favouritable')));
            $table->setColumns([
                'id'            => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
                'user_id'       => "{$table->getUserIDType()} NOT NULL",
@@ -71,7 +71,7 @@
        protected function registerTermIndex():void
        {
            $table = CustomTable::for('user_notification_email_digest');
            $types = implode(',',array_map(function($item) { return "'{$item}'"; }, Registrar::getFeatured('favouritable', 'term')));
            $types = implode(',',array_map(function($item) { return "'{$item}'"; }, Registrar::withFeature('favouritable', 'term')));
            $table->setColumns([
                'id'            => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
                'term_id'       => "{$table->getTermIDType()} NOT NULL",
@@ -187,8 +187,8 @@
        $content = '';
        foreach ($subscription as $item) {
            $temp = match ($item['item_type']) {
                array_merge(['user'], Registrar::getFeatured('favouritable', 'user')) => $this->getUserUpdates($item['item_id'], $frequency),
                Registrar::getFeatured('favouritable', 'term') => $this->getTermUpdates($item['item_id'], $item['item_type'], $frequency),
                array_merge(['user'], Registrar::withFeature('favouritable', 'user')) => $this->getUserUpdates($item['item_id'], $frequency),
                Registrar::withFeature('favouritable', 'term') => $this->getTermUpdates($item['item_id'], $item['item_type'], $frequency),
                default => false,
            };
            if ($temp) {
inc/managers/Notifications/Notifications.php
@@ -46,8 +46,8 @@
                ]
            ]);
        }
        $contentTax = Registrar::getFeatured('is_content', 'term');
        $verifyEntry = Registrar::getFeatured('verify_entry', 'term');
        $contentTax = Registrar::withFeature('is_content', 'term');
        $verifyEntry = Registrar::withFeature('verify_entry', 'term');
        if (!empty(array_intersect($contentTax, $verifyEntry))) {
            $types = array_merge($types, [
                'entry_requested'  => [
@@ -69,7 +69,7 @@
                ]
            ]);
        }
        $invitable = Registrar::getFeatured('invitable');
        $invitable = Registrar::withFeature('invitable');
        if (!empty($invitable)) {
            $types = array_merge($types, [
                'invitation_requested' => [
@@ -97,12 +97,12 @@
            ]);
        }
        $approvals = Registrar::getFeatured('approve_new');
        $approvals = Registrar::withFeature('approve_new');
        if (!empty($approvals)) {
            $tmp = ['user', 'term', 'post'];
            $app = [];
            foreach ($tmp as $t) {
                $approvals = Registrar::getFeatured('approve_new', $t);
                $approvals = Registrar::withFeature('approve_new', $t);
                if (!empty($approvals)) {
                    $app = array_merge($app, [
                        $t.'_new' => [
inc/managers/RoleManager.php
@@ -24,7 +24,7 @@
           return strtolower(str_replace(' ', '_', $registrar->getPlural()??$registrar->getSingular().'s'));
       },array_merge(
           Registrar::getRegistered('post'),
           Registrar::getFeatured('is_content', 'term')
           Registrar::withFeature('is_content', 'term')
       ));
       add_action('set_user_role', [$this, 'updateRoles'], 10, 3);
@@ -470,7 +470,7 @@
        protected function addAdminCaps():void
        {
            $users = get_users(['role' => 'administrator']);
            foreach (array_merge(Registrar::getRegistered('post'), Registrar::getFeatured('is_content')) as $slug) {
            foreach (array_merge(Registrar::getRegistered('post'), Registrar::withFeature('is_content')) as $slug) {
                $this->grantRoleCapabilities('administrator', $slug);
                $this->grantRoleOthersCapabilities('administrator', $slug);
@@ -785,7 +785,7 @@
        if ($ownable === null) {
            $ownable = array_map(function ($instance) {
                return $instance->slug;
            }, Registrar::getFeatured('is_ownable', 'term'));
            }, Registrar::withFeature('is_ownable', 'term'));
        }
        return $ownable;
@@ -803,7 +803,7 @@
        if ($invitable === null) {
            $invitable = array_map(function ($instance) {
                return $instance->slug;
            }, Registrar::getFeatured('invitable', 'term'));
            }, Registrar::withFeature('invitable', 'term'));
        }
        return $invitable;
inc/managers/SEO/render/SchemaOutput.php
@@ -64,7 +64,7 @@
        }
        $isContent = array_values(array_filter(array_map(function($item) {
            return intval(get_option(BASE.$item.'_archive', false));
        },Registrar::getFeatured('is_content', 'term'))));
        },Registrar::withFeature('is_content', 'term'))));
        if (!empty($isContent) && is_page($isContent)){
            $type = get_post_meta(get_the_id(), BASE.'for_type', true);
inc/managers/SEO/render/Thing/Product/Product.php
@@ -2,6 +2,7 @@
namespace JVBase\managers\SEO\render\Thing\Product;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\additionalPropertyTrait;
use JVBase\managers\SEO\render\Traits\_Properties\aggregateRatingTrait;
use JVBase\managers\SEO\render\Traits\_Properties\audienceTrait;
@@ -41,15 +42,13 @@
use JVBase\managers\SEO\render\Traits\_Properties\sloganTrait;
use JVBase\managers\SEO\render\Traits\_Properties\weightTrait;
use JVBase\managers\SEO\render\Traits\_Properties\widthTrait;
use JVBase\managers\SEO\render\Traits\ThingSchema;
if (!defined('ABSPATH')) {
    exit;
}
class Product  {
    use ThingSchema,
        additionalPropertyTrait, aggregateRatingTrait, audienceTrait,
class Product  extends Thing {
    use additionalPropertyTrait, aggregateRatingTrait, audienceTrait,
        awardTrait, brandTrait, categoryTrait, colorTrait, countryOfAssemblyTrait,
        countryOfOriginTrait, depthTrait, displayLocationTrait, hasAdultConsiderationTrait,
        hasCertificationTrait, hasMeasurementTrait, hasMerchantReturnPolicyTrait,
inc/managers/VerifyEntryManager.php
@@ -17,7 +17,7 @@
    protected function defineTables():void
    {
        $types = implode(',', array_map(function($item) { return "`{$item}`"; },Registrar::getFeatured('verify_entry')));
        $types = implode(',', array_map(function($item) { return "`{$item}`"; },Registrar::withFeature('verify_entry')));
        $table = CustomTable::for('verify_entry');
inc/managers/_setup.php
@@ -49,7 +49,7 @@
        require(JVB_DIR . '/inc/managers/UserTermsManager.php');
    }
    if (!empty(Registrar::getFeatured('approve_new'))) {
    if (!empty(Registrar::withFeature('approve_new'))) {
        require(JVB_DIR . '/inc/managers/ApprovalManager.php');
    }
@@ -63,7 +63,7 @@
            require(JVB_DIR . '/inc/managers/Notifications/Preferences.php');
            require(JVB_DIR . '/inc/managers/NotificationManager.php');
        }
        if ($membership->has('forum') && !empty(Registrar::getFeatured('is_content', 'term'))) {
        if ($membership->has('forum') && !empty(Registrar::withFeature('is_content', 'term'))) {
            require(JVB_DIR . '/inc/managers/NewsRelationships.php');
        }
        if ($membership->has('invitable')) {
@@ -83,10 +83,10 @@
        require(JVB_DIR . '/inc/managers/ReferralManager.php');
    }
    if (!empty(Registrar::getFeatured('karma'))) {
    if (!empty(Registrar::withFeature('karma'))) {
        require(JVB_DIR . '/inc/managers/KarmaManager.php');
    }
//  if (Site::has('favourites') && !empty(Registrar::getFeatured('favouritable'))) {
//  if (Site::has('favourites') && !empty(Registrar::withFeature('favouritable'))) {
    if (Site::has('favourites')) {
        require(JVB_DIR . '/inc/managers/FavouritesManager.php');
    }
inc/registrar/Fields.php
@@ -17,7 +17,7 @@
class Fields {
    protected array $fields;
    protected Registrar $registrar;
    private Registrar $registrar;
    public function __construct(?string $type = null, ?Registrar $registrar = null) {
        $this->registrar = $registrar;
inc/registrar/Registrar.php
@@ -468,7 +468,8 @@
    {
        return $this->integrationConfigs;
    }
    public function hasIntegration(string $integration) {
    public function hasIntegration(string $integration):bool
    {
        return array_key_exists($integration, $this->integrationConfigs);
    }
    public function hasAnyIntegrations(array $integrations = []):bool
@@ -562,6 +563,24 @@
        }
        return $this;
    }
    public function unsetAll(array $flags):self
    {
        $flags = array_filter($flags, function($flag) {
            return in_array($flag, static::$allFlags);
        });
        foreach ($flags as $flag) {
            $this->$flag = false;
            switch ($flag) {
                case 'is_content':
                    remove_action('init', [$this, 'setupContent'], 20);
                    break;
                case 'is_glossary':
                    $this->hide_single = false;
                    break;
            }
        }
        return $this;
    }
    public function prefixWith(string $prefix):self
    {
        $this->prefix_with = sanitize_title($prefix);
@@ -654,7 +673,7 @@
        protected function getBreadcrumbs():Breadcrumbs
        {
            if (!isset($this->breadcrumbs)) {
                $this->breadcrumbs = new Breadcrumbs($this->slug, $this);
                $this->breadcrumbs = new Breadcrumbs($this->slug);
            }
            return $this->breadcrumbs;
@@ -672,7 +691,7 @@
        protected function getDashboard():Dashboard
        {
            if (!isset($this->dashboard)) {
                $this->dashboard = new Dashboard($this->plural, $this);
                $this->dashboard = new Dashboard($this->plural);
            }
            return $this->dashboard;
@@ -715,9 +734,40 @@
    }
    public function addSection(string $title):Section
    {
        $slug = sanitize_title($title);
        if (!array_key_exists($slug, $this->sections)) {
        $section = new Section($title, $this);
        $this->sections[] = $section;
        return $section;
            $this->sections[$slug] = $section;
        }
        return $this->sections[$slug];
    }
    public static function maybeBuildSections():void
    {
        foreach (self::$instances as $inst) {
            $inst->buildSections();
        }
    }
        protected function buildSections():void
        {
            $fields = $this->getFields();
            $sections = array_unique(array_values(array_map(function ($f) {
                return array_key_exists('section', $f) && !is_null($f['section']) ? $f['section'] : 'main';
            }, $fields)));
            foreach ($sections as $s) {
                $section = new Section($s, $this);
                $section->setTitle(ucwords(implode(' ', explode('-', $s))));
                $sectionFields = array_map(function ($f) {
                    return $f['name'];
                }, array_filter($fields, function ($f) use ($s) {
                    $tmp = array_key_exists('section', $f) && !is_null($f['section']) ? $f['section'] : 'main';
                    return $s === $tmp;
                }));
                $section->setFields($sectionFields);
                $this->sections[$s] = $section;
            }
    }
    public function setSectionOrder(array $sections):self
@@ -767,7 +817,7 @@
                $this->hideSingleHandler = new HideSingle($this->slug, $this);
            }
            if ($this->is_timeline) {
                $this->isTimelineHandler = new MakeTimelineType($this->slug, $this);
                $this->isTimelineHandler = new MakeTimelineType($this->slug);
                $this->registrar->hierarchical = true;
            }
            if ($this->is_calendar) {
@@ -1104,8 +1154,8 @@
    public static function ensureInstanced():void
    {
        if (empty(self::$instances)) {
            do_action('jvbDefineRegistrar');
            do_action('jvbDefineRegistrarFields');
            do_action('jvb_define_registrar');
            do_action('jvb_define_fields');
        }
    }
inc/registrar/config/Integration.php
@@ -38,6 +38,7 @@
        $this->service_name = $service;
    }
    public function getService_name():string
    {
        return $this->service_name;
@@ -49,7 +50,12 @@
     */
    public function setContentType(string $content):self
    {
        $allowed = JVB()->connect($this->service_name)->getAllowedContent();
        $connection = JVB()->connect($this->service_name);
        if (!$connection){
            error_log('[Integration]::setContentType Service is not setup. '.$this->service_name);
            return $this;
        }
        $allowed = $connection->getAllowedContent();
        if (!in_array($content, $allowed)) {
            error_log($this->service_name.' Connection does not support this content: '.$content);
            return $this;
inc/registrar/config/Section.php
@@ -13,11 +13,10 @@
    protected string $description = '';
    protected string $icon = '';
    protected array $fields = [];
    protected Registrar $registrar;
    private Registrar $registrar;
    public function __construct(string $title, Registrar $registrar) {
        $this->title = $title;
        $this->slug = sanitize_title($title);
    public function __construct(string $slug, Registrar $registrar) {
        $this->slug = sanitize_title($slug);
        $this->registrar = $registrar;
    }
@@ -27,7 +26,7 @@
    }
    public function getTitle():string
    {
        return $this->title;
        return $this->title ?? ucwords(implode(' ', explode('-', $this->slug)));
    }
    public function setDescription(string $description):self
@@ -45,6 +44,10 @@
        $this->icon = $icon;
        return $this;
    }
    public function getIcon():string
    {
        return $this->icon;
    }
    protected function checkFields(string|array $fields):string|array
    {
inc/registrar/fields/Field.php
@@ -21,6 +21,10 @@
    protected int $maxLength;           // of characters
    protected int $min;
    protected int $max;
    /**
     * @var float $step For number fields. Indicates the amount the number increases/decreases with the plus/minus buttons
     */
    protected float $step;
    protected string $subtype;
    protected array $condition;
    protected array $allowedSubtype = ['text', 'url','number','tel','email','number'];
@@ -64,6 +68,11 @@
        }
    }
    public function getName():string
    {
        return $this->name;
    }
    public function setDescription(string $description):void
    {
        $this->description = $description;
@@ -144,6 +153,14 @@
    {
        return $this->max??null;
    }
    public function setStep(float $step):void
    {
        $this->step = $step;
    }
    public function getStep():?float
    {
        return $this->step??null;
    }
    public function setMaxLength(int $maxLength):void
    {
        $this->maxLength = $maxLength;
inc/registrar/helpers/AddIntegrationFields.php
@@ -11,13 +11,13 @@
class AddIntegrationFields {
    protected string $service_name;
    protected Registrar $registrar;
    private Registrar $registrar;
    protected Integration $config;
    protected array $allowed;
    public function __construct(string $service_name, ?Registrar $registrar = null) {
        $this->initAllowed();
        if (!in_array($service_name, $this->allowed)) {
        if (!array_key_exists($service_name, $this->allowed)) {
            return;
        }
@@ -28,6 +28,7 @@
        $this->config = $registrar->getIntegration($service_name);
        add_action('jvb_define_integrations', [$this, 'addIntegrationFields'],20);
    }
    protected function initAllowed():void
    {
@@ -56,6 +57,7 @@
    public function addIntegrationFields():void
    {
        $fields = $this->getIntegrationFields();
//      error_log('[AddIntegrationFields] adding fields for '.$this->service_name.': '.print_r($fields, true));
        foreach ($fields as $fieldName => $fieldConfig) {
            $this->registrar->fields()->addField($fieldName, $fieldConfig);
        }
inc/registrar/helpers/HideSingle.php
@@ -12,15 +12,14 @@
class HideSingle {
    protected string $slug;
    protected string $postType;
    protected Registrar $registrar;
    public function __construct(string $slug, Registrar $registrar) {
        $this->slug = $slug;
        $this->postType = jvbCheckBase($slug);
        $this->registrar = $registrar;
        if ($this->registrar->hasFeature('hide_single')) {
        if ($registrar->hasFeature('hide_single')) {
            add_filter('is_post_type_viewable', [$this, 'hideFromPublic']);
            if ($this->registrar->hasFeature('redirect_to_author')) {
            if ($registrar->hasFeature('redirect_to_author')) {
                add_filter('post_type_link', [$this, 'redirectSingleToAuthor'], 15, 2);
                add_action('template_redirect', [$this, 'actuallyRedirectToAuthor']);
            } else {
inc/registrar/helpers/MakeCalendarType.php
@@ -12,7 +12,7 @@
class MakeCalendarType {
    protected string $slug;
    protected string $postType;
    protected Registrar $registrar;
    private Registrar $registrar;
    public function __construct(string $slug, Registrar $registrar) {
        $this->slug = $slug;
        $this->postType = jvbCheckBase($slug);
@@ -25,6 +25,13 @@
        add_action('init', [$this, 'addCalendarRewrites']);
    }
    public function __debugInfo() {
        $vars = get_object_vars($this);
        unset($vars['registrar']);
        return $vars;
    }
    protected function addCalendarFields():void
    {
inc/registrar/helpers/MakeTimelineType.php
@@ -12,11 +12,9 @@
class MakeTimelineType {
    protected string $slug;
    protected string $postType;
    protected Registrar $registrar;
    public function __construct(string $slug, Registrar $registrar) {
    public function __construct(string $slug) {
        $this->slug = $slug;
        $this->postType = jvbCheckBase($slug);
        $this->registrar = $registrar;
        add_action('template_redirect', [$this, 'redirectChildToParent']);
    }
inc/rest/Rest.php
@@ -224,7 +224,7 @@
        // Keep existing author filtering logic
        $authorQuery = [];
        foreach (Registrar::getFeatured('can_create', 'user') as $type) {
        foreach (Registrar::withFeature('can_create', 'user') as $type) {
            if (array_key_exists($type, $data)) {
                $artist_ids = array_map(
                    'absint',
@@ -537,7 +537,7 @@
            return false;
        }
        $post_types = is_array($args['post_type']) ? $args['post_type'] : [$args['post_type']];
        $hasTimeline = array_map(function($item) { return jvbCheckBase($item); },Registrar::getFeatured('is_timeline', 'post'));
        $hasTimeline = array_map(function($item) { return jvbCheckBase($item); },Registrar::withFeature('is_timeline', 'post'));
        return !empty(array_intersect($post_types, $hasTimeline));
    }
    // =========================================================================
inc/rest/RestRouteManager.php
@@ -323,7 +323,7 @@
        // Keep existing author filtering logic
        $authorQuery = [];
        foreach (Registrar::getFeatured('can_create', 'user') as $type) {
        foreach (Registrar::withFeature('can_create', 'user') as $type) {
            if (array_key_exists($type, $data)) {
                $artist_ids = array_map(
                    'absint',
@@ -479,7 +479,7 @@
    protected function isTimeline($args, $data):bool
    {
        $post_types = is_array($args['post_type']) ? $args['post_type'] : [$args['post_type']];
        $areTimeline = array_map(function($type) { return BASE.$type; },Registrar::getFeatured('is_timeline', 'post'));
        $areTimeline = array_map(function($type) { return BASE.$type; },Registrar::withFeature('is_timeline', 'post'));
        return !empty(array_intersect($post_types, $areTimeline));
    }
inc/rest/_setup.php
@@ -51,11 +51,11 @@
//if (Site::has('referrals')) {
    require(JVB_DIR . '/inc/rest/routes/ReferralRoutes.php');
//}
//if (!empty(Registrar::getFeatured('has_responses'))) {
//if (!empty(Registrar::withFeature('has_responses'))) {
    require(JVB_DIR . '/inc/rest/routes/ResponseRoutes.php');
//}
//if (!empty(Registrar::getFeatured('karma'))) {
//if (!empty(Registrar::withFeature('karma'))) {
    require(JVB_DIR . '/inc/rest/routes/VoteRoutes.php');
//}
inc/rest/routes/ApprovalRoutes.php
@@ -41,11 +41,11 @@
        $this->termTypes = [];
        $this->allTypes = [];
        if ($this->hasMemberApproval) {
            $this->userTypes = Registrar::getFeatured('approve_new', 'user');
            $this->userTypes = Registrar::withFeature('approve_new', 'user');
            $this->allTypes = $this->userTypes;
        }
        if (Site::has('term_approval')) {
            $this->termTypes = Registrar::getFeatured('approve_new', 'term');
            $this->termTypes = Registrar::withFeature('approve_new', 'term');
            $this->allTypes[] = 'term';
        }
    }
inc/rest/routes/ContentTermsRoutes.php
@@ -55,7 +55,7 @@
    {
        $registry = JVB()->queue()->registry();
        $executor = new ContentTermExecutor();
        $taxonomies = Registrar::getFeatured('is_content', 'term');
        $taxonomies = Registrar::withFeature('is_content', 'term');
        foreach($taxonomies as $taxonomy) {
            $registry->register("{$taxonomy}_update", new TypeConfig(
inc/rest/routes/FeedRoutes.php
@@ -501,7 +501,7 @@
                    : explode(',', $args['post_type']);
                // Check if filtering global feed content
                if (in_array(jvbNoBase($context['type']), Registrar::getFeatured('is_content', 'term'))) {
                if (in_array(jvbNoBase($context['type']), Registrar::withFeature('is_content', 'term'))) {
                    // Global: show posts from any content type with this taxonomy
                    $for_content = Registrar::getInstance($context['type'])->registrar->for ?? [];
@@ -509,7 +509,7 @@
                    $post_types = array_map(fn($type) => jvbCheckBase($type), $for_content);
                    // Filter to only show_feed content types
                    $show_feed_types = Registrar::getFeatured('show_feed', 'post');
                    $show_feed_types = Registrar::withFeature('show_feed', 'post');
                    $args['post_type'] = array_intersect(
                        $post_types,
                        array_map(fn($type) => jvbCheckBase($type), $show_feed_types)
@@ -1147,7 +1147,7 @@
                $config = [];
                // Get content types with show_feed
                $contentTypes = Registrar::getFeatured('show_feed', 'post');
                $contentTypes = Registrar::withFeature('show_feed', 'post');
                foreach ($contentTypes as $slug) {
                    $this->cache->tag('content:'.$slug);
                    $registrar = Registrar::getInstance($slug);
@@ -1163,7 +1163,7 @@
                }
                // Get taxonomies with show_feed (content taxonomies)
                $taxonomies = Registrar::getFeatured('show_feed', 'term');
                $taxonomies = Registrar::withFeature('show_feed', 'term');
                foreach ($taxonomies as $slug) {
                    $registrar = Registrar::getInstance($slug);
                    if (!$registrar || !($registrar->hasFeature('is_content') ?? false)) {
inc/rest/routes/LoginRoutes.php
@@ -136,18 +136,23 @@
     */
    public function handleLogin(WP_REST_Request $request): WP_REST_Response
    {
        error_log('Handling login...');
        $email = sanitize_email($request->get_param('user_email'));
        $password = $request->get_param('user_password');
        $remember = (bool) $request->get_param('remember_me');
        $redirect_to = $request->get_param('redirect_to');
        // Verify Turnstile
        if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) {
            error_log('[handleLogin]Turnstile failed');
            return $this->error(
                'Security verification failed. Please try again.',
                'turnstile_failed',
                403
            );
        } else {
            error_log('[handleLogin]Turnstile succeeded');
        }
        // Attempt authentication
@@ -720,9 +725,7 @@
    protected function buildAuth(?int $user = null): array
    {
        $userId = $user ?? (is_user_logged_in() ? get_current_user_id() : 0);
        $cacheKey = $userId ?: 'guest';
        return Cache::for('auth', 300)->remember($cacheKey, function() use ($userId) {
            if ($userId) {
                return [
                    'authenticated' => true,
@@ -730,12 +733,12 @@
                    'nonces'        => $this->getUserNonces($userId),
                ];
            }
            return [
                'authenticated' => false,
                'user'          => false,
                'nonces'        => ['wp_rest' => wp_create_nonce('wp_rest')],
            ];
        });
    }
    protected function getUserNonces(int $userID):array {
        $nonces = [
@@ -747,7 +750,7 @@
        if (Site::has('favourites')) {
            $nonces['favourites'] = wp_create_nonce('favourites-'.$userID);
        }
        if (!empty(Registrar::getFeatured('karma'))) {
        if (!empty(Registrar::withFeature('karma'))) {
            $nonces['votes'] = wp_create_nonce('votes-'.$userID);
        }
        if (Site::has('notifications')) {
inc/rest/routes/NotificationsRoutes.php
@@ -555,7 +555,7 @@
                $statusCondition = $wpdb->prepare("a.status = %s", $status);
            }
            $approvals = Registrar::getFeatured('approve_new');
            $approvals = Registrar::withFeature('approve_new');
            foreach ($approvals as $type => $config) {
                $table = $wpdb->prefix . BASE . 'approval_' . $type . 'requests';
                $votes = $wpdb->prefix . BASE . 'approval_' . $type . 'votes';
jvb.php
@@ -4,7 +4,7 @@
Plugin URI: https://jakevan.ca
Description: The Base Plugin for JakeVan clients
Author: Jake Vanderwerf
Version: 1.0.0
Version: 1.1.0
Author URI: https://jakevan.ca/
Textdomain: jvb
*/
@@ -88,6 +88,7 @@
}, 10, 3);
function jvbIgnoredPostTypes():array
{
    return [BASE.'directory', BASE.'dash', 'attachment', 'revision', 'nav_menu_item'];
@@ -248,11 +249,54 @@
require(JVB_DIR . '/inc/admin/_setup.php');
require(JVB_DIR . '/JVBase.php');
/**
 * After moving to the Registrar::based registration, we need to carefully time
 *  when Site gets defined, as well as Registrar initial definitions, field definitions,
 *  and integration config
 * These custom actions should simplify the timing for us.
 */
add_action('plugins_loaded', 'jvb_site_definitions',1);
add_action('plugins_loaded', 'jvb_registrar_definitions',2);
add_action('plugins_loaded', 'jvb_field_definitions', 3);
add_action('init', 'jvbLoadBase', 1);
add_action('init', 'jvb_integration_definitions',3);
add_action('init', 'jvb_field_section_definitions', 5);
/**
 * Can define the Site settings
 * @return void
 */
function jvb_site_definitions():void
{
    do_action('jvb_define_site');
}
function jvb_registrar_definitions():void
{
    do_action('jvb_define_registrar');
}
function jvb_field_definitions():void
{
    do_action('jvb_define_fields');
}
function jvbLoadBase():void
{
    JVB::getInstance();
}
function jvb_integration_definitions():void
{
    do_action('jvb_define_integrations');
}
function jvb_field_section_definitions():void
{
    do_action('jvb_define_field_sections');
    Registrar::maybeBuildSections();
}
function JVB(): JVB
{
    return JVB::getInstance();
@@ -317,7 +361,7 @@
    if (Site::has('favourites')) {
        $interactions[] = 'favourites';
    }
    if (!empty(Registrar::getFeatured('karma'))) {
    if (!empty(Registrar::withFeature('karma'))) {
        $interactions[] = 'karma';
    }
    if (Site::has('notifications')) {
@@ -372,7 +416,7 @@
        }';
    }
    if (!empty(Registrar::getFeatured('karma'))) {
    if (!empty(Registrar::withFeature('karma'))) {
        wp_enqueue_script('jvb-votes');
        $initUserSettings .= '// Fetch user votes
        try {