' ) — never the * tier directly — so the free/premium split can be changed here alone. * * Tiers: free < pro < agency. * * No premium folder (free build) → always 'free'. In the Pro build the * license decides (1.2.0): bootstrap.php hooks the clusterlane_edition filter and * maps the Freemius plan to 'pro' or 'agency'; without an active license * the tier stays at DEFAULT_TIER ('free'). * * The CLUSTERLANE_EDITION constant in wp-config.php ('free' | 'pro' | 'agency') can * only LOWER the tier — handy for trying the free experience on a licensed * site. It never unlocks anything. */ final class Edition { const DEFAULT_TIER = 'free'; const TIERS = [ 'free' => 0, 'pro' => 1, 'agency' => 2 ]; /** * feature => lowest tier that has it. Anything not listed is free. * Free (not listed): scan, clusters, health/archetypes, dashboard, * Cluster/Article Inspector, graphs, Text Match, custom clusters, * roles & capabilities, Help, support report, multisite install/cleanup. */ const FEATURES = [ 'ai' => 'pro', // every AI call: gap, briefs/drafts, link suggestions, analyses, cluster suggestions 'link_templates' => 'pro', // Link Card Templates 'elementor' => 'pro', // writing links into Elementor layouts 'ai_cost' => 'pro', // AI usage & limits (only meaningful with AI) 'report' => 'pro', 'pillar_box' => 'pro', // link box to the chosen pillar, added when the article is shown // Report page for your own site (history, work done); Free shows the demo example 'network' => 'agency', // Network Admin → Clusterlane overview + CSV 'network_ai' => 'agency', // network-wide AI key, policy and per-site allowance ]; public static function tier(): string { // Free build (no pro__premium_only/ folder): always Free, whatever the // constant or filter says — there is no premium code to run. if ( ! self::premium_code() ) return 'free'; $tier = (string) apply_filters( 'clusterlane_edition', self::DEFAULT_TIER ); if ( ! isset( self::TIERS[ $tier ] ) ) $tier = 'free'; $cap = defined( 'CLUSTERLANE_EDITION' ) ? (string) CLUSTERLANE_EDITION : ''; if ( isset( self::TIERS[ $cap ] ) && self::TIERS[ $cap ] < self::TIERS[ $tier ] ) $tier = $cap; return $tier; } /** * The tier a Freemius license gives: 'agency' for the Agency plan, 'pro' * for Pro (or a trial of it), 'free' without an active license. * * @param object|null $fs The Freemius instance (anything with can_use_premium_code() and is_plan()). */ public static function from_license( $fs ): string { if ( ! is_object( $fs ) || ! method_exists( $fs, 'can_use_premium_code' ) || ! $fs->can_use_premium_code() ) return 'free'; return $fs->is_plan( 'agency' ) ? 'agency' : 'pro'; } /** True when the premium folder is installed (it defines CLUSTERLANE_PRO_LOADED). */ public static function premium_code(): bool { return defined( 'CLUSTERLANE_PRO_LOADED' ); } public static function has( string $feature ): bool { $needs = self::FEATURES[ $feature ] ?? 'free'; return self::TIERS[ self::tier() ] >= self::TIERS[ $needs ]; } /** Lowest tier that includes a feature ('free' | 'pro' | 'agency'). */ public static function tier_for( string $feature ): string { return self::FEATURES[ $feature ] ?? 'free'; } public static function label( ?string $tier = null ): string { $n = Brand::name(); return [ 'free' => $n . ' Free', 'pro' => $n . ' Pro', 'agency' => $n . ' Agency' ][ $tier ?? self::tier() ]; } public static function upgrade_url(): string { return Brand::url( 'pricing' ); // keeps the clusterlane_upgrade_url filter } /** "… is part of Clusterlane Pro." — shown wherever a feature is missing. */ public static function missing_message( string $feature ): string { /* translators: %s: edition name, e.g. Clusterlane Pro */ return sprintf( __( 'This feature is part of %s.', 'clusterlane' ), self::label( self::tier_for( $feature ) ) ); } /** * For REST permission callbacks: true, or a 403 explaining the edition. * Use as: fn() => Capabilities::can_use() ? Edition::rest( 'ai' ) : false */ public static function rest( string $feature ): bool|\WP_Error { return self::has( $feature ) ? true : new \WP_Error( 'clusterlane_edition', self::missing_message( $feature ), [ 'status' => 403 ] ); } /** Data for JS (clusterlaneEdition). */ public static function for_js(): array { $features = []; foreach ( array_keys( self::FEATURES ) as $f ) $features[ $f ] = self::has( $f ); return [ 'tier' => self::tier(), 'label' => self::label(), 'features' => $features, 'upgradeUrl' => self::upgrade_url(), ]; } }