actions->retryPendingConfirm(); wp_enqueue_script( 'lingocanvas-cloud', Assets::url( 'assets/cloud.js' ), array(), Assets::version( 'assets/cloud.js' ), true ); $consent = $this->settings->cloudConsentAt() > 0; $claim = $this->hasToken() ? null : $this->providers->cloudClient()->pendingClaim(); wp_localize_script( 'lingocanvas-cloud', 'lingocanvasCloud', array( 'statusUrl' => esc_url_raw( rest_url( CloudController::NAMESPACE . '/cloud/status' ) ), 'claimUrl' => esc_url_raw( rest_url( CloudController::NAMESPACE . '/cloud/claim' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'connected' => $this->hasToken(), // Без согласия скрипт не обращается к Cloud: ни статус, ни опрос оплаты. 'consent' => $consent, 'pending' => null !== $claim && $consent, 'expiresIn' => null !== $claim ? max( 0, $claim['expires_at'] - time() ) : 0, 'i18n' => array( 'loading' => __( 'Loading status…', 'lingocanvas' ), 'failed' => __( 'Could not load the status.', 'lingocanvas' ), 'checking' => __( 'Checking the payment…', 'lingocanvas' ), 'timeout' => __( 'The payment has not been confirmed yet. Use "Check payment" later.', 'lingocanvas' ), 'connected' => __( 'LingoCanvas Cloud is connected.', 'lingocanvas' ), 'noPack' => __( 'Extra packs can be bought only with an active subscription.', 'lingocanvas' ), ), ) ); } // --- Разметка --------------------------------------------------------------- /** * Переключатель провайдера перевода. */ public function providerHtml(): string { $current = $this->settings->provider(); $radio = static fn( string $value, string $label ): string => sprintf( '
', esc_attr( $value ), Settings::sanitizeProvider( $current, Settings::PROVIDER_OPENAI ) === $value ? ' checked="checked"' : '', esc_html( $label ) ); return ''; } /** * Блок Cloud: раскрытие данных, согласие и состояние (регистрация / ожидание оплаты / подключено). */ public function cloudHtml(): string { $hasToken = $this->hasToken(); $claim = $hasToken ? null : $this->providers->cloudClient()->pendingClaim(); $consent = $this->settings->cloudConsentAt() > 0; $html = '' . esc_html__( 'You canceled a payment on this site. If you had already paid, the confirmation can still be received until the payment session expires.', 'lingocanvas' ) . '
' . ''; } /** * Выводит сообщение о результате последнего действия (один раз). */ public function renderNotice(): void { $key = self::NOTICE_PREFIX . get_current_user_id(); $notice = get_transient( $key ); if ( ! is_array( $notice ) ) { return; } delete_transient( $key ); $type = in_array( $notice['type'] ?? '', array( 'success', 'error', 'info', 'warning' ), true ) ? (string) $notice['type'] : 'info'; printf( '%2$s
' . sprintf( /* translators: 1: link "Terms", 2: link "Privacy Policy". */ esc_html__( 'The texts you translate, the source and target language, the site name and an optional e-mail address are sent to LingoCanvas Cloud (cloud.centerai.eu, operated by CenterAI). The translation is performed by DeepSeek, whose servers are in China. Read the %1$s and the %2$s. Manual translation and your own API key remain free and work without registration.', 'lingocanvas' ), $terms, $privacy ) . '
'; } /** * Галочка согласия внутри главной формы (её можно снять и сохранить). * * @param bool $checked Согласие уже дано. */ private function consentHtml( bool $checked ): string { return ''; } /** * Подписка: согласие (если ещё нет), почта и кнопка. * * @param bool $consent Согласие уже дано. */ private function signupHtml( bool $consent ): string { $form = $this->formId( 'signup' ); $html = '
'
. '
' . esc_html__( 'You will be redirected to Stripe to pay. After the payment, come back to this page: the site connects automatically.', 'lingocanvas' ) . '
' . '' . esc_html__( 'Waiting for payment confirmation…', 'lingocanvas' ) . '
' . '
'; } /** * Подключённый сайт: панель состояния и кнопки. */ private function connectedHtml(): string { $byConstant = $this->providers->isOverriddenByConstant( ProviderFactory::CONST_CLOUD_TOKEN ); $field = static fn( string $name, string $label ): string => sprintf( '' . esc_html( $byConstant ? __( 'This site is connected to LingoCanvas Cloud. The site token is set in wp-config.php.', 'lingocanvas' ) : __( 'This site is connected to LingoCanvas Cloud.', 'lingocanvas' ) ) . '
'; $html .= '' . ( $this->settings->cloudConsentAt() > 0 ? esc_html__( 'Loading status…', 'lingocanvas' ) : esc_html__( 'Status is not loaded: you have not agreed to send texts to LingoCanvas Cloud. Tick the agreement above and save the settings.', 'lingocanvas' ) ) . '
' . '' . esc_html__( 'To disconnect this site, remove LINGOCANVAS_CLOUD_TOKEN from wp-config.php.', 'lingocanvas' ) . '
'; return $html; } return $html . '' . '' . esc_html__( 'Disconnecting only removes the connection from this site: your subscription is NOT canceled and keeps being billed. To cancel it, use "Manage subscription". Translations already saved stay in place, and AI translation switches back to your own API key.', 'lingocanvas' ) . '
'; } /** * Пустая форма действия: кнопки подключаются к ней атрибутом `form`. * * @param string $action Действие admin-post. * @param string $suffix Окончание id формы. * @param string $product Товар (для оплаты) или пустая строка. */ private function formHtml( string $action, string $suffix, string $product = '' ): string { return ''; } /** * Идентификатор формы действия. * * @param string $suffix Окончание. */ private function formId( string $suffix ): string { return 'lingocanvas-cloud-form-' . $suffix; } /** * Есть ли токен сайта (константа или настройки). Сам токен не читается наружу. */ private function hasToken(): bool { return '' !== $this->providers->cloudToken(); } // --- Обработчики admin-post -------------------------------------------------- /** * Подписка: уходит в Stripe. */ public function handleSignup(): void { $this->denyUnlessAdmin(); check_admin_referer( self::ACTION_SIGNUP ); $email = isset( $_POST['cloud_email'] ) && is_scalar( $_POST['cloud_email'] ) ? trim( sanitize_text_field( wp_unslash( (string) $_POST['cloud_email'] ) ) ) : ''; $consentValue = isset( $_POST['cloud_consent'] ) && is_scalar( $_POST['cloud_consent'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['cloud_consent'] ) ) : ''; $this->finish( $this->actions->signup( $email, '' !== $consentValue && '0' !== $consentValue ) ); } /** * Кнопка «Проверить оплату». */ public function handleClaim(): void { $this->denyUnlessAdmin(); check_admin_referer( self::ACTION_CLAIM ); $this->backWithClaimResult( $this->actions->claim() ); } /** * Кнопка «Уже оплатили? Проверить ещё раз» у отменённой заявки. */ public function handleClaimArchived(): void { $this->denyUnlessAdmin(); check_admin_referer( self::ACTION_CLAIM_OLD ); $this->backWithClaimResult( $this->actions->claimArchived() ); } /** * Кнопка «Отмена» у ожидающей оплаты: сначала одна проверка оплаты, затем заявка уходит в архив. */ public function handleCancel(): void { $this->denyUnlessAdmin(); check_admin_referer( self::ACTION_CANCEL ); $result = $this->actions->cancelClaim(); $this->back( 'connected' === $result['status'] ? 'success' : 'info', $result['message'] ); } /** * Возвращает на страницу настроек с итогом проверки оплаты. * * @param array{status: string, message: string} $result Результат CloudActions::claim(). */ private function backWithClaimResult( array $result ): void { $types = array( 'connected' => 'success', 'pending' => 'info', 'no_consent' => 'error', 'error' => 'error', ); $this->back( $types[ $result['status'] ] ?? 'error', $result['message'] ); } /** * Покупка пакета символов или повторная подписка. */ public function handleCheckout(): void { $this->denyUnlessAdmin(); check_admin_referer( self::ACTION_CHECKOUT ); $product = isset( $_POST['product'] ) && is_scalar( $_POST['product'] ) ? sanitize_key( wp_unslash( (string) $_POST['product'] ) ) : ''; $this->finish( $this->actions->checkout( $product ) ); } /** * Управление подпиской на портале Stripe. */ public function handlePortal(): void { $this->denyUnlessAdmin(); check_admin_referer( self::ACTION_PORTAL ); $this->finish( $this->actions->portal() ); } /** * Отключение сайта от Cloud. */ public function handleDisconnect(): void { $this->denyUnlessAdmin(); check_admin_referer( self::ACTION_DISCONNECT ); $result = $this->actions->disconnect(); $this->back( $result['ok'] ? 'success' : 'error', $result['message'] ); } /** * Останавливает запрос, если у пользователя нет права менять настройки. */ private function denyUnlessAdmin(): void { if ( ! current_user_can( SettingsPage::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to change these settings.', 'lingocanvas' ), '', array( 'response' => 403 ) ); } } /** * Завершает действие: уходит на Stripe или возвращается с сообщением об ошибке. * * @param array{ok: bool, url: string, message: string} $result Результат действия. */ private function finish( array $result ): void { if ( ! $result['ok'] ) { $this->back( 'error', $result['message'] ); return; } try { $url = CloudClient::stripeUrl( $result['url'] ); } catch ( CloudException $e ) { $this->back( 'error', $e->userMessage() ); return; } $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); // Узел Stripe разрешается только для этого одного редиректа. add_filter( 'allowed_redirect_hosts', static function ( $hosts ) use ( $host ) { $hosts = is_array( $hosts ) ? $hosts : array(); $hosts[] = $host; return $hosts; } ); wp_safe_redirect( $url ); exit; } /** * Возвращает на страницу настроек с сообщением. * * @param string $type success, error, info или warning. * @param string $message Текст для владельца сайта (никогда не сырой ответ сервера). */ private function back( string $type, string $message ): void { set_transient( self::NOTICE_PREFIX . get_current_user_id(), array( 'type' => $type, 'message' => $message, ), 2 * MINUTE_IN_SECONDS ); wp_safe_redirect( add_query_arg( array( 'page' => SettingsPage::MENU_SLUG ), admin_url( 'admin.php' ) ) . '#' . self::ANCHOR ); exit; } }