'checkoutline',
'screen_id' => self::screen_id(),
'title' => __( 'Checkout Fields', 'checkoutline' ),
)
);
}
}
/** @param array $ids */
public function screen_ids( $ids ): array {
$ids = (array) $ids;
$ids[] = self::screen_id();
return $ids;
}
/** @param array $links */
public function action_links( $links ): array {
return array_merge( array( '' . esc_html__( 'Checkout Fields', 'checkoutline' ) . '' ), (array) $links );
}
public function enqueue( $hook = '' ): void {
if ( self::screen_id() !== $hook ) {
return;
}
wp_enqueue_script( 'checkoutline-admin', CHECKOUTLINE_URL . 'assets/js/admin.js', array( 'jquery', 'jquery-ui-sortable', 'wc-enhanced-select' ), CHECKOUTLINE_VERSION, true );
wp_localize_script( 'checkoutline-admin', 'checkoutlineAdmin', array( 'confirmDelete' => __( 'Delete this field? Orders keep the values already saved.', 'checkoutline' ) ) );
wp_enqueue_style( 'checkoutline-admin', CHECKOUTLINE_URL . 'assets/css/admin.css', array( 'woocommerce_admin_styles' ), CHECKOUTLINE_VERSION );
}
/**
* Lines under a field's label in the list: why it isn't shown at checkout, if it can't be, and
* what add-ons say about it (e.g. its conditions).
*
* @return string[]
*/
public static function notes( array $field ): array {
$notes = array();
if ( ! Fields::type( $field['type'] ) ) {
$notes[] = __( 'Not shown at checkout: the plugin that provides this type is inactive. Its settings and saved answers are kept.', 'checkoutline' );
} elseif ( ! Fields::available( $field ) ) {
$notes[] = __( 'Not shown at checkout: the plugin that checks when it shows is inactive. Its settings and saved answers are kept.', 'checkoutline' );
}
/**
* Lines shown under a field's label in the list of fields, as plain text.
*
* @param string[] $notes Lines so far.
* @param array $field The field.
*/
return array_values( array_filter( array_map( 'strval', (array) apply_filters( 'checkoutline_field_summary', $notes, $field ) ) ) );
}
public function render(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- which screen to show.
$edit = isset( $_GET['edit'] ) ? sanitize_key( wp_unslash( (string) $_GET['edit'] ) ) : '';
if ( '' !== $edit ) {
$this->render_edit( $edit );
return;
}
$fields = Fields::all();
$types = Fields::types();
?>
[ type, text ]. */
public static function messages(): array {
$messages = array(
'saved' => array( 'success', __( 'Checkout fields saved.', 'checkoutline' ) ),
'added' => array( 'success', __( 'Field added. It shows at checkout now.', 'checkoutline' ) ),
'updated' => array( 'success', __( 'Field updated.', 'checkoutline' ) ),
'deleted' => array( 'success', __( 'Field deleted. Orders keep the values already saved.', 'checkoutline' ) ),
'label' => array( 'error', __( 'Give the field a label.', 'checkoutline' ) ),
'key' => array( 'error', __( 'That key is used by another field. Choose another.', 'checkoutline' ) ),
'options' => array( 'error', __( 'This type of field needs at least one option.', 'checkoutline' ) ),
);
/**
* Notices on the Checkout Fields screen, by the code in its `message` query argument: code =>
* [ 'success' or 'error', text ]. An add-on adds the texts for its own error codes here
* (see `checkoutline_field_error`).
*
* @param array $messages
*/
$filtered = apply_filters( 'checkoutline_admin_messages', $messages );
return is_array( $filtered ) ? $filtered + $messages : $messages;
}
private function notices(): void {
$messages = self::messages();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a message after a redirect.
$message = isset( $_GET['message'] ) ? sanitize_key( wp_unslash( (string) $_GET['message'] ) ) : '';
if ( isset( $messages[ $message ] ) && is_array( $messages[ $message ] ) ) {
$type = 'error' === ( $messages[ $message ][0] ?? '' ) ? 'error' : 'success';
printf( '', esc_attr( $type ), esc_html( (string) ( $messages[ $message ][1] ?? '' ) ) );
}
}
/** Types (space-separated) whose definition matches, for a row's data-types; $extra is a stored type no plugin registers. */
private static function types_where( callable $test, string $extra = '' ): string {
$types = array_keys( array_filter( Fields::registry(), $test ) );
if ( '' !== $extra ) {
$types[] = $extra;
}
return implode( ' ', $types );
}
private function render_edit( string $key ): void {
$field = 'new' === $key ? null : Fields::get( $key );
$draft = get_transient( 'checkoutline_draft_' . get_current_user_id() );
if ( is_array( $draft ) ) {
delete_transient( 'checkoutline_draft_' . get_current_user_id() );
}
$value = is_array( $draft ) ? $draft : ( $field ? $field : array(
'key' => '',
'label' => '',
'location' => 'order',
'type' => 'text',
'required' => false,
'options' => array(),
'enabled' => true,
) );
$types = Fields::types();
$unknown = isset( $types[ $value['type'] ] ) ? '' : (string) $value['type'];
$box = 'checkbox' === Fields::block_type( $value['type'] ) && '' === $unknown;
?>
$field ) {
$sorted[ $key ]['enabled'] = in_array( $key, $enabled, true );
}
Fields::save( $sorted );
if ( $core ) {
CoreFields::save( $core );
}
wp_safe_redirect( self::url( array( 'message' => 'saved' ) ) );
exit;
}
/** Adds or updates one field. */
public function save_field(): void {
self::check( 'checkoutline_field' );
// phpcs:disable WordPress.Security.NonceVerification.Missing -- checked above.
$original = isset( $_POST['original'] ) ? sanitize_key( wp_unslash( (string) $_POST['original'] ) ) : '';
$label = isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['label'] ) ) : '';
$key = isset( $_POST['key'] ) ? Fields::key_from( sanitize_text_field( wp_unslash( (string) $_POST['key'] ) ) ) : '';
$type = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( (string) $_POST['type'] ) ) : 'text';
$text = static fn( string $name ) => isset( $_POST[ $name ] ) ? sanitize_text_field( wp_unslash( (string) $_POST[ $name ] ) ) : '';
$fields = Fields::all();
$stored = '' !== $original && isset( $fields[ $original ] ) ? $fields[ $original ] : array();
if ( ! Fields::type( $type ) && $type !== ( $stored['type'] ?? '' ) ) {
$type = 'text'; // Only a registered type, or the one the field already has.
}
$field = array(
'key' => '' !== $original ? $original : ( '' !== $key ? $key : Fields::key_from( $label ) ),
'label' => $label,
'location' => isset( $_POST['location'] ) ? sanitize_key( wp_unslash( (string) $_POST['location'] ) ) : 'order',
'type' => $type,
'required' => ! empty( $_POST['required'] ),
'enabled' => ! empty( $_POST['enabled'] ),
'options' => Fields::parse_options( isset( $_POST['options'] ) ? sanitize_textarea_field( wp_unslash( (string) $_POST['options'] ) ) : '' ),
'placeholder' => $text( 'placeholder' ),
'default' => 'checkbox' === Fields::block_type( $type ) ? ( empty( $_POST['default_checked'] ) ? '' : '1' ) : $text( 'default' ),
);
// phpcs:enable
if ( '' === $original && 'new' === $field['key'] ) {
$field['key'] = 'new_field'; // "new" is the Add screen's address (?edit=new).
}
// Settings this screen doesn't show (an inactive add-on's) stay as they are.
$field += array_diff_key( $stored, array_flip( Fields::OWN ) );
/**
* A field posted from its settings screen, before it's checked and saved: an add-on adds
* its own settings from $_POST (the nonce and capability are already checked).
*
* @param array $field The field from the posted form.
*/
$field = (array) apply_filters( 'checkoutline_posted_field', $field );
$error = '';
if ( '' === $label ) {
$error = 'label';
} elseif ( '' === $original && ( '' === $field['key'] || isset( $fields[ $field['key'] ] ) ) ) {
$error = 'key';
} elseif ( Fields::has_options( $field['type'] ) && ! $field['options'] ) {
$error = 'options';
} else {
/**
* Why a posted field can't be saved, as a message code ('' when it can). The field
* goes back to its screen with the notice for that code (`checkoutline_admin_messages`).
*
* @param string $error Default ''.
* @param array $field The posted field.
*/
$error = sanitize_key( (string) apply_filters( 'checkoutline_field_error', '', $field ) );
}
if ( $error ) {
set_transient( 'checkoutline_draft_' . get_current_user_id(), $field, MINUTE_IN_SECONDS * 10 );
wp_safe_redirect(
self::url(
array(
'edit' => '' !== $original ? $original : 'new',
'message' => $error,
)
)
);
exit;
}
$fields[ $field['key'] ] = $field; // An edit keeps its place; a new field goes last.
Fields::save( $fields );
wp_safe_redirect( self::url( array( 'message' => '' !== $original ? 'updated' : 'added' ) ) );
exit;
}
public function delete_field(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- checked next.
$key = isset( $_GET['key'] ) ? sanitize_key( wp_unslash( (string) $_GET['key'] ) ) : '';
self::check( 'checkoutline_delete_' . $key );
$fields = Fields::all();
unset( $fields[ $key ] );
Fields::save( $fields );
wp_safe_redirect( self::url( array( 'message' => 'deleted' ) ) );
exit;
}
}