'; public function __construct( private readonly KeptFixesPage $kept = new KeptFixesPage(), private readonly AssistantPage $assistant = new AssistantPage(), private readonly ReportPage $report = new ReportPage(), private readonly RequestsPage $requests = new RequestsPage(), ) { } public function register(): void { $this->kept->register(); $this->assistant->register(); add_action('admin_menu', [$this, 'registerMenu']); add_action('admin_post_' . self::OPT_IN_ACTION, [$this, 'optIn']); add_action('admin_enqueue_scripts', [$this, 'enqueueStyles']); } public function registerMenu(): void { $badge = current_user_can(self::CAPABILITY) ? InboxSummary::badge() : ''; add_menu_page( esc_html(Brand::name()), esc_html(Brand::name()) . $badge, self::CAPABILITY, self::SLUG, [$this, 'renderInbox'], self::menuIcon(), 81, ); add_submenu_page(self::SLUG, __('Inbox', 'sarcio'), __('Inbox', 'sarcio') . $badge, self::CAPABILITY, self::SLUG, [$this, 'renderInbox']); add_submenu_page(self::SLUG, __('Assistant', 'sarcio'), __('Assistant', 'sarcio'), self::CAPABILITY, AssistantPage::SLUG, [$this->assistant, 'render']); add_submenu_page(self::SLUG, __('Live fixes', 'sarcio'), __('Live fixes', 'sarcio'), self::CAPABILITY, self::LIVE_SLUG, [$this, 'renderLive']); add_submenu_page(self::SLUG, __('Kept fixes', 'sarcio'), __('Kept fixes', 'sarcio'), self::CAPABILITY, KeptFixesPage::SLUG, [$this->kept, 'render']); add_submenu_page(self::SLUG, __('Requests', 'sarcio'), __('Requests', 'sarcio') . RequestsPage::badge(), self::CAPABILITY, RequestsPage::SLUG, [$this->requests, 'render']); add_submenu_page(self::SLUG, __('Report', 'sarcio'), __('Report', 'sarcio'), self::CAPABILITY, ReportPage::SLUG, [$this->report, 'render']); add_submenu_page(self::SLUG, __('Settings', 'sarcio'), __('Settings', 'sarcio'), 'manage_options', 'options-general.php?page=' . SettingsPage::SLUG); } public static function menuIcon(): string { return 'data:image/svg+xml;base64,' . base64_encode(self::MENU_ICON); } public function enqueueStyles(string $hook): void { $isInbox = $hook === 'toplevel_page_' . self::SLUG || str_ends_with($hook, '_page_' . self::LIVE_SLUG); if (!$isInbox && !str_ends_with($hook, '_page_' . KeptFixesPage::SLUG)) { return; } wp_add_inline_style('common', Cards::styles()); if (!$isInbox || !$this->ready()) { return; } wp_add_inline_style('common', self::asset('css/inbox.css')); wp_register_script(self::SCRIPT, false, [], SARCIO_WP_VERSION, true); wp_enqueue_script(self::SCRIPT); wp_add_inline_script(self::SCRIPT, self::asset('js/inbox.js')); wp_add_inline_script(self::SCRIPT, 'var sarcioInbox=' . InlineJson::encode([ 'canDecide' => current_user_can(InboxRoutes::decideCapability()) ? '1' : '', 'canKeep' => current_user_can('manage_options') ? '1' : '', 'canUpdate' => current_user_can('update_plugins') || current_user_can('update_themes') ? '1' : '', 'keepAction' => KeptFixesPage::KEEP_ACTION, 'keepNonce' => wp_create_nonce(KeptFixesPage::KEEP_ACTION), 'keptUrl' => KeptFixesPage::url(), 'nonce' => wp_create_nonce('wp_rest'), 'postUrl' => esc_url_raw(admin_url('admin-post.php')), 'restUrl' => esc_url_raw(rest_url(InboxRoutes::NAMESPACE . '/inbox/')), 'strings' => self::strings(), ]) . ';', 'before'); } private function ready(): bool { return (string) Options::get('site_key') !== '' && (string) Options::get('sidecar_key') !== '' && Options::get('review_in_admin') && Account::has(get_current_user_id()); } /** The plugin's own bundled asset, printed inline so it works wherever the plugin is installed. */ private static function asset(string $file): string { $path = dirname(__DIR__, 2) . '/assets/' . $file; if (!is_readable($path)) { return ''; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- the plugin's own bundled file. $source = file_get_contents($path); return is_string($source) ? $source : ''; } public function optIn(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Only an administrator can turn this on.', 'sarcio'), 403); } check_admin_referer(self::OPT_IN_ACTION); Options::update(['review_in_admin' => true]); wp_safe_redirect(admin_url('admin.php?page=' . self::SLUG)); exit; } public function renderInbox(): void { $this->render('inbox', __('Inbox', 'sarcio')); } public function renderLive(): void { $this->render('live', __('Live fixes', 'sarcio')); } private function render(string $view, string $title): void { if (!current_user_can(self::CAPABILITY)) { return; } if (!Connect::connected()) { Cards::welcome(); return; } $this->renderNotices(); $info = Embed::info(); $newTab = Embed::workspaceUrl() ?? 'https://www.sarcio.io/app/'; if (!Options::get('review_in_admin')) { $this->renderOptIn($newTab, $info['workspaceName'] ?? ''); return; } if (isset($info['error'])) { Cards::notice( __('Sarcio', 'sarcio'), __('The Sarcio Inbox could not be opened here', 'sarcio'), $info['error'], [[__('Open Sarcio in a new tab', 'sarcio'), $newTab, false]], ); return; } $userId = get_current_user_id(); if (!Account::has($userId) || isset(Account::accessToken($userId)['reconnect'])) { Cards::account(Brand::screen($title), $info['workspaceName'] ?? '', $newTab, Account::has($userId)); return; } InboxSummary::forget(); echo '
'; echo '

' . wp_kses(Brand::logoTag(), Brand::ALLOWED_HTML) . esc_html(Brand::screen($title)) . '

'; printf('
', esc_attr($view)); echo '

' . esc_html__('Loading…', 'sarcio') . '

'; echo ''; echo '
'; printf('
'; } private function renderNotices(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only: the redirect after Connect::finish(). $connected = isset($_GET['sarcio-connected']) ? sanitize_key(wp_unslash($_GET['sarcio-connected'])) : ''; if ($connected !== '') { $message = $connected === 'account' ? __('Your Sarcio account is connected. WordPress keeps you signed in here.', 'sarcio') : __('This site is connected to Sarcio. Reports people send from it arrive in the Inbox.', 'sarcio'); echo '

' . esc_html($message) . '

'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only: the redirect after Connect::finish(). if (isset($_GET['sarcio-connect-error'])) { echo '

' . esc_html__('Sarcio could not connect your account. Try again.', 'sarcio') . '

'; } } /** @return array */ private static function strings(): array { return [ 'applied' => __('Applied', 'sarcio'), 'approve' => __('Approve and go live', 'sarcio'), 'approveElevated' => __('Approve, but ask someone else to check it too', 'sarcio'), 'approved' => __('Approved.', 'sarcio'), 'back' => __('Inbox', 'sarcio'), 'cancel' => __('Cancel', 'sarcio'), 'confirm' => __('Confirm and approve', 'sarcio'), 'confirmCode' => __('Use a code from your authenticator instead', 'sarcio'), 'confirmCodeLabel' => __('Authenticator code', 'sarcio'), 'confirmIntro' => __('Confirm it is you to approve: enter your Sarcio password.', 'sarcio'), 'confirmPassword' => __('Use your Sarcio password instead', 'sarcio'), 'confirmPasswordLabel' => __('Sarcio password', 'sarcio'), 'details' => __('Details', 'sarcio'), 'draftNew' => __('Draft a new fix', 'sarcio'), 'drafting' => __('Sarcio is working out a fix. This usually takes under a minute.', 'sarcio'), 'emptyInbox' => __('When a visitor clicks Report a bug on your site, it shows up here with a fix ready to check.', 'sarcio'), 'emptyInboxTitle' => __('No reports yet', 'sarcio'), 'emptyLive' => __('Fixes you approve from the Inbox show up here.', 'sarcio'), 'emptyLiveTitle' => __('Nothing live right now', 'sarcio'), 'failed' => __('Sarcio could not do that right now. Try again in a moment.', 'sarcio'), 'hideDetails' => __('Hide details', 'sarcio'), 'hideScreenshot' => __('Hide screenshot', 'sarcio'), 'keep' => __('Keep this fix', 'sarcio'), 'keepAdmin' => __('A site administrator can keep it.', 'sarcio'), 'keepIntro' => __('Live for every visitor now. Keep it to save it on your site for good.', 'sarcio'), 'kept' => __('This fix is saved on your site. It keeps working for good, even without Sarcio. Find it under Sarcio → Kept fixes.', 'sarcio'), 'loadFailed' => __('The Inbox could not be loaded.', 'sarcio'), 'noDecide' => __('You can look at fixes here. Someone with more access on this site approves them.', 'sarcio'), 'noticed' => __('Sarcio noticed', 'sarcio'), 'notABug' => __('Sarcio did not find anything broken here, so there is no fix to approve.', 'sarcio'), 'preview' => __('Preview', 'sarcio'), 'previewBlocked' => __('Your browser blocked the preview window. Allow pop-ups for this page and try again.', 'sarcio'), 'reconnect' => __('Your Sarcio sign-in here has ended. Reload the page to connect again.', 'sarcio'), 'redraftLabel' => __('What should be different? (optional)', 'sarcio'), 'redraftToggle' => __('Not quite right? Ask Sarcio for a different fix', 'sarcio'), 'reject' => __('Turn down', 'sarcio'), 'rejected' => __('Turned down.', 'sarcio'), /* translators: %d: how many times the same problem was reported again. */ 'repeats' => __('Reported %d more times', 'sarcio'), 'reportHeading' => __('A visitor reported', 'sarcio'), 'screenshot' => __('See their screenshot', 'sarcio'), 'screenshotAlt' => __('What the visitor saw', 'sarcio'), 'secondApproval' => __('Someone approved this and asked for a second person to check it. It goes live when a different person approves it.', 'sarcio'), 'stateApproved' => __('Waiting for a second approval', 'sarcio'), 'stateKept' => __('Kept on this site', 'sarcio'), 'stateLive' => __('Live', 'sarcio'), 'stateReady' => __('Ready to check', 'sarcio'), 'stateRejected' => __('Turned down', 'sarcio'), 'stateRetired' => __('Turned off', 'sarcio'), 'statusNotABug' => __('Not a bug', 'sarcio'), 'statusReady' => __('Fix ready', 'sarcio'), 'statusTriaging' => __('Working on it', 'sarcio'), 'theFix' => __('The fix', 'sarcio'), 'turnedOff' => __('Turned off. The site is back to how it was before the fix.', 'sarcio'), 'turnOff' => __('Turn off', 'sarcio'), 'turnOffAsk' => __('Turn off this fix? The site goes back to how it was before the fix.', 'sarcio'), 'turnOffConfirm' => __('Confirm and turn off', 'sarcio'), 'turnOffConfirmIntro' => __('Confirm it’s you to turn off this fix: enter your Sarcio password.', 'sarcio'), 'turnOffFailed' => __('The fix could not be turned off right now. It is still live. Try again in a moment.', 'sarcio'), 'updateCheck' => __('Update check', 'sarcio'), 'updateDecided_kept' => __('Kept as it is.', 'sarcio'), 'updateDecided_rolled_back' => __('The earlier version was put back.', 'sarcio'), 'updateDecided_updated_again' => __('The update was tried again.', 'sarcio'), 'updateDone' => __('Done.', 'sarcio'), 'updateHeading' => __('What the update check found', 'sarcio'), 'updateKeep' => __('Keep the update', 'sarcio'), 'updateKeepAnyway' => __('Leave it as it is', 'sarcio'), 'updateKeepEarlier' => __('Keep the earlier version', 'sarcio'), 'updateNoAccess' => __('Someone who can update plugins on this site decides what happens next.', 'sarcio'), 'updateOutcome_held' => __('Nothing crashed, so the update was kept. Put the earlier version back if something is wrong, or keep it.', 'sarcio'), 'updateOutcome_rollback_failed' => __('Putting the earlier version back did not work, so the site may still be broken. Try again, or ask Sarcio for a fix below.', 'sarcio'), 'updateOutcome_rolled_back' => __('Sarcio put the earlier version back, so the site works as before. Keep it, or try the update again later.', 'sarcio'), 'updateRetry' => __('Try the update again', 'sarcio'), 'updateRollback' => __('Put the earlier version back', 'sarcio'), 'updateRollbackAgain' => __('Try putting it back again', 'sarcio'), 'updateSaving' => __('Saving…', 'sarcio'), 'updateWorking' => __('Working on it. This can take a minute.', 'sarcio'), 'urgent' => __('Urgent', 'sarcio'), ]; } private function renderOptIn(string $newTab, string $workspace): void { $steps = [ ['dashicons-flag', __('A report comes in', 'sarcio'), __('Someone presses Report a bug on your site. Sarcio drafts a fix for it.', 'sarcio')], ['dashicons-visibility', __('Preview the fix', 'sarcio'), __('Try it on your live site. Only you see it until you approve.', 'sarcio')], ['dashicons-yes-alt', __('Approve & go live', 'sarcio'), __('One click, and every visitor gets the fix.', 'sarcio')], ]; echo '

' . esc_html(Brand::screen(__('Inbox', 'sarcio'))) . '

'; echo '
'; echo '

' . esc_html__('Review bug reports without leaving WordPress', 'sarcio') . '

'; echo '

' . esc_html__('Bring your Sarcio Inbox into this page: every report from this site, its drafted fix in plain words, and the buttons to preview, approve or reject it.', 'sarcio') . '

'; Cards::steps($steps); echo '
'; echo '
' . esc_html__('What you need', 'sarcio') . '
' . esc_html($workspace !== '' ? sprintf( /* translators: %s: Sarcio workspace name. */ __('Your own Sarcio account in the %s workspace. You sign in once, and WordPress keeps you signed in. An admin adds people under Members in Sarcio.', 'sarcio'), $workspace, ) : __('Your own Sarcio account in this site\'s workspace. You sign in once, and WordPress keeps you signed in. An admin adds people under Members in Sarcio.', 'sarcio')) . '
'; echo '
' . esc_html__('Privacy', 'sarcio') . '
' . esc_html__('WordPress asks Sarcio for this site\'s reports with your own Sarcio sign-in, stored for you encrypted, and shows them on this page. Approving a fix always asks for your Sarcio password again.', 'sarcio') . '
'; echo '
'; echo '
'; if (current_user_can('manage_options')) { printf('
', esc_url(admin_url('admin-post.php'))); printf('', esc_attr(self::OPT_IN_ACTION)); wp_nonce_field(self::OPT_IN_ACTION); submit_button(__('Open the Inbox here', 'sarcio'), 'primary', 'submit', false); echo '
'; } else { echo '

' . esc_html__('An administrator can turn this on.', 'sarcio') . '

'; } printf('%s', esc_url($newTab), esc_html__('Open Sarcio in a new tab', 'sarcio')); echo '
'; } }