`: the query both service links carry. */ function causefolk_embed_handoff_query(): string { $return = admin_url( 'admin.php?page=' . CAUSEFOLK_EMBED_MENU_SLUG ); return 'ref=wordpress-plugin&return=' . rawurlencode( $return ); } /** * A slug handed to the settings page in its query (`?causefolk_slug=…`, the way the service can * send a new organization back to this site): valid and lowercased, or '' for anything else. * It only prefills the field; nothing is saved until the form is submitted. */ function causefolk_embed_prefill_slug( $value ): string { $slug = strtolower( trim( is_string( $value ) ? $value : '' ) ); return preg_match( CAUSEFOLK_EMBED_SLUG_PATTERN, $slug ) ? $slug : ''; } /** * A supported language code (`en`, `es`), lowercased and trimmed, or '' for anything else (the * embed then follows the organization's volunteer language). */ function causefolk_embed_language( $value ): string { $lang = strtolower( trim( is_string( $value ) ? $value : '' ) ); return in_array( $lang, CAUSEFOLK_EMBED_LANGUAGES, true ) ? $lang : ''; } /* --------------------------------------------------------------------------------------------- * Renderer (shared by the shortcode and the block) * ------------------------------------------------------------------------------------------- */ /** * The embed `'; } /* --------------------------------------------------------------------------------------------- * Shortcode and block * ------------------------------------------------------------------------------------------- */ /** [causefolk_embed opportunity="" accent="" lang=""] */ function causefolk_embed_shortcode( $atts ): string { $atts = shortcode_atts( array( 'opportunity' => '', 'accent' => '', 'lang' => '', ), $atts, 'causefolk_embed' ); return causefolk_embed_render_snippet( $atts ); } add_shortcode( 'causefolk_embed', 'causefolk_embed_shortcode' ); /** Server-side render of the `causefolk-embed/embed` block. */ function causefolk_embed_render_block( $attributes ): string { return causefolk_embed_render_snippet( is_array( $attributes ) ? $attributes : array() ); } function causefolk_embed_register_block(): void { wp_register_script( 'causefolk_embed_block_editor', plugins_url( 'block.js', __FILE__ ), array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ), CAUSEFOLK_EMBED_VERSION, true ); wp_add_inline_script( 'causefolk_embed_block_editor', 'window.causefolkEmbed = ' . wp_json_encode( array( 'configured' => '' !== causefolk_embed_org_slug(), 'settingsUrl' => admin_url( 'admin.php?page=' . CAUSEFOLK_EMBED_MENU_SLUG ), 'signupUrl' => causefolk_embed_signup_url(), 'connectUrl' => causefolk_embed_connect_url(), ) ) . ';', 'before' ); register_block_type( __DIR__ . '/block.json', array( 'render_callback' => 'causefolk_embed_render_block' ) ); } add_action( 'init', 'causefolk_embed_register_block' ); /* --------------------------------------------------------------------------------------------- * Settings * ------------------------------------------------------------------------------------------- */ /** Lowercased slug matching the app's slug format; empty clears it; anything else keeps the previous value. */ function causefolk_embed_sanitize_org_slug( $value ): string { $slug = strtolower( trim( is_string( $value ) ? $value : '' ) ); if ( '' === $slug || preg_match( CAUSEFOLK_EMBED_SLUG_PATTERN, $slug ) ) { return $slug; } add_settings_error( 'causefolk_embed_org_slug', 'causefolk_embed_invalid_slug', __( 'The organization slug is lowercase letters, digits and hyphens, at most 63 characters, and cannot start or end with a hyphen.', 'causefolk-embed' ) ); return (string) get_option( 'causefolk_embed_org_slug', '' ); } /** A hostname with optional port: no scheme, no path. Empty resets to the default; invalid input keeps the previous value. */ function causefolk_embed_sanitize_root_domain( $value ): string { $host = strtolower( trim( is_string( $value ) ? $value : '' ) ); if ( '' === $host ) { return CAUSEFOLK_EMBED_DEFAULT_ROOT_DOMAIN; } if ( strlen( $host ) <= 260 && preg_match( CAUSEFOLK_EMBED_HOST_PATTERN, $host ) ) { return $host; } add_settings_error( 'causefolk_embed_root_domain', 'causefolk_embed_invalid_root_domain', __( 'The root domain must be a host name such as causefolk.com, without https:// or a path.', 'causefolk-embed' ) ); return (string) get_option( 'causefolk_embed_root_domain', CAUSEFOLK_EMBED_DEFAULT_ROOT_DOMAIN ); } /** One of `auto`, `http`, `https`; anything else falls back to `auto`. */ function causefolk_embed_sanitize_root_scheme( $value ): string { $value = strtolower( trim( (string) $value ) ); return in_array( $value, array( 'auto', 'http', 'https' ), true ) ? $value : 'auto'; } function causefolk_embed_register_settings(): void { register_setting( 'causefolk_embed', 'causefolk_embed_org_slug', array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'causefolk_embed_sanitize_org_slug', ) ); register_setting( 'causefolk_embed', 'causefolk_embed_root_domain', array( 'type' => 'string', 'default' => CAUSEFOLK_EMBED_DEFAULT_ROOT_DOMAIN, 'sanitize_callback' => 'causefolk_embed_sanitize_root_domain', ) ); register_setting( 'causefolk_embed', 'causefolk_embed_lang', array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'causefolk_embed_language', ) ); register_setting( 'causefolk_embed', 'causefolk_embed_root_scheme', array( 'type' => 'string', 'default' => 'auto', 'sanitize_callback' => 'causefolk_embed_sanitize_root_scheme', ) ); } add_action( 'admin_init', 'causefolk_embed_register_settings' ); /* --------------------------------------------------------------------------------------------- * Admin menu and page * ------------------------------------------------------------------------------------------- */ /** * The Causefolk mark as a data URI for the admin menu. Fill-only shapes (the stroke is drawn as a * rounded rectangle), so WordPress can recolor it to the admin color scheme. */ function causefolk_embed_menu_icon(): string { $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); } function causefolk_embed_add_menu(): void { add_menu_page( __( 'Causefolk', 'causefolk-embed' ), __( 'Causefolk', 'causefolk-embed' ), 'manage_options', CAUSEFOLK_EMBED_MENU_SLUG, 'causefolk_embed_render_settings_page', causefolk_embed_menu_icon(), '58.7' ); } add_action( 'admin_menu', 'causefolk_embed_add_menu' ); /** A "Settings" link on the Plugins list, next to Deactivate. */ function causefolk_embed_action_links( $links ): array { if ( ! is_array( $links ) ) { $links = array(); } $settings = '' . esc_html__( 'Settings', 'causefolk-embed' ) . ''; array_unshift( $links, $settings ); return $links; } add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'causefolk_embed_action_links' ); /** The page's styles, registered as an inline stylesheet on its own screen only. */ function causefolk_embed_admin_styles( string $hook_suffix ): void { if ( 'toplevel_page_' . CAUSEFOLK_EMBED_MENU_SLUG !== $hook_suffix ) { return; } wp_register_style( 'causefolk-embed-admin', false, array(), CAUSEFOLK_EMBED_VERSION ); wp_enqueue_style( 'causefolk-embed-admin' ); wp_add_inline_style( 'causefolk-embed-admin', '.causefolk-embed-layout{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:24px;align-items:start;margin-top:12px}' . '@media (max-width:1100px){.causefolk-embed-layout{grid-template-columns:minmax(0,1fr)}}' . '.causefolk-embed-card{background:#fff;border:1px solid #dcdde0;border-radius:12px;padding:20px 22px;margin-bottom:16px}' . '.causefolk-embed-panel{background:#f5f4fd;border:1px solid #dcddf0;border-radius:12px;padding:22px 24px}' . '.causefolk-embed-panel h2{margin:0 0 6px;font-size:20px;line-height:1.2;color:#15172b}' . '.causefolk-embed-panel h2 em{font-style:italic;font-weight:400}' . '.causefolk-embed-panel p{color:#3e4260;margin:0 0 12px}' . '.causefolk-embed-panel .button-primary{background:#ff6a3d;border-color:#ff6a3d;color:#15172b;text-shadow:none;box-shadow:none;font-weight:600}' . '.causefolk-embed-panel .button-primary:hover,.causefolk-embed-panel .button-primary:focus{background:#ffb089;border-color:#ffb089;color:#15172b}' . '.causefolk-embed-panel .button{margin:4px 8px 4px 0}' . '.causefolk-embed-mark{display:inline-block;width:28px;height:28px;vertical-align:middle;margin-right:8px}' . '.causefolk-embed-links{margin:12px 0 0;padding:0;list-style:none}' . '.causefolk-embed-links li{margin:0 0 6px}' . '.causefolk-embed-code{display:block;margin:6px 0 10px;padding:8px 10px;background:#f6f7f7;border-radius:6px}' ); } add_action( 'admin_enqueue_scripts', 'causefolk_embed_admin_styles' ); /* --------------------------------------------------------------------------------------------- * Getting people to the service: the activation redirect and the "not connected" notice * ------------------------------------------------------------------------------------------- */ /** On activation, remember to open the plugin's page on the next admin request (single activations only). */ function causefolk_embed_activate( $network_wide = false ): void { if ( $network_wide ) { return; } set_transient( 'causefolk_embed_activation_redirect', get_current_user_id(), 60 ); } register_activation_hook( __FILE__, 'causefolk_embed_activate' ); /** Sends the activating administrator to the plugin's page once, never during a bulk activation. */ function causefolk_embed_maybe_redirect_after_activation(): void { $user_id = get_transient( 'causefolk_embed_activation_redirect' ); if ( false === $user_id ) { return; } delete_transient( 'causefolk_embed_activation_redirect' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only to detect a bulk activation. if ( isset( $_GET['activate-multi'] ) || (int) $user_id !== get_current_user_id() || ! current_user_can( 'manage_options' ) || wp_doing_ajax() ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=' . CAUSEFOLK_EMBED_MENU_SLUG ) ); exit; } add_action( 'admin_init', 'causefolk_embed_maybe_redirect_after_activation' ); /** Records the current user's dismissal of the connect notice (a nonce-checked GET from the notice itself). */ function causefolk_embed_handle_notice_dismiss(): void { if ( ! isset( $_GET['causefolk_embed_dismiss'] ) || ! current_user_can( 'manage_options' ) ) { return; } check_admin_referer( 'causefolk_embed_dismiss' ); update_user_meta( get_current_user_id(), 'causefolk_embed_notice_dismissed', '1' ); wp_safe_redirect( remove_query_arg( array( 'causefolk_embed_dismiss', '_wpnonce' ) ) ); exit; } add_action( 'admin_init', 'causefolk_embed_handle_notice_dismiss' ); /** * Whether the "not connected" notice shows on this screen: only to administrators, only while no * slug is saved, only on the dashboard and the plugins list, never on the plugin's own page, and * not after the user dismissed it. Pure, so the check script can pin the rules. */ function causefolk_embed_notice_wanted( string $screen_id, bool $configured, bool $can_manage, bool $dismissed ): bool { return $can_manage && ! $configured && ! $dismissed && in_array( $screen_id, array( 'dashboard', 'plugins' ), true ); } function causefolk_embed_connect_notice(): void { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; $wanted = causefolk_embed_notice_wanted( $screen ? (string) $screen->id : '', '' !== causefolk_embed_org_slug(), current_user_can( 'manage_options' ), '1' === (string) get_user_meta( get_current_user_id(), 'causefolk_embed_notice_dismissed', true ) ); if ( ! $wanted ) { return; } $settings_url = admin_url( 'admin.php?page=' . CAUSEFOLK_EMBED_MENU_SLUG ); $dismiss_url = wp_nonce_url( add_query_arg( 'causefolk_embed_dismiss', '1' ), 'causefolk_embed_dismiss' ); ?>

'; } function causefolk_embed_render_settings_page(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $slug = causefolk_embed_org_slug(); $signup_url = causefolk_embed_signup_url(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a prefill for the form field only; saving still goes through options.php and its nonce. $prefill = '' === $slug ? causefolk_embed_prefill_slug( sanitize_text_field( wp_unslash( $_GET['causefolk_slug'] ?? '' ) ) ) : ''; $dashboard_url = '' === $slug ? '' : causefolk_embed_service_url( '/orgs/' . rawurlencode( $slug ), 'app' ); $portal_url = '' === $slug ? '' : causefolk_embed_service_url( '/', $slug ); $blog_url = causefolk_embed_service_url( '/blog' ); $lang_value = causefolk_embed_language( get_option( 'causefolk_embed_lang', '' ) ); $scheme_value = (string) get_option( 'causefolk_embed_root_scheme', 'auto' ); ?>

[causefolk_embed]

[causefolk_embed opportunity="00000000-0000-0000-0000-000000000000" accent="#ff6a3d" lang="es"]