Files
sure/app/views/settings/providers/show.html.erb
Juan José Mata 4623bc3653 feat(settings/providers): card grid for available providers with connect drawer
- Add Provider::Metadata registry with static display data (region, kind,
  tier, maturity, logo) for all 11 providers
- Add Settings::ProviderCard ViewComponent rendering logo square, name,
  Beta/Alpha pill, meta line (region · type · tier), tagline, and Connect link
- Add connect_form action + route (GET /settings/providers/:key/connect_form)
  that opens the existing panel partial or config form in a DS::Dialog drawer
- Replace the Available accordion loop with a 2-column responsive card grid;
  empty state when all providers are connected
- Fix layout override: use turbo_rails/frame layout for frame requests so the
  drawer response is not wrapped in the full settings layout (was causing
  Turbo to pick the empty outer drawer frame instead of the filled one)
- Add SyncAllProvidersJob and last_sync_all_attempted_at migration (sync-all
  throttle support)
- Unify Connected + Action needed into a single "Your connections" section;
  items with warn/err status auto-open
- Fix Enable Banking grouping: items with expired sessions were returning
  :off (Available) instead of :warn (Your connections); gate now checks
  any? instead of any?(&:session_valid?)
- Add reconsent_required locale key for fully-expired EB sessions
- Surface Beta/Alpha maturity pills on connected provider accordion rows
  via new badge: param on settings_section helper
- Add i18n taglines for all 11 providers; add connect and empty_available keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:36:24 +00:00

98 lines
4.6 KiB
Plaintext

<%= content_for :page_title, "Bank Sync" %>
<div class="space-y-4">
<% if @encryption_error %>
<div class="p-4 rounded-lg bg-destructive/10 border border-destructive/20">
<div class="flex items-start gap-3">
<%= icon("triangle-alert", class: "w-5 h-5 text-destructive shrink-0 mt-0.5") %>
<div>
<h3 class="font-medium text-primary"><%= t("settings.providers.encryption_error.title") %></h3>
<p class="text-secondary text-sm mt-1"><%= t("settings.providers.encryption_error.message") %></p>
</div>
</div>
</div>
<% else %>
<div class="flex items-start justify-between gap-4">
<p class="text-secondary">
Connect external accounts so transactions, balances and holdings flow into Sure automatically.
</p>
<% if @connected.any? || @needs_attention.any? %>
<% sync_all_disabled = Current.family.last_sync_all_attempted_at.present? && Current.family.last_sync_all_attempted_at > 30.seconds.ago %>
<%= button_to sync_all_settings_providers_path,
method: :post,
disabled: sync_all_disabled,
title: sync_all_disabled ? t("settings.providers.sync_all_recently") : nil,
class: "inline-flex items-center gap-2 shrink-0 px-3 py-1.5 text-sm font-medium text-secondary hover:text-primary border border-secondary rounded-lg hover:border-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed" do %>
<%= icon "refresh-cw", class: "w-4 h-4" %>
<%= t("settings.providers.sync_all") %>
<% end %>
<% end %>
</div>
<%= render Settings::HealthSummary.new(counts: @health_counts) %>
<% all_connections = @needs_attention + @connected %>
<%= render "settings/providers/group_heading",
title: t("settings.providers.groups.your_connections"),
count: all_connections.size %>
<% if all_connections.empty? %>
<p class="text-sm text-secondary px-1 py-2"><%= t("settings.providers.groups.empty_connected") %></p>
<% end %>
<% all_connections.each do |entry| %>
<% auto_open = [ :warn, :err ].include?(entry[:summary][:status]) || all_connections.size == 1 %>
<% sync_action = entry[:partial].present? ? render("settings/providers/sync_button", provider_key: entry[:provider_key], last_synced_at: entry[:summary][:last_synced_at]) : nil %>
<% maturity_label = Settings::ProviderCard::MATURITY_LABELS[entry[:maturity]] %>
<% maturity_badge = maturity_label ? content_tag(:span, maturity_label, class: "text-xs font-medium px-1.5 py-0.5 rounded-full bg-alpha-black-50 text-secondary") : nil %>
<%= settings_section title: entry[:title],
collapsible: true,
open: auto_open,
auto_open_param: entry[:auto_open_param],
status: entry[:summary][:status],
meta: entry[:summary][:meta],
actions: sync_action,
badge: maturity_badge do %>
<% if entry[:configuration] %>
<%= render "settings/providers/provider_form", configuration: entry[:configuration] %>
<% else %>
<turbo-frame id="<%= entry[:turbo_id] %>-providers-panel">
<%= render "settings/providers/#{entry[:partial]}" %>
</turbo-frame>
<% end %>
<% end %>
<% end %>
<% unless @available.empty? %>
<%= render "settings/providers/add_provider_cta" %>
<% end %>
<%= render "settings/providers/group_heading",
title: t("settings.providers.groups.available"),
count: @available.size,
anchor: "available" %>
<% if @available.empty? %>
<p class="text-sm text-secondary px-1 py-2"><%= t("settings.providers.groups.empty_available") %></p>
<% else %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<% @available.each do |entry| %>
<% meta = Provider::Metadata.for(entry[:provider_key]) %>
<%= render Settings::ProviderCard.new(
provider_key: entry[:provider_key],
name: entry[:title],
tagline: t("settings.providers.taglines.#{entry[:provider_key]}", default: nil),
region: meta[:region],
kind: meta[:kind],
tier: meta[:tier],
maturity: meta[:maturity] || :stable,
logo_bg: meta[:logo_bg],
logo_text: meta[:logo_text]
) %>
<% end %>
</div>
<% end %>
<% end %>
</div>