'unavailable' ); } $args = array( 'timeout' => 12, 'headers' => array( 'X-OrderGuard-Key' => orderguard_get_api_key(), 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'interval' => $interval ) ) ); $reply = wp_remote_post( ORDERGUARD_SERVER_URL . '/v1/paddle/checkout', $args ); if ( is_wp_error( $reply ) ) { return array( 'error' => 'unavailable' ); } $data = json_decode( wp_remote_retrieve_body( $reply ), true ); if ( 200 !== (int) wp_remote_retrieve_response_code( $reply ) ) { if ( is_array( $data ) && ( $data['detail'] ?? '' ) === 'checkout_other_interval_open' ) { return array( 'error' => 'other_interval' ); } return array( 'error' => is_array( $data ) && ( $data['detail'] ?? '' ) === 'already_pro_complimentary' ? 'complimentary' : 'unavailable' ); } if ( ! is_array( $data ) || array_keys( $data ) !== array( 'transaction_id' ) || ! orderguard_checkout_url( $data['transaction_id'] ) ) { return array( 'error' => 'unavailable' ); } // Format is not ownership: the authenticated server must confirm its ledger. $args['body'] = wp_json_encode( array( 'transaction_id' => $data['transaction_id'] ) ); $check = wp_remote_post( ORDERGUARD_SERVER_URL . '/v1/paddle/checkout/verify', $args ); if ( is_wp_error( $check ) || 200 !== (int) wp_remote_retrieve_response_code( $check ) || json_decode( wp_remote_retrieve_body( $check ), true ) !== $data ) { return array( 'error' => 'unavailable' ); } return array( 'url' => orderguard_checkout_url( $data['transaction_id'] ) ); } function orderguard_handle_upgrade() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- request method is only compared with the fixed string 'POST' if ( 'POST' !== ( $_SERVER['REQUEST_METHOD'] ?? '' ) || ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Upgrade request refused.', 'orderguard' ) ); } $nonce = isset( $_POST['_wpnonce'] ) && is_string( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'orderguard_upgrade' ) ) { wp_die( esc_html__( 'Upgrade request refused.', 'orderguard' ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- checked against the monthly/annual allowlist before any server call $interval = isset( $_POST['interval'] ) && is_string( $_POST['interval'] ) ? wp_unslash( $_POST['interval'] ) : ''; $result = orderguard_prepare_upgrade( $interval ); if ( isset( $result['url'] ) ) { // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- fixed HTTPS host/path after ID and ownership verification. wp_redirect( $result['url'], 303 ); exit; } $notice = ( $result['error'] ?? '' ) === 'complimentary' ? 'checkout_comped' : ( ( $result['error'] ?? '' ) === 'other_interval' ? 'checkout_other_interval' : 'checkout_err' ); set_transient( 'orderguard_flag_notice_' . get_current_user_id(), $notice, 120 ); wp_safe_redirect( admin_url( 'admin.php?page=orderguard&tab=plan' ) ); exit; } add_action( 'admin_post_orderguard_upgrade', 'orderguard_handle_upgrade' ); /** * 0.3.1: the Pro prices shown on the Free plan's monthly/yearly choice. * Fixed list prices (Giuliano's decision, 17 Oct 2026; equal to the live Paddle prices checked that day: * monthly 800 EUR, yearly 4600 EUR). The exact amount, including any VAT, is shown at checkout. * Amounts are in the smallest currency unit. */ define( 'ORDERGUARD_PRO_PRICE_CURRENCY', 'EUR' ); define( 'ORDERGUARD_PRO_PRICE_MONTHLY', 800 ); define( 'ORDERGUARD_PRO_PRICE_ANNUAL', 4600 ); /** * One server-reported amount from /v1/me paddle.amounts., or null. * Types are checked BEFORE any conversion: amount = a string of 1-9 digits, or an int 1..999999999; * currency = a string of exactly three capital letters. Anything else (bool, float, array, null, * object, "8.00", " 800") is ignored without a conversion or a warning. */ function orderguard_plan_server_amount( $amounts, $k ) { $a = isset( $amounts[ $k ] ) && is_array( $amounts[ $k ] ) ? $amounts[ $k ] : array(); $am = array_key_exists( 'amount', $a ) ? $a['amount'] : null; $cu = array_key_exists( 'currency', $a ) ? $a['currency'] : null; if ( is_string( $am ) && 1 === preg_match( '/\A[0-9]{1,9}\z/', $am ) ) { $minor = (int) $am; } elseif ( is_int( $am ) && $am >= 1 && $am <= 999999999 ) { $minor = $am; } else { return null; } if ( $minor < 1 || ! is_string( $cu ) || 1 !== preg_match( '/\A[A-Z]{3}\z/', $cu ) ) { return null; } return array( $minor, $cu ); } /** * Display prices for the Free plan's monthly/yearly choice. * A later server may report prices in /v1/me paddle.amounts.{monthly,annual}; they are used only when BOTH are valid and * in the same currency. Otherwise (the current server reports none) both fixed prices above are used, never a mix. * Returns array( 'monthly' => '€8', 'annual' => '€46', 'annual_per_month' => '€3.83', 'saving' => '€50' ); * the last two only when the yearly price is lower than twelve monthly payments. */ function orderguard_plan_prices( $me ) { $amounts = ( is_array( $me ) && isset( $me['paddle']['amounts'] ) && is_array( $me['paddle']['amounts'] ) ) ? $me['paddle']['amounts'] : array(); $m = orderguard_plan_server_amount( $amounts, 'monthly' ); $y = orderguard_plan_server_amount( $amounts, 'annual' ); if ( null === $m || null === $y || $m[1] !== $y[1] ) { $m = array( ORDERGUARD_PRO_PRICE_MONTHLY, ORDERGUARD_PRO_PRICE_CURRENCY ); $y = array( ORDERGUARD_PRO_PRICE_ANNUAL, ORDERGUARD_PRO_PRICE_CURRENCY ); } $out = array( 'monthly' => orderguard_format_price_short( $m[0], $m[1] ), 'annual' => orderguard_format_price_short( $y[0], $y[1] ), ); $saving = $m[0] * 12 - $y[0]; if ( $saving > 0 ) { $out['annual_per_month'] = orderguard_format_price_short( (int) round( $y[0] / 12 ), $y[1] ); $out['saving'] = orderguard_format_price_short( $saving, $y[1] ); } return $out; } /** A price without ",00"/".00" when it is a whole amount (e.g. €8 rather than €8.00). */ function orderguard_format_price_short( $minor, $currency ) { $s = orderguard_format_money( $minor, $currency ); if ( 0 === ( (int) $minor ) % 100 ) { $dp = ( isset( $GLOBALS['wp_locale'] ) && is_object( $GLOBALS['wp_locale'] ) && ! empty( $GLOBALS['wp_locale']->number_format['decimal_point'] ) ) ? $GLOBALS['wp_locale']->number_format['decimal_point'] : '.'; $s = preg_replace( '/' . preg_quote( $dp, '/' ) . '00(?![0-9])/', '', $s, 1 ); } return $s; }