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"
}
}