mirror of
https://github.com/we-promise/sure.git
synced 2026-05-24 21:14:56 +00:00
* feat: beta features toggle + Beta pill primitive Adds the infrastructure for self-service beta opt-in. No call sites yet: this PR is meant to land first so feature PRs (Goals, etc.) can ship behind the gate incrementally. User opts in via a single toggle at the bottom of Settings → Preferences. The flag persists in the existing `users.preferences` JSONB column under `beta_features_enabled` — same shape as `dashboard_two_column` and `show_split_grouped`, so no migration is needed. Controllers gate a beta feature by adding `before_action :require_beta_features!` from the new `BetaGateable` concern (included in ApplicationController). Views use the `beta_features_enabled?` helper to hide / show nav items, banners, etc. Logged-out callers always return false. Ships `DS::BetaPill`, a small inline marker for tagging features as Beta / Canary in nav, headers, and lists. Five tones (violet by default, indigo, fuchsia, amber, gray) map to existing Sure color tokens — no raw hex. Three styles (soft / filled / outline) and two sizes (sm / md) cover the surfaces in the design handoff. The `dot_only:` mode renders just the colored dot for use on a collapsed sidebar. * review: rename to DS::Pill, fix CR/Codex nits, add tests CodeRabbit + Codex review feedback: - Rename DS::BetaPill → DS::Pill. The component was already generic in shape (tones, styles, sizes); the name was misleading scope. "Beta" becomes the default label (still i18n-driven). Goals' StatusPill can later refactor onto this primitive without a third pill. - Localize the default pill label via i18n (`ds.pill.default_label`) instead of hard-coding English. - Add role="img" to the dot-only span so the aria-label is consistently exposed to assistive tech. - Wrap the Preferences toggle row in <label for="…"> so the title and description become an honest click target for the toggle (matches the cursor-pointer affordance). - Drop arbitrary Tailwind values (py-[3px], gap-[5px], tracking-[…]) in favor of scale tokens. text-[10/11px] stays because the pill is intentionally sub-12px (Sure's smallest scale token is text-xs / 12px) to read as a marker, not a label. - Add User#beta_features_enabled? predicate tests covering default-off, explicit-true, and non-boolean truthy values. Won't fix: - Palette refs (`--color-violet-*` etc.). Sure has no semantic Beta/ Canary tokens; introducing them in this PR would be a design-system change beyond the scope. The component centralizes palette use in one `palette` method, matching the existing pattern in Goals::StatusPillComponent. * review: consistent title fallback in full-pill branch * docs: how to gate a feature behind the beta toggle * docs: unwrap doc lines to match existing style * chore(preview): run Cloudflare PR previews on basic instances (#1831) * fix(preview): use Rails health endpoint for container ping (#1823) * fix(preview): use Rails health endpoint for container ping * fix(preview): point container ping to localhost/up --------- Co-authored-by: Sure Admin (bot) <sure-admin@splashblot.com>
107 lines
2.8 KiB
Ruby
107 lines
2.8 KiB
Ruby
class ApplicationController < ActionController::Base
|
|
include RestoreLayoutPreferences, Onboardable, Localize, AutoSync, Authentication, Invitable,
|
|
SelfHostable, StoreLocation, Impersonatable, Breadcrumbable,
|
|
FeatureGuardable, Notifiable, SafePagination, AccountAuthorizable,
|
|
BetaGateable
|
|
include Pundit::Authorization
|
|
|
|
include Pagy::Backend
|
|
|
|
# Pundit uses current_user by default, but this app uses Current.user
|
|
def pundit_user
|
|
Current.user
|
|
end
|
|
|
|
before_action :detect_os
|
|
before_action :set_default_chat
|
|
before_action :set_active_storage_url_options
|
|
|
|
helper_method :demo_config, :demo_host_match?, :show_demo_warning?
|
|
|
|
private
|
|
def accept_pending_invitation_for(user)
|
|
return false if user.blank?
|
|
|
|
token = session[:pending_invitation_token]
|
|
return false if token.blank?
|
|
|
|
invitation = Invitation.pending.find_by(token: token.to_s)
|
|
return false unless invitation
|
|
return false unless invitation.accept_for(user)
|
|
|
|
session.delete(:pending_invitation_token)
|
|
true
|
|
end
|
|
|
|
def store_pending_invitation_if_valid
|
|
token = params[:invitation].to_s.presence
|
|
return if token.blank?
|
|
|
|
invitation = Invitation.pending.find_by(token: token)
|
|
session[:pending_invitation_token] = token if invitation
|
|
end
|
|
|
|
def require_admin!
|
|
return if Current.user&.admin?
|
|
|
|
respond_to do |format|
|
|
format.html { redirect_to accounts_path, alert: t("shared.require_admin") }
|
|
format.turbo_stream { head :forbidden }
|
|
format.json { head :forbidden }
|
|
format.any { head :forbidden }
|
|
end
|
|
end
|
|
|
|
def detect_os
|
|
user_agent = request.user_agent
|
|
@os = case user_agent
|
|
when /Windows/i then "windows"
|
|
when /Macintosh/i then "mac"
|
|
when /Linux/i then "linux"
|
|
when /Android/i then "android"
|
|
when /iPhone|iPad/i then "ios"
|
|
else ""
|
|
end
|
|
end
|
|
|
|
# By default, we show the user the last chat they interacted with
|
|
def set_default_chat
|
|
@last_viewed_chat = Current.user&.last_viewed_chat
|
|
@chat = @last_viewed_chat
|
|
end
|
|
|
|
def set_active_storage_url_options
|
|
ActiveStorage::Current.url_options = {
|
|
protocol: request.protocol,
|
|
host: request.host,
|
|
port: request.optional_port
|
|
}
|
|
end
|
|
|
|
def demo_config
|
|
Rails.application.config_for(:demo)
|
|
rescue RuntimeError, Errno::ENOENT, Psych::SyntaxError
|
|
nil
|
|
end
|
|
|
|
def demo_host_match?(demo = demo_config)
|
|
return false unless demo.is_a?(Hash) && demo["hosts"].present?
|
|
|
|
demo["hosts"].include?(request.host)
|
|
end
|
|
|
|
def show_demo_warning?
|
|
demo_host_match?
|
|
end
|
|
|
|
def accessible_accounts
|
|
Current.accessible_accounts
|
|
end
|
|
helper_method :accessible_accounts
|
|
|
|
def finance_accounts
|
|
Current.finance_accounts
|
|
end
|
|
helper_method :finance_accounts
|
|
end
|