diff --git a/admin/class-informatiq-sp-admin.php b/admin/class-informatiq-sp-admin.php
index b98253cc..062b74e8 100644
--- a/admin/class-informatiq-sp-admin.php
+++ b/admin/class-informatiq-sp-admin.php
@@ -44,9 +44,13 @@ class Informatiq_SP_Admin {
// Register settings.
add_action( 'admin_init', array( $this, 'register_settings' ) );
+ // Handle OAuth callback.
+ add_action( 'admin_init', array( $this, 'handle_oauth_callback' ) );
+
// Handle AJAX requests.
add_action( 'wp_ajax_informatiq_sp_manual_sync', array( $this, 'handle_manual_sync' ) );
add_action( 'wp_ajax_informatiq_sp_test_connection', array( $this, 'handle_test_connection' ) );
+ add_action( 'wp_ajax_informatiq_sp_revoke_auth', array( $this, 'handle_revoke_auth' ) );
// Enqueue admin assets.
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
@@ -72,7 +76,8 @@ class Informatiq_SP_Admin {
public function register_settings() {
// Register settings.
register_setting( 'informatiq_sp_settings', 'informatiq_sp_merchant_id' );
- register_setting( 'informatiq_sp_settings', 'informatiq_sp_service_account' );
+ register_setting( 'informatiq_sp_settings', 'informatiq_sp_client_id' );
+ register_setting( 'informatiq_sp_settings', 'informatiq_sp_client_secret' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_minimum_margin' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_auto_update_enabled' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_update_frequency' );
@@ -109,9 +114,17 @@ class Informatiq_SP_Admin {
);
add_settings_field(
- 'informatiq_sp_service_account',
- __( 'Service Account JSON', 'informatiq-smart-pricing' ),
- array( $this, 'render_service_account_field' ),
+ 'informatiq_sp_oauth_credentials',
+ __( 'OAuth 2.0 Credentials', 'informatiq-smart-pricing' ),
+ array( $this, 'render_oauth_credentials_field' ),
+ 'informatiq_sp_settings',
+ 'informatiq_sp_google_settings'
+ );
+
+ add_settings_field(
+ 'informatiq_sp_authorization',
+ __( 'Google Authorization', 'informatiq-smart-pricing' ),
+ array( $this, 'render_authorization_field' ),
'informatiq_sp_settings',
'informatiq_sp_google_settings'
);
@@ -141,6 +154,108 @@ class Informatiq_SP_Admin {
);
}
+ /**
+ * Handle OAuth callback from Google.
+ */
+ public function handle_oauth_callback() {
+ // Check if this is an OAuth callback.
+ if ( ! isset( $_GET['informatiq_sp_oauth'] ) || $_GET['informatiq_sp_oauth'] !== 'callback' ) {
+ return;
+ }
+
+ // Verify state parameter.
+ $state = isset( $_GET['state'] ) ? sanitize_text_field( $_GET['state'] ) : '';
+ $saved_state = get_transient( 'informatiq_sp_oauth_state' );
+
+ if ( empty( $state ) || $state !== $saved_state ) {
+ add_settings_error(
+ 'informatiq_sp_messages',
+ 'informatiq_sp_oauth_error',
+ __( 'OAuth state mismatch. Please try again.', 'informatiq-smart-pricing' ),
+ 'error'
+ );
+ return;
+ }
+
+ delete_transient( 'informatiq_sp_oauth_state' );
+
+ // Check for errors.
+ if ( isset( $_GET['error'] ) ) {
+ $error = sanitize_text_field( $_GET['error'] );
+ $error_desc = isset( $_GET['error_description'] ) ? sanitize_text_field( $_GET['error_description'] ) : $error;
+ add_settings_error(
+ 'informatiq_sp_messages',
+ 'informatiq_sp_oauth_error',
+ sprintf( __( 'Authorization failed: %s', 'informatiq-smart-pricing' ), $error_desc ),
+ 'error'
+ );
+ return;
+ }
+
+ // Get authorization code.
+ $code = isset( $_GET['code'] ) ? sanitize_text_field( $_GET['code'] ) : '';
+
+ if ( empty( $code ) ) {
+ add_settings_error(
+ 'informatiq_sp_messages',
+ 'informatiq_sp_oauth_error',
+ __( 'No authorization code received.', 'informatiq-smart-pricing' ),
+ 'error'
+ );
+ return;
+ }
+
+ // Exchange code for tokens.
+ $client_id = get_option( 'informatiq_sp_client_id' );
+ $client_secret = get_option( 'informatiq_sp_client_secret' );
+ $redirect_uri = $this->get_oauth_redirect_uri();
+
+ try {
+ $tokens = Informatiq_SP_Google_API::exchange_code_for_tokens(
+ $code,
+ $client_id,
+ $client_secret,
+ $redirect_uri
+ );
+
+ // Save refresh token.
+ update_option( 'informatiq_sp_refresh_token', $tokens['refresh_token'] );
+
+ $this->logger->info( 'Google OAuth authorization successful' );
+
+ // Redirect to settings page with success message.
+ wp_redirect( admin_url( 'admin.php?page=informatiq-smart-pricing&oauth=success' ) );
+ exit;
+
+ } catch ( Exception $e ) {
+ $this->logger->error( 'OAuth token exchange failed: ' . $e->getMessage() );
+ add_settings_error(
+ 'informatiq_sp_messages',
+ 'informatiq_sp_oauth_error',
+ sprintf( __( 'Token exchange failed: %s', 'informatiq-smart-pricing' ), $e->getMessage() ),
+ 'error'
+ );
+ }
+ }
+
+ /**
+ * Get OAuth redirect URI.
+ *
+ * @return string Redirect URI.
+ */
+ private function get_oauth_redirect_uri() {
+ return admin_url( 'admin.php?page=informatiq-smart-pricing&informatiq_sp_oauth=callback' );
+ }
+
+ /**
+ * Check if plugin is authorized.
+ *
+ * @return bool True if authorized.
+ */
+ private function is_authorized() {
+ return ! empty( get_option( 'informatiq_sp_refresh_token' ) );
+ }
+
/**
* Render admin page.
*/
@@ -149,6 +264,16 @@ class Informatiq_SP_Admin {
return;
}
+ // Check for OAuth success message.
+ if ( isset( $_GET['oauth'] ) && $_GET['oauth'] === 'success' ) {
+ add_settings_error(
+ 'informatiq_sp_messages',
+ 'informatiq_sp_message',
+ __( 'Google authorization successful!', 'informatiq-smart-pricing' ),
+ 'success'
+ );
+ }
+
// Check if form was submitted.
if ( isset( $_GET['settings-updated'] ) ) {
// Update cron schedule if frequency changed.
@@ -185,14 +310,20 @@ class Informatiq_SP_Admin {
-
+ is_authorized() ) : ?>
+
+
+
+
+
@@ -217,7 +348,7 @@ class Informatiq_SP_Admin {
* Render Google settings section description.
*/
public function render_google_settings_section() {
- echo '' . esc_html__( 'Configure your Google Merchant Center API credentials.', 'informatiq-smart-pricing' ) . '
';
+ echo '' . esc_html__( 'Configure your Google Merchant Center API credentials using OAuth 2.0.', 'informatiq-smart-pricing' ) . '
';
}
/**
@@ -243,25 +374,100 @@ class Informatiq_SP_Admin {
-
+
-
-
-
-
+
is_authorized();
+ $has_credentials = ! empty( $client_id ) && ! empty( $client_secret );
+
+ if ( $is_authorized ) {
+ ?>
+
+ get_oauth_redirect_uri(),
+ $state
+ );
+ ?>
+
+
+
+
+
+
+
@@ -461,6 +668,10 @@ class Informatiq_SP_Admin {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
+ if ( ! $this->is_authorized() ) {
+ wp_send_json_error( array( 'message' => __( 'Please authorize with Google first.', 'informatiq-smart-pricing' ) ) );
+ }
+
try {
$scheduler = new Informatiq_SP_Scheduler( $this->price_updater );
$results = $scheduler->trigger_manual_update();
@@ -491,16 +702,28 @@ class Informatiq_SP_Admin {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
- $merchant_id = get_option( 'informatiq_sp_merchant_id' );
- $service_account = get_option( 'informatiq_sp_service_account' );
+ $merchant_id = get_option( 'informatiq_sp_merchant_id' );
+ $client_id = get_option( 'informatiq_sp_client_id' );
+ $client_secret = get_option( 'informatiq_sp_client_secret' );
+ $refresh_token = get_option( 'informatiq_sp_refresh_token' );
- if ( empty( $merchant_id ) || empty( $service_account ) ) {
- wp_send_json_error( array( 'message' => __( 'Please configure Google Merchant settings first.', 'informatiq-smart-pricing' ) ) );
+ if ( empty( $merchant_id ) ) {
+ wp_send_json_error( array( 'message' => __( 'Please enter your Merchant ID.', 'informatiq-smart-pricing' ) ) );
+ }
+
+ if ( empty( $refresh_token ) ) {
+ wp_send_json_error( array( 'message' => __( 'Please authorize with Google first.', 'informatiq-smart-pricing' ) ) );
}
try {
- $google_api = new Informatiq_SP_Google_API( $merchant_id, $service_account, $this->logger );
- $success = $google_api->test_connection();
+ $google_api = new Informatiq_SP_Google_API(
+ $merchant_id,
+ $client_id,
+ $client_secret,
+ $refresh_token,
+ $this->logger
+ );
+ $success = $google_api->test_connection();
if ( $success ) {
wp_send_json_success( array( 'message' => __( 'Connection successful!', 'informatiq-smart-pricing' ) ) );
@@ -512,6 +735,23 @@ class Informatiq_SP_Admin {
}
}
+ /**
+ * Handle revoke authorization AJAX request.
+ */
+ public function handle_revoke_auth() {
+ check_ajax_referer( 'informatiq_sp_admin', 'nonce' );
+
+ if ( ! current_user_can( 'manage_woocommerce' ) ) {
+ wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
+ }
+
+ delete_option( 'informatiq_sp_refresh_token' );
+
+ $this->logger->info( 'Google OAuth authorization revoked' );
+
+ wp_send_json_success( array( 'message' => __( 'Authorization revoked.', 'informatiq-smart-pricing' ) ) );
+ }
+
/**
* Enqueue admin assets.
*
@@ -544,8 +784,10 @@ class Informatiq_SP_Admin {
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'informatiq_sp_admin' ),
'strings' => array(
- 'syncInProgress' => __( 'Sync in progress...', 'informatiq-smart-pricing' ),
- 'testInProgress' => __( 'Testing connection...', 'informatiq-smart-pricing' ),
+ 'syncInProgress' => __( 'Sync in progress...', 'informatiq-smart-pricing' ),
+ 'testInProgress' => __( 'Testing connection...', 'informatiq-smart-pricing' ),
+ 'revokeConfirm' => __( 'Are you sure you want to revoke Google authorization?', 'informatiq-smart-pricing' ),
+ 'revokeInProgress' => __( 'Revoking...', 'informatiq-smart-pricing' ),
),
)
);
diff --git a/assets/js/admin.js b/assets/js/admin.js
index e13ab619..26e4580d 100644
--- a/assets/js/admin.js
+++ b/assets/js/admin.js
@@ -19,6 +19,7 @@
bindEvents: function() {
$('#informatiq-sp-manual-sync').on('click', this.handleManualSync);
$('#informatiq-sp-test-connection').on('click', this.handleTestConnection);
+ $('#informatiq-sp-revoke-auth').on('click', this.handleRevokeAuth);
},
/**
@@ -138,6 +139,46 @@
}, 5000);
}
});
+ },
+
+ /**
+ * Handle revoke authorization button click
+ */
+ handleRevokeAuth: function(e) {
+ e.preventDefault();
+
+ var $button = $(this);
+
+ // Confirm action
+ if (!confirm(informatiqSP.strings.revokeConfirm || 'Are you sure you want to revoke Google authorization?')) {
+ return;
+ }
+
+ // Disable button and show loading state
+ $button.prop('disabled', true).text(informatiqSP.strings.revokeInProgress || 'Revoking...');
+
+ // Make AJAX request
+ $.ajax({
+ url: informatiqSP.ajaxUrl,
+ type: 'POST',
+ data: {
+ action: 'informatiq_sp_revoke_auth',
+ nonce: informatiqSP.nonce
+ },
+ success: function(response) {
+ if (response.success) {
+ // Reload page to show updated status
+ location.reload();
+ } else {
+ alert('Error: ' + (response.data.message || 'Unknown error'));
+ $button.prop('disabled', false).text('Revoke Authorization');
+ }
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ alert('Error: ' + errorThrown);
+ $button.prop('disabled', false).text('Revoke Authorization');
+ }
+ });
}
};
diff --git a/includes/class-informatiq-sp-google-api.php b/includes/class-informatiq-sp-google-api.php
index 137acd65..05f7c236 100644
--- a/includes/class-informatiq-sp-google-api.php
+++ b/includes/class-informatiq-sp-google-api.php
@@ -1,9 +1,9 @@
merchant_id = $merchant_id;
- $this->logger = $logger;
+ public function __construct( $merchant_id, $client_id, $client_secret, $refresh_token, $logger ) {
+ $this->merchant_id = $merchant_id;
+ $this->client_id = $client_id;
+ $this->client_secret = $client_secret;
+ $this->refresh_token = $refresh_token;
+ $this->logger = $logger;
- // Parse service account JSON.
- $this->credentials = json_decode( $service_account, true );
-
- if ( json_last_error() !== JSON_ERROR_NONE ) {
- throw new Exception( 'Invalid service account JSON: ' . json_last_error_msg() );
+ if ( empty( $client_id ) || empty( $client_secret ) ) {
+ throw new Exception( 'OAuth Client ID and Client Secret are required' );
}
- if ( empty( $this->credentials['client_email'] ) || empty( $this->credentials['private_key'] ) ) {
- throw new Exception( 'Service account JSON must contain client_email and private_key' );
+ if ( empty( $refresh_token ) ) {
+ throw new Exception( 'Not authorized. Please authorize with Google first.' );
}
}
+ /**
+ * Get the authorization URL for OAuth flow.
+ *
+ * @param string $client_id OAuth Client ID.
+ * @param string $redirect_uri Redirect URI after authorization.
+ * @param string $state State parameter for CSRF protection.
+ * @return string Authorization URL.
+ */
+ public static function get_authorization_url( $client_id, $redirect_uri, $state ) {
+ $params = array(
+ 'client_id' => $client_id,
+ 'redirect_uri' => $redirect_uri,
+ 'response_type' => 'code',
+ 'scope' => self::API_SCOPE,
+ 'access_type' => 'offline',
+ 'prompt' => 'consent',
+ 'state' => $state,
+ );
+
+ return self::AUTH_URL . '?' . http_build_query( $params );
+ }
+
+ /**
+ * Exchange authorization code for tokens.
+ *
+ * @param string $code Authorization code from Google.
+ * @param string $client_id OAuth Client ID.
+ * @param string $client_secret OAuth Client Secret.
+ * @param string $redirect_uri Redirect URI used in authorization.
+ * @return array Tokens array with access_token and refresh_token.
+ * @throws Exception If token exchange fails.
+ */
+ public static function exchange_code_for_tokens( $code, $client_id, $client_secret, $redirect_uri ) {
+ $response = wp_remote_post(
+ self::TOKEN_URL,
+ array(
+ 'body' => array(
+ 'code' => $code,
+ 'client_id' => $client_id,
+ 'client_secret' => $client_secret,
+ 'redirect_uri' => $redirect_uri,
+ 'grant_type' => 'authorization_code',
+ ),
+ 'timeout' => 30,
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ throw new Exception( 'Token exchange failed: ' . $response->get_error_message() );
+ }
+
+ $body = json_decode( wp_remote_retrieve_body( $response ), true );
+
+ if ( isset( $body['error'] ) ) {
+ throw new Exception( 'Token error: ' . ( $body['error_description'] ?? $body['error'] ) );
+ }
+
+ if ( empty( $body['access_token'] ) || empty( $body['refresh_token'] ) ) {
+ throw new Exception( 'Invalid token response from Google' );
+ }
+
+ return array(
+ 'access_token' => $body['access_token'],
+ 'refresh_token' => $body['refresh_token'],
+ 'expires_in' => $body['expires_in'] ?? 3600,
+ );
+ }
+
/**
* Get access token, refreshing if necessary.
*
@@ -104,33 +188,38 @@ class Informatiq_SP_Google_API {
* @throws Exception If token retrieval fails.
*/
private function get_access_token() {
- // Return cached token if still valid (with 60 second buffer).
- if ( $this->access_token && $this->token_expires > ( time() + 60 ) ) {
+ // Return cached token if available.
+ if ( $this->access_token ) {
return $this->access_token;
}
- // Create JWT for token exchange.
- $jwt = $this->create_jwt();
-
- // Exchange JWT for access token.
+ // Refresh the access token.
$response = wp_remote_post(
self::TOKEN_URL,
array(
'body' => array(
- 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
- 'assertion' => $jwt,
+ 'client_id' => $this->client_id,
+ 'client_secret' => $this->client_secret,
+ 'refresh_token' => $this->refresh_token,
+ 'grant_type' => 'refresh_token',
),
'timeout' => 30,
)
);
if ( is_wp_error( $response ) ) {
- throw new Exception( 'Token request failed: ' . $response->get_error_message() );
+ throw new Exception( 'Token refresh failed: ' . $response->get_error_message() );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $body['error'] ) ) {
+ // If refresh token is invalid, user needs to re-authorize.
+ if ( $body['error'] === 'invalid_grant' ) {
+ // Clear the stored refresh token.
+ delete_option( 'informatiq_sp_refresh_token' );
+ throw new Exception( 'Authorization expired. Please re-authorize with Google.' );
+ }
throw new Exception( 'Token error: ' . ( $body['error_description'] ?? $body['error'] ) );
}
@@ -138,71 +227,11 @@ class Informatiq_SP_Google_API {
throw new Exception( 'No access token in response' );
}
- $this->access_token = $body['access_token'];
- $this->token_expires = time() + ( $body['expires_in'] ?? 3600 );
+ $this->access_token = $body['access_token'];
return $this->access_token;
}
- /**
- * Create a signed JWT for authentication.
- *
- * @return string Signed JWT.
- * @throws Exception If JWT creation fails.
- */
- private function create_jwt() {
- $now = time();
-
- // JWT Header.
- $header = array(
- 'alg' => 'RS256',
- 'typ' => 'JWT',
- );
-
- // JWT Claims.
- $claims = array(
- 'iss' => $this->credentials['client_email'],
- 'scope' => self::API_SCOPE,
- 'aud' => self::TOKEN_URL,
- 'iat' => $now,
- 'exp' => $now + 3600,
- );
-
- // Encode header and claims.
- $header_encoded = $this->base64url_encode( wp_json_encode( $header ) );
- $claims_encoded = $this->base64url_encode( wp_json_encode( $claims ) );
-
- // Create signature input.
- $signature_input = $header_encoded . '.' . $claims_encoded;
-
- // Sign with private key.
- $private_key = openssl_pkey_get_private( $this->credentials['private_key'] );
-
- if ( ! $private_key ) {
- throw new Exception( 'Invalid private key in service account' );
- }
-
- $signature = '';
- $success = openssl_sign( $signature_input, $signature, $private_key, OPENSSL_ALGO_SHA256 );
-
- if ( ! $success ) {
- throw new Exception( 'Failed to sign JWT' );
- }
-
- // Return complete JWT.
- return $signature_input . '.' . $this->base64url_encode( $signature );
- }
-
- /**
- * Base64url encode (URL-safe base64).
- *
- * @param string $data Data to encode.
- * @return string Encoded data.
- */
- private function base64url_encode( $data ) {
- return rtrim( strtr( base64_encode( $data ), '+/', '-_' ), '=' );
- }
-
/**
* Make an API request to the Merchant API.
*
@@ -411,8 +440,8 @@ class Informatiq_SP_Google_API {
$endpoint .= '&pageToken=' . urlencode( $page_token );
}
- $response = $this->api_request( 'GET', $endpoint );
- $products = $response['products'] ?? array();
+ $response = $this->api_request( 'GET', $endpoint );
+ $products = $response['products'] ?? array();
$all_products = array_merge( $all_products, $products );
$page_token = $response['nextPageToken'] ?? null;
diff --git a/includes/class-informatiq-sp-price-updater.php b/includes/class-informatiq-sp-price-updater.php
index eecf6822..7d221d50 100644
--- a/includes/class-informatiq-sp-price-updater.php
+++ b/includes/class-informatiq-sp-price-updater.php
@@ -58,19 +58,21 @@ class Informatiq_SP_Price_Updater {
);
// Get plugin settings.
- $merchant_id = get_option( 'informatiq_sp_merchant_id' );
- $service_account = get_option( 'informatiq_sp_service_account' );
- $minimum_margin = (float) get_option( 'informatiq_sp_minimum_margin', 10 );
+ $merchant_id = get_option( 'informatiq_sp_merchant_id' );
+ $client_id = get_option( 'informatiq_sp_client_id' );
+ $client_secret = get_option( 'informatiq_sp_client_secret' );
+ $refresh_token = get_option( 'informatiq_sp_refresh_token' );
+ $minimum_margin = (float) get_option( 'informatiq_sp_minimum_margin', 10 );
// Validate settings.
- if ( empty( $merchant_id ) || empty( $service_account ) ) {
- $this->logger->error( 'Google Merchant settings not configured. Please configure settings first.' );
+ if ( empty( $merchant_id ) || empty( $refresh_token ) ) {
+ $this->logger->error( 'Google Merchant settings not configured. Please configure and authorize first.' );
return $results;
}
// Initialize Google API.
try {
- $google_api = new Informatiq_SP_Google_API( $merchant_id, $service_account, $this->logger );
+ $google_api = new Informatiq_SP_Google_API( $merchant_id, $client_id, $client_secret, $refresh_token, $this->logger );
} catch ( Exception $e ) {
$this->logger->error( 'Failed to initialize Google API: ' . $e->getMessage() );
return $results;
@@ -362,19 +364,21 @@ class Informatiq_SP_Price_Updater {
}
// Get settings.
- $merchant_id = get_option( 'informatiq_sp_merchant_id' );
- $service_account = get_option( 'informatiq_sp_service_account' );
- $minimum_margin = (float) get_option( 'informatiq_sp_minimum_margin', 10 );
+ $merchant_id = get_option( 'informatiq_sp_merchant_id' );
+ $client_id = get_option( 'informatiq_sp_client_id' );
+ $client_secret = get_option( 'informatiq_sp_client_secret' );
+ $refresh_token = get_option( 'informatiq_sp_refresh_token' );
+ $minimum_margin = (float) get_option( 'informatiq_sp_minimum_margin', 10 );
- if ( empty( $merchant_id ) || empty( $service_account ) ) {
+ if ( empty( $merchant_id ) || empty( $refresh_token ) ) {
return array(
'success' => false,
- 'message' => 'Google Merchant settings not configured',
+ 'message' => 'Google Merchant settings not configured or not authorized',
);
}
try {
- $google_api = new Informatiq_SP_Google_API( $merchant_id, $service_account, $this->logger );
+ $google_api = new Informatiq_SP_Google_API( $merchant_id, $client_id, $client_secret, $refresh_token, $this->logger );
$updated = $this->process_single_product( $product, $google_api, $minimum_margin );
return array(