get_upgrade_url()` and `get_account_url()` return
* `admin.php?page=ai-rank-suite-pricing` / `-account`. The SDK registers
* neither page until the customer has opted in AND registered
* (Freemius::add_submenu_items() gates the account page on is_registered()
* and the pricing page on the `pricing` menu flag we deliberately switch
* off). On a fresh free install both slugs are unregistered, and WordPress
* answers an unregistered admin page by sending the visitor to the
* dashboard.
* - The fallback was `https://airanksuite.com/pricing` through
* wp_safe_redirect(), which refuses any host that is not this site and
* substitutes admin_url() — the dashboard again.
*
* So every path out of "Upgrade to Pro" landed on the WordPress dashboard,
* which reads exactly like a plugin that takes your money and does nothing.
*
* A page has no such failure mode: it renders what it has to say, and the one
* link that leaves the site is an ordinary anchor the browser follows.
*
* @package AiRankSuite
*/
declare(strict_types=1);
namespace AiRankSuite\Modules\Core;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class UpgradeScreen {
/** Menu slug. Free edition only — the Pro build never registers it. */
public const SLUG = 'ai-rank-suite-upgrade';
/** Freemius plan id. Public information; it is in the checkout URL. */
private const PLAN_ID = 63286;
/** Launch coupon, and the day it stops being true. */
private const COUPON = 'GETNOW30';
private const COUPON_ENDS = '2026-09-30';
/** Where this screen lives. */
public static function url(): string {
return admin_url( 'admin.php?page=' . self::SLUG );
}
/**
* Where the buy button goes, and whether that leaves WordPress.
*
* Freemius' own in-admin checkout is preferred when it exists, because a
* purchase made there activates the licence on this site by itself. It
* only exists once the SDK has registered its pricing page, so the answer
* is computed while the page renders — at that point $_registered_pages is
* complete and the question can be answered truthfully rather than
* guessed.
*
* @return array{0:string,1:bool} URL, and true when it is off-site.
*/
public static function checkout(): array {
if ( function_exists( 'airanksuite_fs' ) && null !== airanksuite_fs() && method_exists( airanksuite_fs(), 'checkout_url' ) ) {
$url = (string) airanksuite_fs()->checkout_url();
if ( '' !== $url && self::page_exists( self::page_of( $url ) ) ) {
return array( $url, false );
}
}
$id = defined( 'AIRANKSUITE_FS_ID' ) && is_numeric( AIRANKSUITE_FS_ID ) ? (int) AIRANKSUITE_FS_ID : 0;
// A development build has no product id, so there is no checkout to
// send anyone to. The public pricing page is the honest destination.
if ( $id <= 0 ) {
return array( 'https://airanksuite.com/pricing', true );
}
$url = sprintf( 'https://checkout.freemius.com/plugin/%d/plan/%d/', $id, self::PLAN_ID );
if ( self::coupon_live() ) {
$url = add_query_arg( 'coupon', self::COUPON, $url );
}
/**
* Filter the destination of the buy button.
*
* @param string $url Checkout URL.
*/
return array( (string) apply_filters( 'airanksuite/upgrade_url', $url ), true );
}
/** Is the launch discount still running? */
private static function coupon_live(): bool {
return time() < (int) strtotime( self::COUPON_ENDS . ' 23:59:59 UTC' );
}
/**
* Where somebody who already paid activates their key.
*
* `get_activation_url()` resolves to the menu slug handed to the SDK in
* freemius.php, which this plugin registers itself — so unlike the account
* and pricing slugs, it is always a page that exists.
*/
public static function activation_url(): string {
if ( function_exists( 'airanksuite_fs' ) && null !== airanksuite_fs() && method_exists( airanksuite_fs(), 'get_activation_url' ) ) {
$url = (string) airanksuite_fs()->get_activation_url();
if ( '' !== $url && self::page_exists( self::page_of( $url ) ) ) {
return $url;
}
}
return admin_url( 'admin.php?page=ai-rank-suite-settings#/settings/license' );
}
/** The `page` query arg of an admin URL, or '' when there is none. */
public static function page_of( string $url ): string {
$query = (string) wp_parse_url( $url, PHP_URL_QUERY );
if ( '' === $query ) {
return '';
}
parse_str( $query, $args );
return isset( $args['page'] ) ? sanitize_key( (string) $args['page'] ) : '';
}
/**
* Has some plugin actually registered this admin page?
*
* `$_registered_pages` is the list add_submenu_page() writes to and the
* list admin.php checks before rendering. Asking it is the only way to
* know that a link leads somewhere rather than to the dashboard.
*/
public static function page_exists( string $page ): bool {
if ( '' === $page ) {
return false;
}
global $_registered_pages;
if ( ! is_array( $_registered_pages ) ) {
return false;
}
foreach ( array_keys( $_registered_pages ) as $hook ) {
if ( str_ends_with( (string) $hook, '_page_' . $page ) ) {
return true;
}
}
return false;
}
/** What the free edition already does, in the buyer's words. */
private static function free_features(): array {
return array(
__( 'Titles, descriptions and schema for every post type', 'ai-rank-suite' ),
__( 'Sitemap, llms.txt and the crawl rules for AI assistants', 'ai-rank-suite' ),
__( 'Site audit, redirects and the 404 log', 'ai-rank-suite' ),
__( 'Imports from 14 other SEO plugins, including Rank Math and Yoast', 'ai-rank-suite' ),
);
}
/** What paying adds. One line per Pro module, no invented capabilities. */
private static function pro_features(): array {
return array(
__( 'Everything in the free edition', 'ai-rank-suite' ),
__( 'AI visibility: ask ChatGPT, Claude, Gemini, Perplexity and Mistral the questions your buyers ask, and see who gets named', 'ai-rank-suite' ),
__( 'Rank tracking for up to 100 keywords, long and short mixed', 'ai-rank-suite' ),
__( 'Bulk descriptions and FAQ, written on your own API key at your provider\'s list price', 'ai-rank-suite' ),
__( 'Autopilot: it fixes what the audit finds, and every change can be undone', 'ai-rank-suite' ),
__( 'Internal links, broken-link checker, backlinks and scheduled reports', 'ai-rank-suite' ),
);
}
/**
* Render the screen.
*
* Self-contained: no bundle, no stylesheet, no external asset. This page
* has to paint correctly on an install where something else is broken,
* because it is the page a customer reaches when they want to pay.
*/
public static function render(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You cannot see this page.', 'ai-rank-suite' ), '', array( 'response' => 403 ) );
}
list( $checkout, $external ) = self::checkout();
$target = $external ? ' target="_blank" rel="noopener"' : '';
echo '
';
echo '
' . esc_html__( 'Upgrade to Pro', 'ai-rank-suite' ) . '
';
/*
* The hero says what the product is for before it says what it costs.
* The panel on the right is the product's whole idea in one picture —
* one question, five assistants, and only some of them name you.
*/
echo '
';
echo '
';
echo '
AI Rank Suite Pro';
echo '
' . esc_html__( 'See which assistants name you — and fix the pages that stop them.', 'ai-rank-suite' ) . '
';
echo '
' . esc_html__( 'The free edition prepares your site for AI search. Pro measures whether it worked: it asks the assistants your customers ask, tracks what changes, and does the repetitive fixing for you.', 'ai-rank-suite' ) . '
';
echo '
';
echo '
'
. '' . esc_html__( '14-day money back', 'ai-rank-suite' ) . ''
. '' . esc_html__( 'Your own API key, no markup', 'ai-rank-suite' ) . ''
. '' . esc_html__( 'Cancel any time', 'ai-rank-suite' ) . ''
. '
';
echo '
';
echo '
';
echo '
“' . esc_html__( 'Who should I hire for this in my city?', 'ai-rank-suite' ) . '”
';
foreach ( array(
array( 'ChatGPT', true ),
array( 'Claude', false ),
array( 'Gemini', true ),
array( 'Perplexity', false ),
array( 'Mistral', false ),
) as $row ) {
echo '
'
. '' . esc_html( $row[0] ) . ''
. '' . ( $row[1] ? esc_html__( 'names you', 'ai-rank-suite' ) : esc_html__( 'does not', 'ai-rank-suite' ) ) . ''
. '
';
}
echo '
';
echo '
';
echo '
' . esc_html__( 'What each plan includes', 'ai-rank-suite' ) . '
';
echo '
';
// --- free -----------------------------------------------------------
echo '
';
echo '
' . esc_html__( 'Your plan', 'ai-rank-suite' ) . '';
echo '
' . esc_html__( 'Free', 'ai-rank-suite' ) . '
';
echo '
$0' . esc_html__( 'installed and running', 'ai-rank-suite' ) . '
';
echo '
';
foreach ( self::free_features() as $line ) {
echo '- ' . esc_html( $line ) . '
';
}
echo '
';
echo '
';
// --- pro ------------------------------------------------------------
echo '
';
// --- agency ---------------------------------------------------------
echo '
';
echo '
' . esc_html__( 'For agencies', 'ai-rank-suite' ) . '';
echo '
' . esc_html__( 'Agency', 'ai-rank-suite' ) . '
';
echo '
$199' . esc_html__( 'a year, unlimited sites', 'ai-rank-suite' ) . '
';
echo '
- ' . esc_html__( 'Every Pro feature, on every client site you run. The plans differ in the number of sites, never in what the plugin can do.', 'ai-rank-suite' ) . '
';
echo '- ' . esc_html__( 'Choose the number of sites in the same checkout.', 'ai-rank-suite' ) . '
';
echo '
';
echo '
'; // grid
if ( self::coupon_live() ) {
echo '
'
. sprintf(
/* translators: 1: coupon code, 2: end date. */
esc_html__( 'Launch offer: 30%% off the first year with code %1$s, filled in at checkout, until %2$s.', 'ai-rank-suite' ),
'' . esc_html( self::COUPON ) . '',
esc_html( date_i18n( (string) get_option( 'date_format' ), (int) strtotime( self::COUPON_ENDS ) ) )
)
. '
';
}
echo '
';
echo '' . esc_html__( 'Full comparison on the website', 'ai-rank-suite' ) . ' ↗';
echo '
';
echo '
'; // wrap
}
/**
* Inline, because this page must render with nothing else loaded.
*
* The palette, the type and the shapes are the website's, not WordPress'.
* This is the one screen in the plugin whose job is to sell, and a screen
* that sells should look like the thing it is selling: same violet, same
* near-black, same DM Sans, same 18px card radius as airanksuite.com. The
* font is bundled (SIL OFL) rather than fetched — a wp-admin screen has no
* business calling a font host.
*/
public static function enqueue( string $hook ): void {
if ( ! str_ends_with( $hook, '_page_' . self::SLUG ) ) {
return;
}
$fonts = AIRANKSUITE_URL . 'assets/fonts/';
$css = '
@font-face{font-family:"AI Rank Suite DM Sans";font-style:normal;font-weight:100 1000;font-display:swap;
src:url(' . esc_url( $fonts . 'dmsans-latin.woff2' ) . ') format("woff2");
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"AI Rank Suite DM Sans";font-style:normal;font-weight:100 1000;font-display:swap;
src:url(' . esc_url( $fonts . 'dmsans-latin-ext.woff2' ) . ') format("woff2");
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
.airanksuite-upgrade{
--au-brand:#6753ff; --au-brand-2:#8b7bff; --au-brand-h:#5240e0;
--au-ink:#0e1116; --au-ink2:#4a5361; --au-ink3:#77808d;
--au-line:#e2e2ef; --au-alt:#f0f0fa;
--au-dark:#0b0e14; --au-dark2:#141926; --au-dark-ink:#e8ecf3; --au-dark-ink2:#98a3b5;
--au-good:#12a150;
--au-r:18px; --au-r-sm:10px;
max-width:1160px;
font-family:"AI Rank Suite DM Sans",-apple-system,system-ui,Roboto,"Helvetica Neue",Arial,sans-serif;
color:var(--au-ink);
-webkit-font-smoothing:antialiased}
.airanksuite-upgrade *{box-sizing:border-box}
.airanksuite-upgrade h2,.airanksuite-upgrade h3{letter-spacing:-.025em;line-height:1.12;margin:0}
/* The hero, with the website\'s own violet bloom in the top-left corner. */
.airanksuite-upgrade .au-hero{
position:relative;overflow:hidden;
display:grid;grid-template-columns:minmax(0,1.35fr) minmax(280px,.95fr);gap:38px;align-items:center;
margin:16px 0 40px;padding:40px;border-radius:var(--au-r);
background:var(--au-dark);color:var(--au-dark-ink);
box-shadow:0 24px 60px -12px rgba(14,17,22,.28),0 8px 20px -8px rgba(14,17,22,.16)}
.airanksuite-upgrade .au-hero:before{content:"";position:absolute;inset:-30% -10% auto -10%;height:120%;
pointer-events:none;
background:radial-gradient(60% 55% at 22% 8%,rgba(103,83,255,.34),transparent 62%),
radial-gradient(50% 45% at 88% 12%,rgba(139,123,255,.16),transparent 60%)}
.airanksuite-upgrade .au-hero > *{position:relative}
.airanksuite-upgrade .au-eyebrow{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.14em;
text-transform:uppercase;color:var(--au-brand-2);margin-bottom:14px}
.airanksuite-upgrade .au-hero h2{font-size:clamp(27px,3.1vw,40px);font-weight:760;letter-spacing:-.035em;
color:#fff;margin-bottom:14px}
.airanksuite-upgrade .au-hero p{margin:0;font-size:16.5px;line-height:1.65;color:var(--au-dark-ink2);max-width:52ch}
.airanksuite-upgrade .au-cta-row{display:flex;flex-wrap:wrap;align-items:center;gap:18px;margin:26px 0 0}
.airanksuite-upgrade .au-btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;
min-height:54px;padding:0 28px;border-radius:6px;border:1px solid transparent;
background:var(--au-brand);color:#fff;font-size:16.5px;font-weight:620;text-decoration:none;
box-shadow:0 4px 14px -3px rgba(103,83,255,.5),0 10px 34px -12px rgba(103,83,255,.42);
transition:transform .18s ease,box-shadow .18s ease,background .18s ease}
.airanksuite-upgrade .au-btn span{font-weight:500;opacity:.8}
.airanksuite-upgrade .au-btn:hover,.airanksuite-upgrade .au-btn:focus{background:var(--au-brand-h);color:#fff;
transform:translateY(-2px);box-shadow:0 10px 26px -6px rgba(103,83,255,.55)}
.airanksuite-upgrade .au-btn.is-block{display:flex;width:100%;margin-top:20px;min-height:46px;font-size:15px}
.airanksuite-upgrade .au-ghost{color:var(--au-dark-ink);font-size:15px;text-decoration:none;
border-bottom:1px solid rgba(232,236,243,.3);padding-bottom:2px}
.airanksuite-upgrade .au-ghost:hover{color:#fff;border-bottom-color:#fff}
.airanksuite-upgrade .au-trust{display:flex;flex-wrap:wrap;gap:10px 24px;margin:22px 0 0;font-size:13.5px;
color:var(--au-dark-ink2)}
.airanksuite-upgrade .au-trust span{position:relative;padding-left:18px}
.airanksuite-upgrade .au-trust span:before{content:"";position:absolute;left:2px;top:6px;width:4px;height:9px;
border:solid var(--au-good);border-width:0 2px 2px 0;transform:rotate(45deg)}
/* The panel: the same dark card the website uses inside a dark section. */
.airanksuite-upgrade .au-hero-art{background:var(--au-dark2);border:1px solid rgba(255,255,255,.07);
border-radius:14px;padding:18px}
.airanksuite-upgrade .au-ask{font-size:14px;line-height:1.5;color:var(--au-dark-ink);font-style:italic;
margin-bottom:14px}
.airanksuite-upgrade .au-row{display:flex;align-items:center;justify-content:space-between;gap:12px;
padding:10px 12px;border-radius:var(--au-r-sm);background:rgba(255,255,255,.04);margin-bottom:7px}
.airanksuite-upgrade .au-row:last-child{margin-bottom:0}
.airanksuite-upgrade .au-row-name{font-size:14px;font-weight:620;color:#fff}
.airanksuite-upgrade .au-row-verdict{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
color:#f08a84}
.airanksuite-upgrade .au-row.is-yes{background:rgba(18,161,80,.16)}
.airanksuite-upgrade .au-row.is-yes .au-row-verdict{color:#4fd08a}
.airanksuite-upgrade .au-h2{font-size:19px;font-weight:680;letter-spacing:-.015em;margin:0 0 16px;color:var(--au-ink)}
.airanksuite-upgrade .airanksuite-up-grid{display:grid;gap:20px;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));
align-items:stretch}
.airanksuite-upgrade .airanksuite-up-card{background:#fff;border:1px solid var(--au-line);border-radius:var(--au-r);
padding:28px;display:flex;flex-direction:column;
box-shadow:0 1px 2px rgba(14,17,22,.04),0 8px 24px rgba(14,17,22,.06)}
.airanksuite-upgrade .airanksuite-up-card.is-pro{border-color:transparent;
box-shadow:0 0 0 2px var(--au-brand),0 12px 32px -12px rgba(60,33,171,.28)}
.airanksuite-upgrade .airanksuite-up-tag{align-self:flex-start;font-size:11px;font-weight:700;letter-spacing:.09em;
text-transform:uppercase;color:var(--au-ink3);background:var(--au-alt);border-radius:999px;padding:5px 12px}
.airanksuite-upgrade .airanksuite-up-tag.is-pro{color:#fff;background:var(--au-brand)}
.airanksuite-upgrade .airanksuite-up-card h3{margin:16px 0 4px;font-size:19px;font-weight:680;letter-spacing:-.015em}
.airanksuite-upgrade .airanksuite-up-price{margin:0 0 18px;font-size:38px;font-weight:760;color:var(--au-ink);
line-height:1.1;letter-spacing:-.035em}
.airanksuite-upgrade .airanksuite-up-price span{display:block;font-size:14px;font-weight:400;color:var(--au-ink3);
margin-top:5px;letter-spacing:0}
.airanksuite-upgrade .airanksuite-up-card ul{margin:0;padding:0;list-style:none;flex:1}
.airanksuite-upgrade .airanksuite-up-card li{position:relative;padding:0 0 0 28px;margin:0 0 12px;font-size:14.5px;
line-height:1.6;color:var(--au-ink2)}
.airanksuite-upgrade .airanksuite-up-card li:before{content:"";position:absolute;left:6px;top:7px;width:5px;height:10px;
border:solid var(--au-brand);border-width:0 2px 2px 0;transform:rotate(45deg)}
.airanksuite-upgrade .airanksuite-up-card.is-pro li:before{border-color:var(--au-brand)}
.airanksuite-upgrade .airanksuite-up-note{margin:26px 0 0;font-size:14.5px;color:var(--au-ink2)}
.airanksuite-upgrade .airanksuite-up-note b{color:var(--au-brand);font-weight:700}
.airanksuite-upgrade .airanksuite-up-links{margin:10px 0 0;font-size:14.5px}
.airanksuite-upgrade .airanksuite-up-links a{color:var(--au-brand);text-decoration:none}
.airanksuite-upgrade .airanksuite-up-links a:hover{color:var(--au-brand-h);text-decoration:underline}
@media(max-width:900px){
.airanksuite-upgrade .au-hero{grid-template-columns:1fr;padding:28px 24px;gap:26px}
}
';
\AiRankSuite\Support\Inline::style( 'airanksuite-upgrade', $css );
}
}