has_dev_override() ) { return $this->get_tier_from_options(); } // 2. Freemius active + registered → query plan if ( $this->is_freemius_active() ) { $fs = $this->get_freemius(); if ( $fs !== null && $fs->is_registered() ) { return $this->get_tier_from_freemius( $fs ); } } // 3. Otherwise free. The tier option used to be the fallback here, so // `wp option update webcal_license_tier agency` unlocked every // paid gate on the premium build without a license. It is read // only through the dev override now. return self::FREE; } public function set_tier( string $tier ): void { if ( in_array( $tier, FeatureMap::TIER_ORDER, true ) ) { update_option( self::TIER_OPTION, $tier ); } } /** * Check if a feature is available in the current tier. * * @param string $feature Feature identifier. * @return bool */ public function is_feature_available( string $feature ): bool { $min_tier = FeatureMap::min_tier( $feature ); if ( null === $min_tier ) { // Undeclared keys are not gated. FeatureGatingSyncTest forbids // unknown keys at call sites, so this cannot hide a typo. return true; } return FeatureMap::tier_rank( $this->get_tier() ) >= FeatureMap::tier_rank( $min_tier ); } /** * Check if Freemius SDK is active and available. * * Override in test subclasses (or via Mockery partial mock) to simulate * the "Freemius not loaded" code path. The default implementation only * checks that the global helper function is defined; in tests the helper * is always defined via the stub in tests/phpstan-freemius-stub.php so * the marker-based pro code paths can be exercised. * * @return bool */ public function is_freemius_active(): bool { return $this->freemius_helper_available(); } /** * Indirection for is_freemius_active() so tests can override the * "is webcal_fs() defined?" check via Mockery partial mocks. * * @return bool */ protected function freemius_helper_available(): bool { return function_exists( 'webcal_fs' ); } /** * Get the Freemius instance, or null if unavailable. * * @return \Freemius|null */ public function get_freemius(): ?\Freemius { if ( ! $this->is_freemius_active() ) { return null; } return function_exists( 'webcal_fs' ) ? webcal_fs() : null; } /** * Whether this site may use the dev tier override at all: WP_DEBUG on, * and an environment type of local or development. WP_DEBUG alone was * the guard before, and it is often left on in staging and production. * The settings screen shows the override fields only when this is true. */ public function dev_override_allowed(): bool { return $this->debug_enabled() && in_array( wp_get_environment_type(), [ 'local', 'development' ], true ); } /** * Check if the dev override is enabled. * * @return bool */ private function has_dev_override(): bool { if ( ! $this->dev_override_allowed() ) { return false; } $value = get_option( self::DEV_OVERRIDE_OPTION, 'no' ); return is_scalar( $value ) && (string) $value === 'yes'; } /** * WP_DEBUG, behind a method so tests can simulate it: a test cannot * define the constant without defining it for the whole suite. */ protected function debug_enabled(): bool { return defined( 'WP_DEBUG' ) && WP_DEBUG; } /** * Determine tier from Freemius plan. * * @param \Freemius $fs Freemius instance. * @return string */ private function get_tier_from_freemius( \Freemius $fs ): string { // A trial unlocks the tier it is a trial of. The SDK's is_paying() // excludes trials, and during one the site's own plan is still the // free plan (the trial plan is held separately), so neither // is_paying() nor is_plan() sees it. Checking only those gave // trialing sites the premium code but the free tier's gates. // // Exact matches only. The non-exact is_trial_plan() ranks plans by // their position in the SDK's cached list, where get_trial_plan() // appends a freshly fetched trial plan last (above Agency), and a // plan name missing from the list always matches — so a Starter // trial could resolve to Agency. (is_plan() does not share this: a // missing plan never matches there.) if ( $fs->is_trial() ) { return $this->highest_matching_tier( static fn ( string $plan ): bool => $fs->is_trial_plan( $plan, true ) ); } if ( ! $fs->is_paying() ) { return self::FREE; } return $this->highest_matching_tier( static fn ( string $plan ): bool => $fs->is_plan( $plan ) ); } /** * The highest paid tier whose plan matches, or Starter when none is * recognized: a paying or trialing site always gets at least that. * * @param callable(string): bool $matches Plan-name predicate. */ private function highest_matching_tier( callable $matches ): string { foreach ( [ self::AGENCY, self::BUSINESS, self::STARTER ] as $tier ) { if ( $matches( $tier ) ) { return $tier; } } return self::STARTER; } /** * Determine tier from wp_options. * * @return string */ private function get_tier_from_options(): string { $value = get_option( self::TIER_OPTION, self::FREE ); return is_scalar( $value ) ? (string) $value : self::FREE; } }