diff --git a/includes/manager.php b/includes/manager.php index cc781854..247b9b20 100644 --- a/includes/manager.php +++ b/includes/manager.php @@ -17,6 +17,7 @@ final class Manager { public static function get_module_list(): array { return [ 'Legacy', + 'Connect', 'Settings', ]; } diff --git a/modules/connect/classes/basic-enum.php b/modules/connect/classes/basic-enum.php new file mode 100644 index 00000000..bf5c1aa9 --- /dev/null +++ b/modules/connect/classes/basic-enum.php @@ -0,0 +1,35 @@ +getConstants(); + } + + return self::$entries[ $caller ]; + } +} diff --git a/modules/connect/classes/config.php b/modules/connect/classes/config.php new file mode 100644 index 00000000..e1f565ba --- /dev/null +++ b/modules/connect/classes/config.php @@ -0,0 +1,22 @@ +get_col( + $wpdb->prepare( + "SELECT option_value + FROM $wpdb->options + WHERE option_name = %s + AND %s = %s + LIMIT 1", + $option_name, + $cache_buster, + $cache_buster + ) + ); + if ( ! empty( $option ) ) { + return $option[0]; + } + return $default; + } + + /** + * insert_option_uniquely + * + * used to insert option if not there already + * direct query to avoid cache and race condition issues + * + * @param $option_name + * @param $option_value + * + * @return bool + */ + public static function insert_option_uniquely( $option_name, $option_value ): bool { + global $wpdb; + if ( ! self::is_option_whitelisted_for_direct_access( $option_name ) ) { + return false; + } + $cache_buster = wp_generate_uuid4(); + $result = $wpdb->query( + $wpdb->prepare( + "INSERT INTO $wpdb->options (option_name, option_value, autoload) + SELECT * FROM (SELECT %s, %s, 'no') AS tmp + WHERE NOT EXISTS ( + SELECT option_name + FROM $wpdb->options + WHERE option_name = %s + AND option_value = %s + AND %s = %s + ) LIMIT 1", + $option_name, + $option_value, + $option_name, + $option_value, + $cache_buster, + $cache_buster + ) + ); + + if ( false === $result || 0 === $result ) { + // false means query failed, 0 means no row inserted because it exists + return false; + } + + return true; + } + + /** + * is_option_whitelisted_for_direct_access + * allowed only list of option names + * + * @param string $option_name + * + * @return boolean + */ + public static function is_option_whitelisted_for_direct_access( string $option_name ): bool { + $options_whitelist = [ + Config::APP_NAME . Service::REFRESH_TOKEN_LOCK, + ]; + return in_array( $option_name, $options_whitelist, true ); + } + + /** + * User is subscription owner. + * + * Check if current user is subscription owner. + * + * @return boolean + */ + public static function user_is_subscription_owner(): bool { + $owner_id = (int) self::get_connect_mode_data( self::OPTION_OWNER_USER_ID, false ); + + return get_current_user_id() === $owner_id; + } + + /** + * clear_session + */ + public static function clear_session( $with_client = false ) { + if ( Config::CONNECT_MODE === 'site' ) { + if ( $with_client ) { + self::delete_option( self::CLIENT_ID ); + self::delete_option( self::CLIENT_SECRET ); + } + self::delete_option( self::ACCESS_TOKEN ); + self::delete_option( self::REFRESH_TOKEN ); + self::delete_option( self::TOKEN_ID ); + self::delete_option( self::SUBSCRIPTION_ID ); + self::delete_option( self::OPTION_OWNER_USER_ID ); + self::delete_option( self::HOME_URL ); + } else { + $user_id = get_current_user_id(); + if ( $with_client ) { + self::delete_user_data( $user_id, self::CLIENT_ID ); + self::delete_user_data( $user_id, self::CLIENT_SECRET ); + } + self::delete_user_data( $user_id, self::ACCESS_TOKEN ); + self::delete_user_data( $user_id, self::REFRESH_TOKEN ); + self::delete_user_data( $user_id, self::TOKEN_ID ); + self::delete_user_data( $user_id, self::SUBSCRIPTION_ID ); + self::delete_user_data( $user_id, self::OPTION_OWNER_USER_ID ); + self::delete_user_data( $user_id, self::HOME_URL ); + } + } +} diff --git a/modules/connect/classes/exceptions/service-exception.php b/modules/connect/classes/exceptions/service-exception.php new file mode 100644 index 00000000..ad0af388 --- /dev/null +++ b/modules/connect/classes/exceptions/service-exception.php @@ -0,0 +1,13 @@ +get_path(); + } + + public function get_path(): string { + return $this->path; + } + + public function get_name(): string { + return ''; + } + + public function post_permission_callback( \WP_REST_Request $request ): bool { + return $this->get_permission_callback( $request ); + } + + public function get_permission_callback( \WP_REST_Request $request ): bool { + $valid = $this->permission_callback( $request ); + + return $valid && user_can( $this->current_user_id, 'manage_options' ); + } +} diff --git a/modules/connect/classes/service.php b/modules/connect/classes/service.php new file mode 100644 index 00000000..88180c45 --- /dev/null +++ b/modules/connect/classes/service.php @@ -0,0 +1,320 @@ + 'POST', + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => wp_json_encode([ + 'redirect_uri' => Utils::get_redirect_uri(), + 'app_type' => Config::APP_TYPE, + ]), + ], 201 ); + + $client_id = $client_data['client_id'] ?? null; + $client_secret = $client_data['client_secret'] ?? null; + + Data::set_client_id( $client_id ); + Data::set_client_secret( $client_secret ); + Data::set_home_url(); + + return $client_id; + } + + /** + * Deactivate license + * + * @return void + * @throws Service_Exception + */ + public static function deactivate_license(): void { + $client_id = Data::get_client_id(); + + if ( ! $client_id ) { + throw new Service_Exception( 'Missing client ID' ); + } + + $deactivation_url = Utils::get_deactivation_url( $client_id ); + + if ( ! $deactivation_url ) { + throw new Service_Exception( 'Missing deactivation URL' ); + } + + $access_token = Data::get_access_token(); + + if ( ! $access_token ) { + throw new Service_Exception( 'Missing access token' ); + } + + $refresh_token = Data::get_refresh_token(); + + if ( ! $refresh_token ) { + throw new Service_Exception( 'Missing refresh token' ); + } + + self::request($deactivation_url, [ + 'method' => 'DELETE', + 'headers' => [ + 'Authorization' => "Bearer {$access_token}", + ], + ], 204); + + self::get_token( 'refresh_token', $refresh_token ); + } + + /** + * disconnect + * + * @return void + * @throws Service_Exception + */ + public static function disconnect(): void { + $sessions_url = Utils::get_sessions_url(); + + if ( ! $sessions_url ) { + throw new Service_Exception( 'Missing sessions URL' ); + } + + $access_token = Data::get_access_token(); + + if ( ! $access_token ) { + throw new Service_Exception( 'Missing access token' ); + } + + self::request( $sessions_url, [ + 'method' => 'DELETE', + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer {$access_token}", + ], + ], 204 ); + + Data::clear_session(); + } + + /** + * disconnect + * + * @return void + * @throws Service_Exception + */ + public static function reconnect(): void { + $sessions_url = Utils::get_sessions_url(); + + if ( ! $sessions_url ) { + throw new Service_Exception( 'Missing sessions URL' ); + } + + Data::clear_session(); + } + + /** + * Get token & optionally save to user + * + * @param string $grant_type + * + * @param string|null $credential + * @param bool|null $update + * + * @return array + * @throws Service_Exception + */ + public static function get_token( string $grant_type, ?string $credential = null, ?bool $update = true ): array { + $token_url = Utils::get_token_url(); + + if ( ! $token_url ) { + throw new Service_Exception( 'Missing token URL' ); + } + + $client_id = Data::get_client_id(); + $client_secret = Data::get_client_secret(); + + if ( empty( $client_id ) || empty( $client_secret ) ) { + throw new Service_Exception( 'Missing client ID or secret' ); + } + + $body = [ + 'grant_type' => $grant_type, + 'redirect_uri' => Utils::get_redirect_uri(), + ]; + + switch ( $grant_type ) { + case GrantTypes::AUTHORIZATION_CODE: + $body['code'] = $credential; + break; + case GrantTypes::REFRESH_TOKEN: + $body[ GrantTypes::REFRESH_TOKEN ] = $credential; + break; + case GrantTypes::CLIENT_CREDENTIALS: + $body['redirect_uri'] = Utils::get_redirect_uri( Data::get_home_url() ); + + break; + default: + throw new Service_Exception( 'Invalid grant type' ); + } + + $data = self::request( $token_url, [ + 'method' => 'POST', + 'headers' => [ + 'x-elementor-apps' => Config::APP_NAME, + 'Authorization' => 'Basic ' . base64_encode( "{$client_id}:{$client_secret}" ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + ], + 'body' => $body, + ] ); + + if ( $update ) { + Data::set_connect_mode_data( Data::TOKEN_ID, $data['id_token'] ?? null ); + Data::set_connect_mode_data( Data::ACCESS_TOKEN, $data['access_token'] ?? null ); + Data::set_connect_mode_data( Data::REFRESH_TOKEN, $data['refresh_token'] ?? null ); + Data::set_connect_mode_data( Data::OPTION_OWNER_USER_ID, get_current_user_id() ?? null ); + } + + return $data; + } + + public static function jwt_decode( $payload ): string { + static $jwks = null; + + $jwks_url = Utils::get_jwks_url(); + if ( ! $jwks_url ) { + return __( 'Missing JWKS URL', 'pojo-accessibility' ); + } + + if ( ! $jwks ) { + $jwks = self::request($jwks_url, [ + 'method' => 'GET', + ]); + } + if ( ! class_exists( 'JWT' ) ) { + require_once EA11Y_PATH . 'vendor/autoload.php'; + if ( ! class_exists( 'JWT' ) ) { + return __( 'JWT class not found', 'pojo-accessibility' ); + } + } + + try { + $decoded = \Firebase\JWT\JWT::decode( $payload, \Firebase\JWT\JWK::parseKeySet( $jwks ) ); + return wp_json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); + } catch ( \Throwable $th ) { + if ( $th instanceof \Firebase\JWT\ExpiredException ) { + self::get_token( GrantTypes::REFRESH_TOKEN, Data::get_refresh_token() ); + return self::jwt_decode( $payload ); + } + return $th->getMessage(); + } + } + + /** + * @param string $url + * @param array $args + * @param int $valid_response_code + * + * @return array|null + * @throws Service_Exception + */ + public static function request( string $url, array $args, int $valid_response_code = 200 ): ?array { + $args['timeout'] = 30; + + $response = wp_remote_request( $url, $args ); + + if ( is_wp_error( $response ) ) { + Logger::error( $response->get_error_message() ); + + throw new Service_Exception( esc_html( $response->get_error_message() ) ); + } + + if ( wp_remote_retrieve_response_code( $response ) !== $valid_response_code ) { + Logger::error( 'Invalid status code ' . wp_remote_retrieve_response_code( $response ) ); + + throw new Service_Exception( esc_html( wp_remote_retrieve_body( $response ) ) ); + } + + return json_decode( wp_remote_retrieve_body( $response ), true ); + } + + /** + * @throws Service_Exception + */ + public static function refresh_token() { + $lock_key = Config::APP_NAME . self::REFRESH_TOKEN_LOCK; + $last_token = Data::fetch_option( $lock_key, '' ); + + $current_refresh_token = Data::get_refresh_token(); + + if ( ! empty( $last_token ) && $last_token === $current_refresh_token ) { + sleep( 1 ); + return; + } + + delete_option( $lock_key ); + $locked = Data::insert_option_uniquely( $lock_key, $current_refresh_token ); + if ( ! $locked ) { + sleep( 1 ); + return; + } + + self::get_token( GrantTypes::REFRESH_TOKEN, $current_refresh_token ); + } + + /** + * @throws Service_Exception + */ + public static function update_redirect_uri(): void { + $client_id = Data::get_client_id(); + + if ( ! $client_id ) { + throw new Service_Exception( 'Missing client ID' ); + } + + $client_patch_url = Utils::get_clients_patch_url( $client_id ); + + [ 'access_token' => $access_token ] = self::get_token( + GrantTypes::CLIENT_CREDENTIALS, + null, + false + ); + + self::request( $client_patch_url, [ + 'method' => 'PATCH', + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer {$access_token}", + ], + 'body' => wp_json_encode( [ + 'redirect_uri' => Utils::get_redirect_uri(), + ] ), + ] ); + + self::refresh_token(); + + Data::set_home_url(); + } +} diff --git a/modules/connect/classes/utils.php b/modules/connect/classes/utils.php new file mode 100644 index 00000000..9edfc198 --- /dev/null +++ b/modules/connect/classes/utils.php @@ -0,0 +1,125 @@ + $client_id, + 'redirect_uri' => rawurlencode( self::get_redirect_uri() ), + 'response_type' => 'code', + 'scope' => Config::SCOPES, + 'state' => wp_create_nonce( Config::STATE_NONCE ), + ], self::get_auth_url() ); + } + + /** + * get_deactivation_url + * @param string $client_id + * + * @return string + */ + public static function get_deactivation_url( string $client_id ): string { + return self::get_base_url() . "/api/v1/clients/{$client_id}/activation"; + } + + public static function get_jwks_url(): string { + return self::get_base_url() . '/v1/.well-known/jwks.json'; + } + + /** + * get_sessions_url + * @return string + */ + public static function get_sessions_url(): string { + return self::get_base_url() . '/api/v1/session'; + } + + public static function get_token_url(): string { + return self::get_base_url() . '/api/v1/oauth2/token'; + } + + /** + * Get clients URL + * + * @param string $client_id + * + * @return string + */ + public static function get_clients_patch_url( string $client_id ): string { + return Utils::get_base_url() . "/api/v1/clients/{$client_id}"; + } + + /** + * get_base_url + * @return string + */ + public static function get_base_url(): string { + return apply_filters( 'ea11y_connect_get_base_url', Config::BASE_URL ); + } + + /** + * is_valid_home_url + * @return bool + */ + public static function is_valid_home_url(): bool { + static $valid = null; + + if ( null === $valid ) { + if ( empty( Data::get_home_url() ) ) { + $valid = true; + } else { + $valid = Data::get_home_url() === home_url(); + } + } + + return $valid; + } +} diff --git a/modules/connect/components/handler.php b/modules/connect/components/handler.php new file mode 100644 index 00000000..5eee71d2 --- /dev/null +++ b/modules/connect/components/handler.php @@ -0,0 +1,91 @@ +should_handle_auth_code() ) { + return; + } + + $code = sanitize_text_field( $_GET['code'] ); + $state = sanitize_text_field( $_GET['state'] ); + + // Check if the state is valid + $this->validate_nonce( $state ); + + try { + // Exchange the code for an access token and store it + Service::get_token( GrantTypes::AUTHORIZATION_CODE, $code ); // Makes sure we won't stick in the mismatch limbo + + Data::set_home_url(); + + do_action( 'on_connect_' . Config::APP_PREFIX . '_connected' ); // Redirect to the redirect URI + } catch ( Throwable $t ) { + Logger::error( 'Unable to handle auth code: ' . $t->getMessage() ); + } + + wp_redirect( Utils::get_redirect_uri() ); + + exit; + } + + /** + * Handler constructor. + */ + public function __construct() { + add_action( 'admin_init', [ $this, 'handle_auth_code' ] ); + } +} diff --git a/modules/connect/module.php b/modules/connect/module.php new file mode 100644 index 00000000..3c38df37 --- /dev/null +++ b/modules/connect/module.php @@ -0,0 +1,62 @@ +register_components(); + $this->register_routes(); + } +} + diff --git a/modules/connect/rest/authorize.php b/modules/connect/rest/authorize.php new file mode 100644 index 00000000..32b68998 --- /dev/null +++ b/modules/connect/rest/authorize.php @@ -0,0 +1,78 @@ +verify_nonce_and_capability( + $request->get_param( self::NONCE_NAME ), + self::NONCE_NAME + ); + + if ( Connect::is_connected() && Utils::is_valid_home_url() ) { + return $this->respond_error_json( [ + 'message' => esc_html__( 'You are already connected', 'pojo-accessibility' ), + 'code' => 'forbidden', + ] ); + } + + try { + $client_id = Data::get_client_id(); + + if ( ! $client_id ) { + $client_id = Service::register_client(); + } + + if ( ! Utils::is_valid_home_url() ) { + if ( $request->get_param( 'update_redirect_uri' ) ) { + Service::update_redirect_uri(); + } else { + return $this->respond_error_json( [ + 'message' => esc_html__( 'Connected domain mismatch', 'pojo-accessibility' ), + 'code' => 'forbidden', + ] ); + } + } + + $authorize_url = Utils::get_authorize_url( $client_id ); + + $authorize_url = apply_filters( 'ea11y_connect_authorize_url', $authorize_url ); + + return $this->respond_success_json( $authorize_url ); + } catch ( Throwable $t ) { + return $this->respond_error_json( [ + 'message' => $t->getMessage(), + 'code' => 'internal_server_error', + ] ); + } + } +} diff --git a/modules/connect/rest/deactivate-and-disconnect.php b/modules/connect/rest/deactivate-and-disconnect.php new file mode 100644 index 00000000..4b7dc87c --- /dev/null +++ b/modules/connect/rest/deactivate-and-disconnect.php @@ -0,0 +1,50 @@ +get_param( 'clear_session' ) ) { + Data::clear_session( true ); + return $this->respond_success_json(); + } + + Service::deactivate_license(); + Service::disconnect(); + + return $this->respond_success_json(); + } catch ( Throwable $t ) { + return $this->respond_error_json( [ + 'message' => $t->getMessage(), + 'code' => 'internal_server_error', + ] ); + } + } +} diff --git a/modules/connect/rest/deactivate.php b/modules/connect/rest/deactivate.php new file mode 100644 index 00000000..01c9aecc --- /dev/null +++ b/modules/connect/rest/deactivate.php @@ -0,0 +1,42 @@ +respond_success_json(); + } catch ( Throwable $t ) { + return $this->respond_error_json( [ + 'message' => $t->getMessage(), + 'code' => 'internal_server_error', + ] ); + } + } +} diff --git a/modules/connect/rest/disconnect.php b/modules/connect/rest/disconnect.php new file mode 100644 index 00000000..303c4779 --- /dev/null +++ b/modules/connect/rest/disconnect.php @@ -0,0 +1,42 @@ +respond_success_json(); + } catch ( Throwable $t ) { + return $this->respond_error_json( [ + 'message' => $t->getMessage(), + 'code' => 'internal_server_error', + ] ); + } + } +} diff --git a/modules/connect/rest/reconnect.php b/modules/connect/rest/reconnect.php new file mode 100644 index 00000000..0c43431b --- /dev/null +++ b/modules/connect/rest/reconnect.php @@ -0,0 +1,41 @@ +respond_success_json(); + } catch ( Throwable $t ) { + return $this->respond_error_json( [ + 'message' => $t->getMessage(), + 'code' => 'internal_server_error', + ] ); + } + } +} diff --git a/modules/connect/rest/switch-domain.php b/modules/connect/rest/switch-domain.php new file mode 100644 index 00000000..d4b54c0c --- /dev/null +++ b/modules/connect/rest/switch-domain.php @@ -0,0 +1,59 @@ +verify_nonce_and_capability( + $request->get_param( self::NONCE_NAME ), + self::NONCE_NAME + ); + + try { + $client_id = Data::get_client_id(); + + if ( ! $client_id ) { + return $this->respond_error_json( [ + 'message' => __( 'Client ID not found', 'pojo-accessibility' ), + 'code' => 'ignore_error', + ] ); + } + + Service::update_redirect_uri(); + + return $this->respond_success_json( [ 'message' => __( 'Domain updated!', 'pojo-accessibility' ) ] ); + } catch ( Throwable $t ) { + return $this->respond_error_json( [ + 'message' => $t->getMessage(), + 'code' => 'internal_server_error', + ] ); + } + } +} diff --git a/modules/settings/assets/js/admin.js b/modules/settings/assets/js/admin.js index 4e51e3ad..a40129e3 100644 --- a/modules/settings/assets/js/admin.js +++ b/modules/settings/assets/js/admin.js @@ -2,6 +2,8 @@ import { ThemeProvider } from '@elementor/ui/styles'; import { StrictMode, Fragment, createRoot } from '@wordpress/element'; import App from './app'; import AdminTopBar from './components/admin-top-bar'; +import { PluginSettingsProvider } from './contexts/plugin-settings'; +import { SettingsProvider, NotificationsProvider } from './hooks'; const rootNode = document.getElementById( 'ea11y-app' ); const topBarNode = document.getElementById( 'ea11y-app-top-bar' ); @@ -21,6 +23,12 @@ topBar.render( root.render( - + + + + + + + , ); diff --git a/modules/settings/assets/js/app.js b/modules/settings/assets/js/app.js index 2ca43d10..83ba765d 100644 --- a/modules/settings/assets/js/app.js +++ b/modules/settings/assets/js/app.js @@ -1,13 +1,22 @@ import '../css/style.css'; import DirectionProvider from '@elementor/ui/DirectionProvider'; import { ThemeProvider } from '@elementor/ui/styles'; -import { ConnectModal } from './components'; +import { ConnectModal, Notifications } from './components'; +import { usePluginSettingsContext } from './contexts/plugin-settings'; +import { useNotificationSettings } from './hooks'; const App = () => { + const { isConnected } = usePluginSettingsContext(); + const { + notificationMessage, + notificationType, + } = useNotificationSettings(); + return ( - + - + { ! isConnected && } + ); diff --git a/modules/settings/assets/js/components/index.js b/modules/settings/assets/js/components/index.js index a07d5cec..ef0c0495 100644 --- a/modules/settings/assets/js/components/index.js +++ b/modules/settings/assets/js/components/index.js @@ -1 +1,2 @@ export { default as ConnectModal } from './connect-modal'; +export { default as Notifications } from './notifications'; diff --git a/modules/settings/assets/js/components/notifications/index.js b/modules/settings/assets/js/components/notifications/index.js new file mode 100644 index 00000000..367ee784 --- /dev/null +++ b/modules/settings/assets/js/components/notifications/index.js @@ -0,0 +1,36 @@ +import Alert from '@elementor/ui/Alert'; +import Snackbar from '@elementor/ui/Snackbar'; +import { useNotificationSettings } from '../../hooks'; + +const Notifications = ( { type, message } ) => { + const { + showNotification, + setShowNotification, + setNotificationMessage, + setNotificationType, + } = useNotificationSettings(); + + const closeNotification = () => { + setShowNotification( ! showNotification ); + setNotificationMessage( '' ); + setNotificationType( '' ); + }; + + return ( + + setShowNotification( ! showNotification ) } + severity={ type } + variant="filled" > + { message } + + + ); +}; + +export default Notifications; diff --git a/modules/settings/assets/js/contexts/plugin-settings.js b/modules/settings/assets/js/contexts/plugin-settings.js new file mode 100644 index 00000000..54d028f9 --- /dev/null +++ b/modules/settings/assets/js/contexts/plugin-settings.js @@ -0,0 +1,40 @@ +import { createContext, useCallback, useContext, useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import API from '../api'; +import { useToastNotification } from '../hooks'; + +const PluginSettingsContext = createContext( {} ); + +export const PluginSettingsProvider = ( { children } ) => { + const { error } = useToastNotification(); + const [ pluginSettings, setPluginSettings ] = useState(); + const [ loaded, setLoaded ] = useState( false ); + + const refreshPluginSettings = useCallback( () => { + API.getPluginSettings().then( ( settings ) => { + if ( 'isConnected' in settings ) { + settings.isConnected = Boolean( settings.isConnected ); + } + + setPluginSettings( settings ); + setLoaded( true ); + } ).catch( () => { + error( __( 'An error occurred.', 'pojo-accessibility' ) ); + setLoaded( true ); + } ); + }, [] ); + + useEffect( () => { + refreshPluginSettings(); + }, [ refreshPluginSettings ] ); + + return ( + + { children } + + ); +}; + +export const usePluginSettingsContext = () => { + return useContext( PluginSettingsContext ); +}; diff --git a/modules/settings/assets/js/hooks/index.js b/modules/settings/assets/js/hooks/index.js index 0d9d628b..1ae75e25 100644 --- a/modules/settings/assets/js/hooks/index.js +++ b/modules/settings/assets/js/hooks/index.js @@ -1,2 +1,8 @@ export { useAuth } from './use-auth'; export { useModal } from './use-modal'; +export { useSettings } from './use-settings'; +export { SettingsProvider } from './use-settings'; +export { useStorage } from './use-storage'; +export { useToastNotification } from './use-notifications'; +export { NotificationsProvider } from './use-notifications'; +export { useNotificationSettings } from './use-notifications'; diff --git a/modules/settings/assets/js/hooks/use-auth.js b/modules/settings/assets/js/hooks/use-auth.js index 791ec33c..65c0fc55 100644 --- a/modules/settings/assets/js/hooks/use-auth.js +++ b/modules/settings/assets/js/hooks/use-auth.js @@ -1,5 +1,6 @@ import API from '../api'; import { UPGRADE_LINK } from '../constants'; +import { usePluginSettingsContext } from '../contexts/plugin-settings'; export const useAuth = () => { const { subscriptionId } = 123; diff --git a/modules/settings/assets/js/hooks/use-modal.js b/modules/settings/assets/js/hooks/use-modal.js index 65c79c13..19aaaaf1 100644 --- a/modules/settings/assets/js/hooks/use-modal.js +++ b/modules/settings/assets/js/hooks/use-modal.js @@ -18,4 +18,3 @@ export const useModal = ( defaultIsOpen = true ) => { close, }; }; - diff --git a/modules/settings/assets/js/hooks/use-notifications.js b/modules/settings/assets/js/hooks/use-notifications.js new file mode 100644 index 00000000..0ea75585 --- /dev/null +++ b/modules/settings/assets/js/hooks/use-notifications.js @@ -0,0 +1,53 @@ +import { useState, createContext, useContext } from '@wordpress/element'; + +const NotificationsContext = createContext( undefined ); + +export function useNotificationSettings() { + return useContext( NotificationsContext ); +} + +export const NotificationsProvider = ( { children } ) => { + const [ showNotification, setShowNotification ] = useState( false ); + const [ notificationMessage, setNotificationMessage ] = useState( '' ); + const [ notificationType, setNotificationType ] = useState( '' ); + + return ( + + { children } + + ); +}; + +export const useToastNotification = () => { + const { + setNotificationMessage, + setNotificationType, + setShowNotification, + } = useContext( NotificationsContext ); + + const error = ( message ) => { + setNotificationMessage( message ); + setNotificationType( 'error' ); + setShowNotification( true ); + }; + + const success = ( message ) => { + setNotificationMessage( message ); + setNotificationType( 'success' ); + setShowNotification( true ); + }; + + return { + success, + error, + }; +}; diff --git a/modules/settings/assets/js/hooks/use-settings.js b/modules/settings/assets/js/hooks/use-settings.js new file mode 100644 index 00000000..69d5a774 --- /dev/null +++ b/modules/settings/assets/js/hooks/use-settings.js @@ -0,0 +1,24 @@ +import { useState, createContext, useContext } from '@wordpress/element'; + +/** + * Context Component. + */ +const SettingsContext = createContext( null ); + +export function useSettings() { + return useContext( SettingsContext ); +} + +export const SettingsProvider = ( { children } ) => { + const [ test, setTest ] = useState( 'Test' ); + return ( + + { children } + + ); +}; diff --git a/modules/settings/assets/js/hooks/use-storage.js b/modules/settings/assets/js/hooks/use-storage.js new file mode 100644 index 00000000..23775120 --- /dev/null +++ b/modules/settings/assets/js/hooks/use-storage.js @@ -0,0 +1,12 @@ +import { store as coreDataStore } from '@wordpress/core-data'; +import { dispatch } from '@wordpress/data'; + +export const useStorage = () => { + const save = async ( data ) => { + return await dispatch( coreDataStore ).saveEntityRecord( 'root', 'site', data ); + }; + + return { + save, + }; +}; diff --git a/modules/settings/classes/route-base.php b/modules/settings/classes/route-base.php new file mode 100644 index 00000000..907a3409 --- /dev/null +++ b/modules/settings/classes/route-base.php @@ -0,0 +1,39 @@ +get_path(); + } + + public function get_path(): string { + return $this->path; + } + + public function get_name(): string { + return ''; + } + + public function get_permission_callback( \WP_REST_Request $request ): bool { + $valid = $this->permission_callback( $request ); + + return $valid && user_can( $this->current_user_id, 'manage_options' ); + } +} diff --git a/modules/settings/classes/settings.php b/modules/settings/classes/settings.php new file mode 100644 index 00000000..1ef1d258 --- /dev/null +++ b/modules/settings/classes/settings.php @@ -0,0 +1,28 @@ + Connect::is_connected(), + ]; + } /** * Module constructor. */ public function __construct() { + $this->register_routes(); $this->register_components(); add_action( 'admin_menu', [ $this, 'register_page' ] ); add_action( 'in_admin_header', [ $this, 'render_top_bar' ] ); diff --git a/modules/settings/rest/get-settings.php b/modules/settings/rest/get-settings.php new file mode 100644 index 00000000..4cdd5815 --- /dev/null +++ b/modules/settings/rest/get-settings.php @@ -0,0 +1,50 @@ +verify_capability(); + + if ( $error ) { + return $error; + } + + $data = Settings::get_plugin_settings(); + + return $this->respond_success_json( $data ); + + } catch ( Throwable $t ) { + return $this->respond_error_json( [ + 'message' => $t->getMessage(), + 'code' => 'internal_server_error', + ] ); + } + } +} diff --git a/package.json b/package.json index e5a1f257..db245eec 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "@wordpress/date": "^5.10.0", "@wordpress/element": "^6.10.0", "@wordpress/i18n": "^5.10.0", - "@wordpress/url": "^4.10.0" + "@wordpress/url": "^4.10.0", + "husky": "^9.1.6", + "prop-types": "^15.8.1" } }