$args Extra query args.
*/
public static function url( array $args = array() ): string {
return add_query_arg( array_merge( array( 'page' => self::SLUG ), $args ), admin_url( 'admin.php' ) );
}
public function handle_connect(): void {
self::guard( 'cortexcommerce_connect' );
$code = isset( $_POST['cortex_code'] ) ? sanitize_text_field( wp_unslash( $_POST['cortex_code'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in guard().
$outcome = ( new ConnectFlow() )->connect( $code, get_current_user_id() );
wp_safe_redirect( self::url( array( 'cortexcommerce_result' => $outcome ) ) );
exit;
}
public function handle_disconnect(): void {
self::guard( 'cortexcommerce_disconnect' );
Disconnector::disconnect( 'disconnected' );
wp_safe_redirect( self::url( array( 'cortexcommerce_result' => 'disconnected' ) ) );
exit;
}
public function render(): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}
echo '
';
self::render_header();
$this->render_notices();
if ( Options::is_connected() ) {
$this->render_connected();
} else {
$this->render_connect_form();
}
echo '
';
}
/**
* The brand band, shared with the Enriched products page. WordPress moves admin notices below .wp-header-end, so
* none of them land inside it.
*/
public static function render_header(): void {
$connected = Options::is_connected();
echo '';
}
private function render_notices(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only.
$result = isset( $_GET['cortexcommerce_result'] ) ? sanitize_key( wp_unslash( $_GET['cortexcommerce_result'] ) ) : '';
if ( 'connected' === $result && Options::is_connected() ) {
self::notice( 'success', __( 'Your store is connected to CortexCommerce.', 'cortexcommerce' ) );
} elseif ( 'disconnected' === $result && ! Options::is_connected() ) {
self::notice( 'success', __( 'Your store is disconnected. The API key was removed and product sync stopped.', 'cortexcommerce' ) );
} elseif ( 'busy' === $result ) {
self::notice( 'warning', Messages::for_reason( 'busy' ) );
}
// Only on the redirect right after the attempt that stored it: a stale error must never sit above a new code.
$error = Options::last_error();
if ( null !== $error && $error['code'] === $result && ! Options::is_connected() ) {
self::notice( 'error', $error['message'] );
}
}
private function render_connected(): void {
// Sync problems stay on screen until a later sync succeeds (ProductSyncSender clears them).
$error = Options::last_error();
if ( null !== $error && 0 === strpos( $error['code'], 'sync_' ) ) {
self::notice( 'warning', $error['message'] );
}
StatsPanel::render();
SyncSection::render();
echo '' . esc_html__( 'Disconnect this store', 'cortexcommerce' ) . '
';
echo '' . esc_html__( 'Revokes the CortexCommerce API key and stops the product sync. To connect again you need a new code from app.cortexcommerce.ai.', 'cortexcommerce' ) . '
';
self::action_form( 'cortexcommerce_disconnect', __( 'Disconnect', 'cortexcommerce' ), 'button-secondary' );
echo ' ';
}
private function render_connect_form(): void {
$blocker = Config::connect_blocker();
if ( null !== $blocker ) {
self::notice( 'error', Messages::for_reason( $blocker ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the code only pre-fills the form; nothing happens without the merchant's click.
$code = isset( $_GET['cortex_code'] ) ? sanitize_text_field( wp_unslash( $_GET['cortex_code'] ) ) : '';
// Arriving with a code means the merchant is about to connect: no other way forward, and the grant is open to read.
$has_code = '' !== $code;
if ( ! $has_code ) {
$this->render_paths();
}
echo '';
echo '
' . ( $has_code ? esc_html__( 'Connect this store', 'cortexcommerce' ) : esc_html__( 'Have your code?', 'cortexcommerce' ) ) . '
';
echo '
';
}
/**
* The two ways to a connect code. Both end in the portal, which hands the code back here.
*/
private function render_paths(): void {
echo '';
echo '
' . esc_html__( 'New to CortexCommerce?', 'cortexcommerce' ) . '
';
echo '
' . esc_html__( 'Create a free account and add this store. You’ll get a code to bring back here.', 'cortexcommerce' ) . '
';
self::external_button( Config::signup_url(), __( 'Create free account', 'cortexcommerce' ), 'button-primary' );
echo '
';
echo '
' . esc_html__( 'Already have an account?', 'cortexcommerce' ) . '
';
echo '
' . esc_html__( 'Sign in and choose Add store, then WooCommerce, to get your code.', 'cortexcommerce' ) . '
';
self::external_button( Config::connect_url(), __( 'Get my code', 'cortexcommerce' ), 'button-secondary' );
echo '
';
}
/**
* A button-styled link into the portal, which opens in its own tab so the merchant keeps this screen.
*
* @param string $url The destination.
* @param string $label The button text.
* @param string $button_class button-primary or button-secondary.
*/
public static function external_button( string $url, string $label, string $button_class ): void {
self::external_link( $url, $label, 'button ' . $button_class );
}
/**
* A link that opens in its own tab, marked with an arrow and, for screen readers, "(opens in a new tab)".
*
* @param string $url The destination.
* @param string $label The link text.
* @param string $css_class CSS classes, or '' for a plain link.
*/
public static function external_link( string $url, string $label, string $css_class = '' ): void {
printf(
'%3$s ↗ %4$s',
esc_url( $url ),
'' === $css_class ? '' : ' class="' . esc_attr( $css_class ) . '"',
esc_html( $label ),
esc_html__( '(opens in a new tab)', 'cortexcommerce' )
);
}
private static function guard( string $action ): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'cortexcommerce' ), 403 );
}
check_admin_referer( $action );
}
private static function notice( string $type, string $message ): void {
printf( '', esc_attr( $type ), esc_html( $message ) );
}
private static function action_form( string $action, string $label, string $button_class ): void {
echo '';
}
}