signer_email ) ? trim( (string) $context->signer_email ) : ''; $phone = isset( $context->signer_phone ) ? trim( (string) $context->signer_phone ) : ''; } // No channel at all: both channels share the same explicit reason. $none = ( '' === $email && '' === $phone ); return array( 'email' => self::status_email( $email, $none ), ); } /** * Availability of the email channel. * * @param string $email Signer email (already trimmed). * @param bool $none True when no channel was provided at all. * @return array { @type bool $available, @type string|null $reason } */ private static function status_email( $email, $none ) { if ( $none ) { return self::unavailable( self::NO_CHANNEL_PROVIDED ); } if ( '' === $email || ! self::is_valid_email( $email ) ) { return self::unavailable( self::EMAIL_INVALID ); } // SINGLE SOURCE OF TRUTH shared with the send path (EquaSign_Brevo::send_email): // the email channel is available through Brevo OR the wp_mail fallback (E-1). Testing // is_key_configured() here (Brevo only) would ignore the fallback and wrongly report // "no channel" on the signer page while the invitation actually went out via wp_mail — // the v0.7.1 inconsistency this unifies. A channel available for the invitation is, by // construction, available for the OTP. if ( ! EquaSign_Brevo::email_transport_available() ) { return self::unavailable( self::BREVO_KEY_MISSING ); } // The Brevo account probe (credits / sender) only applies when Brevo IS the transport; // with the wp_mail fallback there is no Brevo account to verify → available. if ( ! EquaSign_Brevo::is_key_configured() ) { return self::available(); } // Offline checks passed. Account-level verification is deferred (Brevo path). return self::account_status( 'email' ); } /** * Extension point for Brevo account verification (GET /v3/account). * * This is the ONLY place that will perform the network probe in a later * phase. As of this phase there is nothing to decide without the network, * so the channel is reported as available. A future implementation may * return one of the reserved codes: * - blocking → unavailable(): brevo_key_invalid, email_sender_unverified, * email_credit_insufficient, brevo_account_error; * - advisory → available() with a warning reason set (available stays * true): email_credit_low. * * @param string $channel email. * @return array { @type bool $available, @type string|null $reason } */ private static function account_status( $channel ) { $account = EquaSign_Brevo::account(); // Uncertainty fails OPEN: a transient/unknown probe error never blocks a // legitimate signing flow (the real send would surface any hard error). // Only a definite signal changes availability. if ( is_wp_error( $account ) ) { return ( 'brevo_key_invalid' === $account->get_error_code() ) ? self::unavailable( self::BREVO_KEY_INVALID ) : self::available(); } $credits = EquaSign_Brevo::credits_from( $account, $channel ); if ( null === $credits ) { return self::available(); // Indeterminate payload → stay permissive. } if ( $credits <= 0 ) { return self::unavailable( self::EMAIL_CREDIT_INSUFFICIENT ); } // Advisory low-credit warning (channel stays available); threshold per // channel, 0 disables the warning. $threshold = (int) get_option( 'equasign_email_credit_low', 100 ); if ( $threshold > 0 && $credits <= $threshold ) { return self::available( self::EMAIL_CREDIT_LOW ); } return self::available(); } /** * Validate an email address. * * @param string $email Email. * @return bool */ private static function is_valid_email( $email ) { return (bool) is_email( $email ); } /** * Validate a phone number in a permissive E.164 form (6 to 15 digits, * optional leading "+"). Mirrors the sanitizer used on task creation. * * @param string $phone Phone. * @return bool */ private static function is_valid_phone( $phone ) { $phone = preg_replace( '/[^0-9+]/', '', (string) $phone ); return ( '' !== $phone && (bool) preg_match( '/^\+?[0-9]{6,15}$/', $phone ) ); } /** * Available channel sub-status. * * @param string|null $reason Optional advisory reason (kept while available). * @return array */ private static function available( $reason = null ) { return array( 'available' => true, 'reason' => $reason, ); } /** * Unavailable channel sub-status. * * @param string $reason Blocking reason code. * @return array */ private static function unavailable( $reason ) { return array( 'available' => false, 'reason' => $reason, ); } /** * Human-readable message for a reason code (translatable). Unknown codes * fall back to the generic Brevo account guidance. * * @param string|null $code Reason code, or null (channel available). * @return string */ public static function reason_message( $code ) { if ( null === $code || '' === $code ) { return ''; } switch ( $code ) { case self::NO_CHANNEL_PROVIDED: return __( 'No delivery channel was provided.', 'equasign' ); case self::EMAIL_INVALID: return __( 'No valid email address is available.', 'equasign' ); case self::PHONE_INVALID: return __( 'No valid phone number is available.', 'equasign' ); case self::BREVO_KEY_MISSING: return __( 'Brevo API key is not configured.', 'equasign' ); case self::BREVO_KEY_INVALID: return __( 'The Brevo API key is invalid.', 'equasign' ); case self::EMAIL_SENDER_UNVERIFIED: return __( 'The email sender is not verified on Brevo.', 'equasign' ); case self::EMAIL_CREDIT_INSUFFICIENT: return __( 'Not enough email credits on the Brevo account.', 'equasign' ); case self::EMAIL_CREDIT_LOW: return __( 'Email credits are running low.', 'equasign' ); case self::BREVO_ACCOUNT_ERROR: default: return __( 'Check the Brevo account.', 'equasign' ); } } }