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 '
'; 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 '
'; echo '' . esc_html__( 'Recommended', 'ai-rank-suite' ) . ''; echo '

' . esc_html__( 'Pro', 'ai-rank-suite' ) . '

'; echo '

$99' . esc_html__( 'a year, one site', 'ai-rank-suite' ) . '

'; echo '
    '; foreach ( self::pro_features() as $line ) { echo '
  • ' . esc_html( $line ) . '
  • '; } echo '
'; echo '' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- literal attribute built above. . esc_html__( 'Get Pro', 'ai-rank-suite' ) . ''; 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 '
'; // 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 ); } }