'Brevo', * 'version' => '1.0.0', * 'premium' => true, // a paid add-on: only the word on its card * 'docs' => 'https://…', * 'settings' => 'integrations', // the Settings tab its options are in * ]; * * Its card on the Add-ons page (description, category, tags, icon, color, url) comes from the catalog * (AddonsPage::catalog(), remaily_addons_catalog), which shows it before it's installed too. An add-on that isn't * in the catalog passes them here: 'description', 'category' (under its name), 'tags', 'icon' (its own logo, never * a core icon), 'color' (the logo's background). * * return $addons; * } ); * * // Runs while the add-on is on and ready: the rest goes through core's hooks * add_action( 'remaily_addon_init_brevo', [ Brevo::class, 'init' ] ); * * Its options: a section of a Settings tab through remaily_settings_sections, marked 'addon' => slug (a tab it * shares with others, made by whichever comes first), read with the remaily_setting filter. Its tab in the form * editor: a Controls tree through remaily_form_tabs, drawn by the editor, its values in config['addons'][id] with * the tree's defaults and cleaning: * * add_filter( 'remaily_form_tabs', fn( $tabs ) => $tabs + [ 'brevo' => [ 'label' => 'Brevo', 'addon' => 'brevo', * 'icon' => '', 'fields' => [ … ] ] ] ); * * (A tab of its own in JavaScript: remaily.tabs with addon: slug.) * * Core knows nothing of licenses or payments: whatever an add-on needs of its own (a license, an account) it keeps, * checks and shows itself, and tells core only what to do with it: * * 'ready' => false, // don't start it yet (on the Add-ons page its window still opens) * 'readonly' => 'Why not', // its options and form tab show this note and can't be changed * 'status' => [ 'Label', 'tone' ], // its state on its card (tones: success, warning, muted, brand) * 'panel' => callable, // prints its own part at the top of its window, such as its license form * * Its own forms post to its own handlers and come back with do_action( 'remaily_addon_notice', $slug, $type, $text ). * * One plugin can add several. Core never needs any of them: an add-on that is gone, off or not ready doesn't start, * and its saved values wait for it; read-only ones keep theirs too. */ class Addons { // [ slug => [ 'enabled' => bool ] ] const OPTION = 'remaily_addons'; /** * Add-ons * * @var array [ slug => args ] */ private static $addons = []; /** * The form editor's tabs of the add-ons (and of anyone else), built once * * @var array|null [ id => [ label, addon, icon, fields ] ] */ private static $tabs = null; /** * Init: collect the add-ons, start those that run */ public static function init() { foreach ( (array) apply_filters( 'remaily_addons', [] ) as $slug => $args ) { $slug = sanitize_key( (string) $slug ); if ( '' !== $slug && is_array( $args ) ) { self::$addons[ $slug ] = self::normalize( $slug, $args ); } } foreach ( array_keys( self::$addons ) as $slug ) { if ( self::runs( $slug ) ) { do_action( "remaily_addon_init_{$slug}" ); } } AddonsPage::init(); } /** * An add-on's arguments with their defaults * * @param string $slug Slug * @param array $args Arguments * * @return array */ private static function normalize( $slug, array $args ) { $args = wp_parse_args( $args, [ 'name' => $slug, 'description' => '', 'version' => '', 'premium' => false, 'ready' => true, 'readonly' => '', 'status' => [], 'panel' => null, 'category' => '', 'tags' => [], 'icon' => '', // The background of its icon on the Add-ons page, such as #e7f6ef 'color' => '', 'docs' => '', 'url' => '', 'settings' => '', ] ); $args['premium'] = (bool) $args['premium']; $args['ready'] = (bool) $args['ready']; $args['readonly'] = (string) $args['readonly']; $args['status'] = array_values( (array) $args['status'] ); $args['panel'] = is_callable( $args['panel'] ) ? $args['panel'] : null; return $args; } /** * Add-ons * * @return array [ slug => args ] */ public static function all() { return self::$addons; } /** * Whether the add-on is on (an installed one is, until turned off) * * @param string $slug Slug * * @return bool */ public static function enabled( $slug ) { $saved = (array) ( ( (array) get_option( self::OPTION, [] ) )[ $slug ] ?? [] ); return isset( self::$addons[ $slug ] ) && (bool) ( $saved['enabled'] ?? true ); } /** * Whether the add-on runs: on, and ready by its own word * * @param string $slug Slug * * @return bool */ public static function runs( $slug ) { return self::enabled( $slug ) && self::$addons[ $slug ]['ready']; } /** * Why its options can't be changed now, by its own word; '' while they can * * @param string $slug Slug * * @return string */ public static function readonly( $slug ) { return (string) ( self::$addons[ $slug ]['readonly'] ?? '' ); } /** * Tabs of the form editor drawn from Controls trees (remaily_form_tabs), of add-ons that run * * @return array [ id => [ 'label', 'addon', 'icon', 'fields' ] ] */ public static function form_tabs() { if ( null === self::$tabs ) { self::$tabs = []; foreach ( (array) apply_filters( 'remaily_form_tabs', [] ) as $id => $tab ) { $id = sanitize_key( (string) $id ); $addon = (string) ( $tab['addon'] ?? '' ); if ( '' !== $id && !empty( $tab['fields'] ) && ( '' === $addon || self::runs( $addon ) ) ) { self::$tabs[ $id ] = [ 'label' => (string) ( $tab['label'] ?? $id ), 'addon' => $addon, 'icon' => (string) ( $tab['icon'] ?? '' ), 'fields' => (array) $tab['fields'] ]; } } } return self::$tabs; } /** * Values of those tabs in a new form * * @return array [ id => values ] */ public static function form_defaults() { return array_map( static fn( $tab ) => Controls::defaults( $tab['fields'] ), self::form_tabs() ); } /** * Values of those tabs, cleaned by their trees (Config::save() keeps the stored ones of tabs gone or read-only) * * @param array $values [ id => values ] * * @return array */ public static function form_sanitize( array $values ) { $clean = []; foreach ( self::form_tabs() as $id => $tab ) { $clean[ $id ] = Controls::sanitize( $tab['fields'], $values[ $id ] ?? [] ); } return $clean; } /** * Those tabs for the editor * * @return array [ { id, label, addon, icon, fields } ] */ public static function form_tabs_data() { $data = []; foreach ( self::form_tabs() as $id => $tab ) { $data[] = [ 'id' => $id, 'fields' => Controls::export( $tab['fields'] ) ] + $tab; } return $data; } /** * Settings sections with each read-only add-on's nodes marked 'readonly' with its note (shown in place of their * controls, saved as they were) * * @param array $fields Tree * * @return array */ public static function mark_readonly( array $fields ) { foreach ( $fields as $key => $node ) { if ( !is_array( $node ) ) { continue; } if ( !empty( $node['addon'] ) && '' !== self::readonly( (string) $node['addon'] ) ) { $fields[ $key ]['readonly'] = self::readonly( (string) $node['addon'] ); } elseif ( isset( $node['fields'] ) ) { $fields[ $key ]['fields'] = self::mark_readonly( (array) $node['fields'] ); } } return $fields; } /** * Add-ons for the editor: [ slug => [ name, readonly ] ] of those that run, readonly being the note or '' * * @return array */ public static function editor_data() { $data = []; foreach ( self::$addons as $slug => $addon ) { if ( self::runs( $slug ) ) { $data[ $slug ] = [ 'name' => $addon['name'], 'readonly' => self::readonly( $slug ) ]; } } return $data; } }