'', 'email_only' => '', 'title' => '' ), (array) $atts, 'dashbread_login' ); self::style(); $to = '' !== $a['redirect'] ? self::target( (string) $a['redirect'] ) : Settings::app_url(); if ( is_user_logged_in() ) { $u = wp_get_current_user(); return '

' . esc_html( sprintf( /* translators: %s: display name */ __( 'You are signed in as %s.', 'dashbread' ), $u->display_name ) ) . '

' . esc_html__( 'Open the app', 'dashbread' ) . ' ' . esc_html__( 'Sign out', 'dashbread' ) . '

'; } $form = wp_login_form( array( 'echo' => false, 'redirect' => $to, 'form_id' => 'dashbread-login-' . wp_rand( 100, 999 ), 'label_username' => '1' === (string) $a['email_only'] ? __( 'Email address', 'dashbread' ) : __( 'Email or username', 'dashbread' ), 'label_log_in' => __( 'Sign in', 'dashbread' ), 'remember' => true, ) ); // The Dashbread nonce lets a failed sign-in come back to the app's login screen with the redirect kept. $form = str_replace( '', \Dashbread\Auth\Auth::nonce_field( 'login' ) . '', $form ); $links = '

' . esc_html__( 'Forgot your password?', 'dashbread' ) . ''; if ( get_option( 'users_can_register' ) ) { $links .= ' · ' . esc_html__( 'Create an account', 'dashbread' ) . ''; } $links .= '

'; $title = '' !== $a['title'] ? '

' . esc_html( (string) $a['title'] ) . '

' : ''; return '
' . $title . self::login_form_kses( $form ) . $links . '
'; } /** [dashbread_app_link label="Open the app" page="profile" label_out="Sign in" button="1"] */ public function app_link( $atts ): string { $a = shortcode_atts( array( 'label' => __( 'Open the app', 'dashbread' ), 'page' => '', 'label_out' => __( 'Sign in', 'dashbread' ), 'button' => '1' ), (array) $atts, 'dashbread_app_link' ); $to = Settings::app_url( sanitize_text_field( (string) $a['page'] ) ); $cls = '1' === (string) $a['button'] ? 'sui-sc-btn' : 'sui-sc-link'; if ( '1' === (string) $a['button'] ) { self::style(); } if ( ! is_user_logged_in() ) { return '' . esc_html( (string) $a['label_out'] ) . ''; } return '' . esc_html( (string) $a['label'] ) . ''; } /** [dashbread_logged_in]…[/dashbread_logged_in] */ public function logged_in( $atts, $content = '' ): string { return is_user_logged_in() ? self::enclosed( $content ) : ''; } /** [dashbread_logged_out]…[/dashbread_logged_out] */ public function logged_out( $atts, $content = '' ): string { return is_user_logged_in() ? '' : self::enclosed( $content ); } /** * [dashbread_protect roles="subscriber,editor" message=""]…[/dashbread_protect] — a members-only gate: * signed-out visitors get a sign-in prompt; signed-in people outside the roles get the message. */ public function protect( $atts, $content = '' ): string { $a = shortcode_atts( array( 'roles' => '', 'message' => '' ), (array) $atts, 'dashbread_protect' ); if ( is_user_logged_in() && self::role_match( (string) $a['roles'] ) ) { return self::enclosed( $content ); } self::style(); $here = get_permalink() ?: home_url( '/' ); if ( ! is_user_logged_in() ) { $msg = '' !== $a['message'] ? (string) $a['message'] : __( 'This content is for members. Sign in to see it.', 'dashbread' ); return '

' . esc_html( $msg ) . '

' . esc_html__( 'Sign in', 'dashbread' ) . '' . ( get_option( 'users_can_register' ) ? ' ' . esc_html__( 'Create an account', 'dashbread' ) . '' : '' ) . '

'; } $msg = '' !== $a['message'] ? (string) $a['message'] : __( 'Your account does not have access to this content.', 'dashbread' ); return '

' . esc_html( $msg ) . '

'; } /** [dashbread_role is="editor,author" not="subscriber"]…[/dashbread_role] — shows nothing to anyone else. */ public function role( $atts, $content = '' ): string { $a = shortcode_atts( array( 'is' => '', 'not' => '' ), (array) $atts, 'dashbread_role' ); if ( ! is_user_logged_in() ) { return ''; } if ( '' !== $a['is'] && ! self::role_match( (string) $a['is'] ) ) { return ''; } if ( '' !== $a['not'] && self::role_match( (string) $a['not'] ) ) { return ''; } return self::enclosed( $content ); } /** [dashbread_show from="2026-10-01" until="2026-12-31 17:00"]…[/dashbread_show] (alias [dashbread_expire]) — site time zone. */ public function show( $atts, $content = '' ): string { $a = shortcode_atts( array( 'from' => '', 'until' => '' ), (array) $atts, 'dashbread_show' ); $now = time(); $tz = wp_timezone(); try { if ( '' !== $a['from'] && $now < ( new \DateTimeImmutable( (string) $a['from'], $tz ) )->getTimestamp() ) { return ''; } if ( '' !== $a['until'] ) { $until = new \DateTimeImmutable( (string) $a['until'], $tz ); if ( ! preg_match( '/\d{1,2}:\d{2}/', (string) $a['until'] ) ) { $until = $until->setTime( 23, 59, 59 ); // A bare date runs through the end of that day. } if ( $now > $until->getTimestamp() ) { return ''; } } } catch ( \Exception $e ) { return current_user_can( 'edit_posts' ) ? '' : ''; } return self::enclosed( $content ); } /** [dashbread_user field="display_name|first_name|last_name|email|username" fallback=""] — the signed-in person's own details. */ public function user( $atts ): string { $a = shortcode_atts( array( 'field' => 'display_name', 'fallback' => '' ), (array) $atts, 'dashbread_user' ); if ( ! is_user_logged_in() ) { return esc_html( (string) $a['fallback'] ); } $u = wp_get_current_user(); $map = array( 'display_name' => $u->display_name, 'first_name' => $u->first_name, 'last_name' => $u->last_name, 'email' => $u->user_email, 'username' => $u->user_login, ); $val = (string) ( $map[ sanitize_key( (string) $a['field'] ) ] ?? '' ); return esc_html( '' !== $val ? $val : (string) $a['fallback'] ); } /* --------------------------------------------------------------- helpers */ /** * Content between an enclosing tag's open and close: limited to post-safe HTML first, then any * nested shortcodes run (each escapes its own output). * * @param mixed $content */ private static function enclosed( $content ): string { return do_shortcode( wp_kses_post( (string) $content ) ); } /** wp_login_form() markup, escaped at output with the form elements it uses allowed. */ private static function login_form_kses( string $html ): string { $attrs = array( 'id' => true, 'class' => true, 'name' => true, 'type' => true, 'value' => true, 'for' => true, 'action' => true, 'method' => true, 'size' => true, 'autocomplete' => true, 'required' => true, 'checked' => true, 'placeholder' => true, 'aria-describedby' => true, 'spellcheck' => true, 'data-*' => true, 'aria-label' => true ); return wp_kses( $html, array( 'form' => $attrs, 'p' => $attrs, 'label' => $attrs, 'input' => $attrs, 'button' => $attrs, 'span' => $attrs, 'div' => $attrs, 'a' => $attrs + array( 'href' => true ) ) ); } /** Does the current user hold one of these roles? Empty list = any signed-in user. */ public static function role_match( string $roles ): bool { $want = array_filter( array_map( 'sanitize_key', explode( ',', $roles ) ) ); if ( ! $want ) { return is_user_logged_in(); } return (bool) array_intersect( $want, (array) wp_get_current_user()->roles ); } /** A redirect attribute: "profile" or "/app/profile" stays in the app; a full same-site URL is kept; anything else → the app. */ private static function target( string $to ): string { if ( preg_match( '#^https?://#i', $to ) ) { return wp_validate_redirect( $to, Settings::app_url() ); } $to = trim( $to, '/' ); $r = Settings::route(); if ( $to === $r || str_starts_with( $to, $r . '/' ) ) { $to = substr( $to, strlen( $r ) ); } return Settings::app_url( $to ); } /** * For tags that render app components (islands): inside the app they render; on a theme page a * signed-in member gets a link to open it in the app, and visitors get nothing. */ public static function app_only( string $label, string $page = '' ): string { if ( Router::is_app() ) { return ''; } if ( ! is_user_logged_in() ) { return ''; } self::style(); return '

' . esc_html( sprintf( /* translators: %s: feature name */ __( 'Open %s in the app →', 'dashbread' ), $label ) ) . '

'; } /* ------------------------------------------------------------- reference */ /** * Every Dashbread shortcode, grouped for the Help screen. Items: code, close (enclosing tags), note, * guide (knowledge-base slug the tag belongs to), feature (null = Core), available, requires. * * @return array> */ public static function reference(): array { $core = array( array( 'code' => '[dashbread_login]', 'note' => __( 'The sign-in form on any page. Signed-in people see "Open the app" and "Sign out" instead. Optional redirect="profile" (an app page or a same-site URL), email_only="1", title="".', 'dashbread' ), 'guide' => 'sign-in-and-registration' ), array( 'code' => '[dashbread_app_link]', 'note' => __( 'A button into the app — or a Sign in button for visitors, returning them to the same place. Optional label="", label_out="", page="profile", button="0" for a plain link.', 'dashbread' ), 'guide' => 'the-app-route' ), array( 'code' => '[dashbread_logged_in]', 'close' => '[/dashbread_logged_in]', 'note' => __( 'Show the enclosed content only to signed-in people.', 'dashbread' ), 'guide' => 'shortcodes' ), array( 'code' => '[dashbread_logged_out]', 'close' => '[/dashbread_logged_out]', 'note' => __( 'Show the enclosed content only to signed-out visitors.', 'dashbread' ), 'guide' => 'shortcodes' ), array( 'code' => '[dashbread_protect]', 'close' => '[/dashbread_protect]', 'note' => __( 'Members-only gate: visitors get a sign-in prompt instead of the content. Optional roles="subscriber,editor" and message="".', 'dashbread' ), 'guide' => 'shortcodes' ), array( 'code' => '[dashbread_role is="editor"]', 'close' => '[/dashbread_role]', 'note' => __( 'Show the enclosed content only to these roles (comma-separated). not="" hides it from roles instead. Everyone else sees nothing.', 'dashbread' ), 'guide' => 'shortcodes' ), array( 'code' => '[dashbread_show from="2026-10-01" until="2026-12-31"]', 'close' => '[/dashbread_show]', 'note' => __( 'Show content inside a date window (site time zone; a bare date runs to the end of that day). Alias [dashbread_expire].', 'dashbread' ), 'guide' => 'shortcodes' ), array( 'code' => '[dashbread_user field="first_name"]', 'note' => __( 'The signed-in person\'s own display_name, first_name, last_name, email or username. fallback="" for visitors.', 'dashbread' ), 'guide' => 'profile' ), ); $pro = array( array( 'code' => '[dashbread_has_plan plan="gold"]', 'close' => '[/dashbread_has_plan]', 'note' => __( 'Show content only to members with an active membership on these plans (slugs or ids, comma-separated). Leave plan="" for any plan.', 'dashbread' ), 'guide' => 'plans', 'feature' => 'plans' ), array( 'code' => '[dashbread_no_plan plan="gold"]', 'close' => '[/dashbread_no_plan]', 'note' => __( 'The opposite: everyone without that plan, including signed-out visitors — good for "Upgrade" prompts.', 'dashbread' ), 'guide' => 'plans', 'feature' => 'plans' ), array( 'code' => '[dashbread_plans]', 'note' => __( 'Plan cards (price, features, Get button) for a pricing page. Optional plans="gold,silver" and columns="3". Members see "Your plan" on the one they have.', 'dashbread' ), 'guide' => 'plans', 'feature' => 'plans' ), array( 'code' => '[dashbread_field key="company_size"]', 'note' => __( 'The signed-in member\'s own answer to a custom field (choices show their labels, name/address fields join their parts). fallback="".', 'dashbread' ), 'guide' => 'field_builder', 'feature' => 'field_builder' ), array( 'code' => '[dashbread_company field="name"]', 'note' => __( 'The signed-in member\'s primary company: name, city, state, website, phone or email. fallback="".', 'dashbread' ), 'guide' => 'companies', 'feature' => 'companies' ), ); $pmas = array( array( 'code' => '[dashbread_graph source="members_joined"]', 'note' => __( 'Data & Graphs — a chart inside a hosted page. Optional kind="stat|line|bar|pie", range="7|30|90|365", ranges="1", title="". Site sources show only to staff; "My …" sources to each member.', 'dashbread' ), 'guide' => 'data-graphs', 'feature' => 'data_graphs' ), array( 'code' => '[dashbread_tasks]', 'note' => __( 'Tasks — the member\'s open tasks with checkboxes, inside a hosted page. Optional limit="5", add="1" to let them add their own, title="".', 'dashbread' ), 'guide' => 'tasks', 'feature' => 'tasks' ), array( 'code' => '[dashbread_offers]', 'note' => __( 'Offers — the live offers aimed at the signed-in member, as cards, inside a hosted page (every offer, whatever its dashboard or flyout setting). Optional limit="3" (up to 6), title="".', 'dashbread' ), 'guide' => 'offers', 'feature' => 'offers' ), ); $pro_name = Brand::name() . ' Pro'; $labels = array( 'plans' => $pro_name, 'field_builder' => $pro_name, 'companies' => $pro_name, 'data_graphs' => 'Data & Graphs', 'tasks' => 'Tasks', 'offers' => 'Offers' ); $sections = array( array( 'key' => 'core', 'label' => __( 'Core (free)', 'dashbread' ), 'note' => __( 'Ship with Dashbread — work on theme pages and on hosted pages inside the app.', 'dashbread' ), 'items' => $core ), array( 'key' => 'pro', 'label' => __( 'Pro', 'dashbread' ), 'note' => __( 'Unlock with a Dashbread Pro license and read from the Pro modules.', 'dashbread' ), 'items' => $pro ), array( 'key' => 'pmas', 'label' => __( 'Pro Module Add-ons', 'dashbread' ), 'note' => __( 'Each unlocks with its add-on. They render app components, so place them on a hosted page (on a theme page, members get a link into the app).', 'dashbread' ), 'items' => $pmas ), ); /** * Add a section (or items) to the Help screen's shortcode reference. * * @param array> $sections {key,label,note,items[{code,close?,note,guide?,feature?}]} */ $sections = (array) apply_filters( 'dashbread_shortcode_reference', $sections ); foreach ( $sections as &$s ) { foreach ( $s['items'] as &$i ) { $f = $i['feature'] ?? null; $i['close'] = $i['close'] ?? ''; $i['guide'] = $i['guide'] ?? ''; $i['available'] = null === $f || Features::has( (string) $f ); $i['requires'] = null === $f ? '' : ( $labels[ $f ] ?? '' ); unset( $i['feature'] ); } unset( $i ); } unset( $s ); return array_values( $sections ); } }