diff --git a/app/components/DS/alert.html.erb b/app/components/DS/alert.html.erb index 8f5c0fd41..419a4607c 100644 --- a/app/components/DS/alert.html.erb +++ b/app/components/DS/alert.html.erb @@ -1,5 +1,5 @@
<%= meta_line %>
+ <% end %> +<%= tagline %>
+ <% end %> +<%= subtitle %>
<% end %>- <%= provider_link[:name] %> -
-- <%= provider_link[:description] %> -
-PROVIDERS
- · -<%= @providers.count %>
-No providers configured
-Configure providers to link your bank accounts.
-<%= t("settings.providers.binance_panel.setup_instructions") %>
-<%= t("settings.providers.binance_panel.no_withdraw_warning") %>
-<%= t("settings.providers.binance_panel.ip_hint_title") %>
-<%= t("settings.providers.binance_panel.ip_hint_body") %>
- <% server_ip = ENV["BINANCE_EGRESS_IP"].presence %> - <% if server_ip %> -<%= server_ip %>
- <% else %>
- <%= t("settings.providers.binance_panel.ip_hint_contact_admin") %>
- <% end %> ++ <%= t("settings.providers.binance_panel.ip_hint_title") %> +
+<%= t("settings.providers.binance_panel.ip_hint_body") %>
+ <% server_ip = ENV["BINANCE_EGRESS_IP"].presence %> + <% if server_ip %> +<%= server_ip %>
+ <% else %>
+ <%= t("settings.providers.binance_panel.ip_hint_contact_admin") %>
+ <% end %> +<%= error_msg %>
-<%= t("settings.providers.binance_panel.status_connected") %>
- <% else %> - -<%= t("settings.providers.binance_panel.status_not_connected") %>
- <% end %> -<%= t("settings.providers.coinbase_panel.setup_instructions") %>
-<%= error_msg %>
-<%= t("settings.providers.coinbase_panel.status_connected") %>
- <% else %> - -<%= t("settings.providers.coinbase_panel.status_not_connected") %>
- <% end %> -<%= t("coinstats_items.new.setup_instructions") %>
-<%= error_msg %>
-<%= t("coinstats_items.new.status_configured_html", accounts_url: accounts_path).html_safe %>
- <% else %> - -<%= t("coinstats_items.new.status_not_configured") %>
- <% end %> -Setup instructions:
-Field descriptions:
-<%= error_msg %>
-Configuration locked
-Credentials cannot be changed while you have active bank connections. Remove all connections first to update credentials.
-<%= description %>
+ <% end %> +<% end %> diff --git a/app/views/settings/providers/_health_strip.html.erb b/app/views/settings/providers/_health_strip.html.erb new file mode 100644 index 000000000..1a30989f7 --- /dev/null +++ b/app/views/settings/providers/_health_strip.html.erb @@ -0,0 +1,28 @@ +<%# locals: (connected:, needs_attention:, accounts_syncing:, last_synced_at:) %> +<%= t("indexa_capital_items.panel.setup_instructions") %>
-<%= error_msg %>
-<%= t("indexa_capital_items.panel.fields.api_token.label") %>
-<%= t("indexa_capital_items.panel.fields.api_token.description") %>
- <%= form.text_field :api_token, - label: t("indexa_capital_items.panel.fields.api_token.label"), - placeholder: is_new_record ? t("indexa_capital_items.panel.fields.api_token.placeholder_new") : t("indexa_capital_items.panel.fields.api_token.placeholder_update"), - type: :password %> -<%= t("indexa_capital_items.panel.fields.api_token.description") %>
<%= t("indexa_capital_items.panel.status_configured_html", accounts_path: accounts_path).html_safe %>
- <% else %> - -<%= t("indexa_capital_items.panel.status_not_configured") %>
- <% end %> -Setup instructions:
-Field descriptions:
-<%= error_msg %>
-Configured and ready to use. Visit the Accounts tab to manage and set up accounts.
- <% else %> - -Not configured
- <% end %> -<%= t("mercury_items.provider_panel.setup_title") %>
-- <%= t("mercury_items.provider_panel.sandbox_note_html") %> -
-<%= error_msg %>
-<%= t("mercury_items.provider_panel.sandbox_note_html").html_safe %>
- <%= form.text_field :base_url, - label: t("mercury_items.provider_panel.base_url_label"), - placeholder: t("mercury_items.provider_panel.base_url_placeholder") %> +<%= t("mercury_items.provider_panel.configured_html", accounts_link: link_to(t("mercury_items.provider_panel.accounts_link"), accounts_path, class: "link")) %>
- <% else %> - -<%= t("mercury_items.provider_panel.not_configured") %>
- <% end %> -- Configuration can be set via environment variables or overridden below. -
- <% end %> - - <% if configuration.fields.any? { |f| f.description.present? } %> -Field descriptions:
-+ Configuration can be set via environment variables or overridden below. +
+ <% end %> <%= styled_form_with model: Setting.new, url: settings_providers_path, @@ -37,50 +38,34 @@ <% configuration.fields.each do |field| %> <% env_value = ENV[field.env_key] if field.env_key - # Use dynamic hash-style access - works without explicit field declaration setting_value = Setting[field.setting_key] - - # Show the setting value if it exists, otherwise show ENV value - # This allows users to see what they've overridden current_value = setting_value.presence || env_value - # Mask secret values if they exist display_value = if field.secret && current_value.present? "********" else current_value end - # Determine input type input_type = field.secret ? "password" : "text" - - # Don't disable fields - allow overriding ENV variables - disabled = false %> - <%= form.text_field field.setting_key, - label: field.label, - type: input_type, - placeholder: field.default || (field.required ? "" : "Optional"), - value: display_value, - disabled: disabled %> +<%= field.description %>
+ <% end %> +Configured and ready to use
- <% else %> - -Not configured
- <% end %> -+ <%= eyebrow.presence || t("settings.providers.setup_steps.eyebrow") %> +
+Setup instructions:
-Field descriptions:
-<%= @error_message %>
-Configured and ready to use. Visit the Accounts tab to manage and set up accounts.
- <% else %> - -Not configured
- <% end %> -<%= t("providers.snaptrade.description") %>
+ <%= render DS::Alert.new(message: t("providers.snaptrade.free_tier_warning"), variant: :warning) %> -<%= t("providers.snaptrade.setup_title") %>
-<%= icon("alert-triangle", class: "inline-block w-4 h-4 mr-1") %><%= t("providers.snaptrade.free_tier_warning") %>
-<%= error_msg %>
-- <%= t("providers.snaptrade.status_connected", count: item.snaptrade_accounts.count) %> - <% if item.unlinked_accounts_count > 0 %> - (<%= t("providers.snaptrade.needs_setup", count: item.unlinked_accounts_count) %>) - <% end %> -
-- <%= t("providers.snaptrade.connection_limit_info") %> -
- -<%= t("providers.snaptrade.status_needs_registration") %>
-<%= t("providers.snaptrade.status_not_configured") %>
+ <% if items&.any? %> + <% item = items.first %> + <% unless item.user_registered? %> +<%= t("providers.snaptrade.status_needs_registration") %>
+ <%= t("providers.snaptrade.status_connected", count: item.snaptrade_accounts.count) %> + <% if item.unlinked_accounts_count > 0 %> + (<%= t("providers.snaptrade.needs_setup", count: item.unlinked_accounts_count) %>) + <% end %> +
+<%= t("sophtron_items.sophtron_panel.setup_instructions_title") %>
-<%= t("sophtron_items.sophtron_panel.field_descriptions_title") %>
-<%= error_msg %>
-<%= t("sophtron_items.sophtron_panel.status.configured_html", accounts_path: accounts_path) %>
- <% else %> - -<%= t("sophtron_items.sophtron_panel.status.not_configured") %>
- <% end %> -+ <%= t("settings.providers.drawer_trust_statement") %> +
+ <% end %> +<% end %> diff --git a/app/views/settings/providers/show.html.erb b/app/views/settings/providers/show.html.erb index 9f2b07c09..b78a19a96 100644 --- a/app/views/settings/providers/show.html.erb +++ b/app/views/settings/providers/show.html.erb @@ -1,94 +1,98 @@ -<%= content_for :page_title, "Sync Providers" %> +<%= content_for :page_title, t("settings.providers.bank_sync.page_title") %><%= t("settings.providers.encryption_error.message") %>
+ <%= render DS::Alert.new( + variant: :error, + message: safe_join([ + content_tag(:h2, t("settings.providers.encryption_error.title"), class: "font-medium"), + content_tag(:p, t("settings.providers.encryption_error.message"), class: "text-sm mt-1") + ]) + ) %> + <% else %> +<%= t("settings.providers.bank_sync.lede") %>
+ <% 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 %> + <%= render DS::Link.new( + text: t("settings.providers.sync_all"), + icon: "refresh-cw", + variant: "outline", + href: sync_all_settings_providers_path, + method: :post, + title: sync_all_disabled ? t("settings.providers.sync_all_recently") : nil, + aria: { disabled: sync_all_disabled.to_s }, + class: sync_all_disabled ? "opacity-50 pointer-events-none" : nil + ) %> + <% end %> +- Configure credentials for third-party sync providers. Settings configured here will override environment variables. -
-<%= t("settings.providers.groups.empty_available") %>
<% end %> <% end %>https://api-sandbox.mercury.com/api/v1 as the Base URL. Mercury requires IP whitelisting - make sure to add your IP in the Mercury dashboard."
setup_accounts: Set up accounts
setup_title: "Setup instructions:"
diff --git a/config/locales/views/mercury_items/hu.yml b/config/locales/views/mercury_items/hu.yml
index 75091c34c..8bf7271a7 100644
--- a/config/locales/views/mercury_items/hu.yml
+++ b/config/locales/views/mercury_items/hu.yml
@@ -44,11 +44,9 @@ hu:
total: Összesen
unlinked: Nincs összekapcsolva
provider_panel:
- accounts_link: Számlák
add_connection: Mercury kapcsolat hozzáadása
base_url_label: Alap URL (opcionális)
base_url_placeholder: https://api.mercury.com/api/v1 (alapértelmezett)
- configured_html: "Beállítva és használatra kész. A számlák kezeléséhez és beállításához látogass el a %{accounts_link} lapra."
connection_name_label: Kapcsolat neve
connection_name_placeholder: Üzleti folyószámla
default_connection_name: Mercury kapcsolat
@@ -60,7 +58,6 @@ hu:
sign_in_html: "Látogass el a(z) %{link} oldalra, és lépj be az összekapcsolni kívánt fiókba"
whitelist_ip_html: "Fontos: Add a szervered IP-címét a token engedélyezési listájához"
keep_token_placeholder: Hagyd üresen az aktuális token megtartásához
- not_configured: Nincs beállítva
sandbox_note_html: "Minden Mercury bejelentkezéshez/API tokenhez használj külön elnevezett kapcsolatot. Sandbox teszteléshez használd a https://api-sandbox.mercury.com/api/v1 alap URL-t. A Mercury IP engedélyezési listát igényel — győződj meg róla, hogy hozzáadtad az IP-d a Mercury irányítópulton."
setup_accounts: Számlák beállítása
setup_title: "Beállítási utasítások:"
diff --git a/config/locales/views/settings/de.yml b/config/locales/views/settings/de.yml
index e8a64d97e..eb17807f2 100644
--- a/config/locales/views/settings/de.yml
+++ b/config/locales/views/settings/de.yml
@@ -167,8 +167,6 @@ de:
syncing: Wird synchronisiert…
sync: Synchronisieren
disconnect_confirm: Bist du sicher, dass du diese Coinbase-Verbindung trennen möchtest? Deine synchronisierten Konten werden zu manuellen Konten.
- status_connected: Coinbase ist verbunden und synchronisiert deine Krypto-Bestände.
- status_not_connected: Nicht verbunden. Gib deine API-Zugangsdaten oben ein, um zu starten.
enable_banking_panel:
callback_url_instruction: "Für die Callback-URL, verwende %{callback_url}."
connection_error: Verbindungsfehler
diff --git a/config/locales/views/settings/en.yml b/config/locales/views/settings/en.yml
index 79e7c69d3..f3753374a 100644
--- a/config/locales/views/settings/en.yml
+++ b/config/locales/views/settings/en.yml
@@ -176,7 +176,7 @@ en:
whats_new_label: What's new
api_keys_label: API Key
appearance_label: Appearance
- bank_sync_label: Bank Sync
+ bank_sync_label: Bank sync
settings_nav_link_large:
next: Next
previous: Back
@@ -186,11 +186,73 @@ en:
choose_label: (optional)
change: Change photo
providers:
- show:
- coinbase_title: Coinbase
+ not_authorized: Not authorized
+ bank_sync:
+ page_title: Bank sync
+ lede: Connect external accounts so transactions, balances and holdings flow into Sure automatically.
+ status:
+ ok: Connected
+ warn: Action needed
+ err: Error
+ off: Not configured
+ maturity:
+ beta: Beta
+ alpha: Alpha
+ drawer_trust_statement: "Read-only access. Sure can never move money, and your credentials are stored encrypted."
+ setup_steps:
+ eyebrow: Setup
+ need_help: "Need help?"
+ connect: Connect
+ groups:
+ your_connections: Your connections
+ available: Available
+ empty_available: All available providers are connected.
+ health_strip:
+ connected: connected
+ needs_attention: needs attention
+ accounts_syncing: accounts syncing
+ last_synced: Last synced %{time} ago
+ meta:
+ sync_error: Sync error
+ no_recent_sync: Sync overdue
+ registration_needed: Registration needed
+ reconsent_required: Re-consent required
+ reconsent_needed:
+ one: Re-consent needed in 1 day
+ other: Re-consent needed in %{count} days
+ last_synced: Synced %{time} ago
+ sync_all: Sync all
+ sync_all_in_progress: Syncing all connected providers…
+ sync_all_recently: Sync already in progress. Try again in a moment.
+ sync_provider: Sync now
+ sync_provider_in_progress: Sync started.
+ recently_synced: Synced recently. Try again in a moment.
+ taglines:
+ simplefin: Connect US bank accounts via the open SimpleFIN protocol.
+ lunchflow: Connect 20k+ banks from 40+ countries (UK, EU, USA and more!)
+ enable_banking: Sync European bank accounts via PSD2 open banking.
+ coinstats: Track your entire crypto portfolio across wallets and exchanges.
+ mercury: Sync your Mercury business banking accounts automatically.
+ coinbase: Import your Coinbase crypto holdings and track performance.
+ binance: Sync your Binance spot balances using a read-only API key.
+ snaptrade: Connect brokerage accounts via the SnapTrade aggregation network.
+ indexa_capital: Track your Indexa Capital automated investment portfolio.
+ sophtron: Connect US & Canadian banks and utilities.
+ plaid: Connect thousands of US financial institutions via Plaid.
+ plaid_eu: Connect European financial institutions via Plaid (PSD2 / Open Banking).
+ search_filters:
+ aria_label: Search providers
+ placeholder: Search providers
+ chips:
+ all: All
+ bank: Banks
+ crypto: Crypto
+ investment: Investments
+ empty_filter: No providers match your filter.
+ clear_filter: Clear filters
encryption_error:
- title: Encryption Configuration Required
- message: Active Record encryption keys are not configured. Please ensure the encryption credentials (active_record_encryption.primary_key, active_record_encryption.deterministic_key, and active_record_encryption.key_derivation_salt) are properly set up in your Rails credentials or environment variables before using sync providers.
+ title: Encryption keys missing
+ message: "Bank sync needs Active Record encryption configured. Set primary_key, deterministic_key and key_derivation_salt in your Rails credentials or environment variables."
coinbase_panel:
setup_instructions: "To connect Coinbase:"
step1_html: Go to Coinbase API Settings
@@ -204,15 +266,14 @@ en:
syncing: Syncing...
sync: Sync
disconnect_confirm: Are you sure you want to disconnect this Coinbase connection? Your synced accounts will become manual accounts.
- status_connected: Coinbase is connected and syncing your crypto holdings.
- status_not_connected: Not connected. Enter your API credentials above to get started.
binance_panel:
setup_instructions: "To connect Binance, create a read-only API key:"
step1_html: 'Go to Binance API Management'
step2: "Create a new API key with Enable Reading permission only"
step3: "Paste your API Key and Secret below"
- no_withdraw_warning: "Warning: do NOT enable withdrawal permissions"
- ip_hint_title: "IP Whitelisting Required"
+ no_withdraw_title: "Read-only key only"
+ no_withdraw_body: "Don't enable withdrawal permissions when creating your Binance API key. Sure only needs read access."
+ ip_hint_title: "IP whitelisting required"
ip_hint_body: "Add the app server's egress IP to the Binance API Key whitelist:"
ip_hint_contact_admin: "Contact your administrator to obtain the app server's egress IP address."
api_key_label: API Key
@@ -223,8 +284,25 @@ en:
syncing: Syncing...
sync: Sync
disconnect_confirm: "Are you sure you want to disconnect Binance?"
- status_connected: Binance connected
- status_not_connected: Binance not connected
enable_banking_panel:
callback_url_instruction: "For the callback URL, use %{callback_url}."
connection_error: Connection Error
+ step_1_html: "Go to %{link} and grab your developer credentials."
+ step_2: "Pick your country and paste the Application ID + Client Certificate below."
+ step_3: "Save, then use Add Connection to link your bank."
+ lunchflow_panel:
+ step_1_html: "Go to %{link} and create an API key."
+ step_2: "Paste your key below and connect."
+ step_3: "Then head to Accounts to link your synced accounts."
+ simplefin_panel:
+ step_1_html: "Go to %{link} for a one-time setup token."
+ step_2: "Paste the token below and connect."
+ step_3: "Then head to Accounts to link your synced accounts."
+ plaid_panel:
+ step_1_html: "Open the %{link} and copy your Client ID and Secret Key."
+ step_2: "Pick an environment. Use sandbox for testing and production for real accounts."
+ step_3: "Paste your credentials below and connect."
+ plaid_eu_panel:
+ step_1_html: "Open the %{link} and copy your EU Client ID and Secret Key."
+ not_found: Provider not found.
+ sync_provider_no_items: No connections available to sync.
diff --git a/config/locales/views/settings/es.yml b/config/locales/views/settings/es.yml
index b9346a547..e8cf7fb0c 100644
--- a/config/locales/views/settings/es.yml
+++ b/config/locales/views/settings/es.yml
@@ -168,8 +168,6 @@ es:
syncing: Sincronizando...
sync: Sincronizar
disconnect_confirm: ¿Estás seguro de que deseas desconectar esta conexión de Coinbase? Tus cuentas sincronizadas pasarán a ser cuentas manuales.
- status_connected: Coinbase está conectado y sincronizando tus activos de criptomonedas.
- status_not_connected: No conectado. Introduce tus credenciales de API arriba para comenzar.
enable_banking_panel:
callback_url_instruction: "Para la URL de retorno (callback), utiliza %{callback_url}."
connection_error: Error de conexión
\ No newline at end of file
diff --git a/config/locales/views/settings/fr.yml b/config/locales/views/settings/fr.yml
index e3179c63f..a3cb0aec7 100644
--- a/config/locales/views/settings/fr.yml
+++ b/config/locales/views/settings/fr.yml
@@ -202,8 +202,6 @@ fr:
syncing: Synchronisation…
sync: Synchroniser
disconnect_confirm: Êtes-vous sûr(e) de vouloir déconnecter cette connexion Coinbase ? Vos comptes synchronisés deviendront des comptes manuels.
- status_connected: Coinbase est connecté et synchronise vos avoirs en crypto.
- status_not_connected: Non connecté. Saisissez vos identifiants API ci-dessus pour commencer.
binance_panel:
setup_instructions: "Pour connecter Binance, créez une clé API en lecture seule :"
step1_html: 'Allez dans la Gestion des API Binance'
@@ -221,8 +219,6 @@ fr:
syncing: Synchronisation…
sync: Synchroniser
disconnect_confirm: "Êtes-vous sûr(e) de vouloir déconnecter Binance ?"
- status_connected: Binance connecté
- status_not_connected: Binance non connecté
enable_banking_panel:
callback_url_instruction: "Pour l'URL de rappel, utilisez %{callback_url}."
connection_error: Erreur de connexion
diff --git a/config/locales/views/settings/hu.yml b/config/locales/views/settings/hu.yml
index ad687fdff..4d4d3dc91 100644
--- a/config/locales/views/settings/hu.yml
+++ b/config/locales/views/settings/hu.yml
@@ -202,8 +202,6 @@ hu:
syncing: Szinkronizálás...
sync: Szinkronizálás
disconnect_confirm: Biztosan le szeretnéd választani ezt a Coinbase-kapcsolatot? A szinkronizált számlák manuális számlákká válnak.
- status_connected: A Coinbase csatlakoztatva van, és szinkronizálja a kriptovaluta-állományodat.
- status_not_connected: Nincs csatlakoztatva. Az induláshoz add meg az API-hitelesítő adataidat fent.
binance_panel:
setup_instructions: "A Binance csatlakoztatásához hozz létre egy csak olvasási jogosultsággal rendelkező API-kulcsot:"
step1_html: 'Nyisd meg a Binance API-kezelőjét'
@@ -221,8 +219,6 @@ hu:
syncing: Szinkronizálás...
sync: Szinkronizálás
disconnect_confirm: "Biztosan le szeretnéd választani a Binance-t?"
- status_connected: A Binance csatlakoztatva van
- status_not_connected: A Binance nincs csatlakoztatva
enable_banking_panel:
callback_url_instruction: "A visszahívási URL-hez használd a következőt: %{callback_url}."
connection_error: Kapcsolódási hiba
diff --git a/config/locales/views/settings/pl.yml b/config/locales/views/settings/pl.yml
index 9962be6a7..8acfe1772 100644
--- a/config/locales/views/settings/pl.yml
+++ b/config/locales/views/settings/pl.yml
@@ -185,8 +185,6 @@ pl:
syncing: Synchronizacja...
sync: Synchronizuj
disconnect_confirm: Czy na pewno chcesz odłączyć to połączenie Coinbase? Twoje zsynchronizowane konta staną się kontami ręcznymi.
- status_connected: Coinbase jest połączony i synchronizuje Twoje zasoby kryptowalutowe.
- status_not_connected: Brak połączenia. Wprowadź powyżej dane API, aby rozpocząć.
enable_banking_panel:
callback_url_instruction: Dla URL callback użyj %{callback_url}.
connection_error: Błąd połączenia
diff --git a/config/locales/views/settings/pt-BR.yml b/config/locales/views/settings/pt-BR.yml
index 26fa9fa13..65962871b 100644
--- a/config/locales/views/settings/pt-BR.yml
+++ b/config/locales/views/settings/pt-BR.yml
@@ -186,8 +186,6 @@ pt-BR:
syncing: Sincronizando...
sync: Sincronizar
disconnect_confirm: Tem certeza de que deseja desconectar esta conexão com a Coinbase? Suas contas sincronizadas se tornarão contas manuais.
- status_connected: A Coinbase está conectada e sincronizando seus ativos em criptomoedas.
- status_not_connected: Não conectado. Insira suas credenciais de API acima para começar.
enable_banking_panel:
callback_url_instruction: "Para a URL de retorno de chamada, use %{callback_url}."
connection_error: Erro de conexão
diff --git a/config/locales/views/snaptrade_items/de.yml b/config/locales/views/snaptrade_items/de.yml
index c0f0cd22c..6ab8c28cf 100644
--- a/config/locales/views/snaptrade_items/de.yml
+++ b/config/locales/views/snaptrade_items/de.yml
@@ -134,8 +134,6 @@ de:
one: "%{count} muss eingerichtet werden"
other: "%{count} müssen eingerichtet werden"
status_ready: "Bereit zum Verbinden von Brokern"
- status_needs_registration: "Zugangsdaten gespeichert. Gehen Sie zur Konten-Seite, um Broker zu verbinden."
- status_not_configured: "Nicht konfiguriert"
setup_accounts_button: "Konten einrichten"
connect_button: "Broker verbinden"
connected_brokerages: "Verbunden:"
diff --git a/config/locales/views/snaptrade_items/en.yml b/config/locales/views/snaptrade_items/en.yml
index e9cdfd250..053978a4b 100644
--- a/config/locales/views/snaptrade_items/en.yml
+++ b/config/locales/views/snaptrade_items/en.yml
@@ -117,7 +117,7 @@ en:
step_2: "Copy your Client ID and Consumer Key from the dashboard"
step_3: "Enter your credentials below and click Save"
step_4: "Go to the Accounts page and use 'Connect another brokerage' to link your investment accounts"
- free_tier_warning: "Free tier includes 5 brokerage connections. Additional connections require a paid SnapTrade plan."
+ free_tier_warning: "SnapTrade's free tier covers 5 brokerage connections. Upgrade on SnapTrade for more."
client_id_label: "Client ID"
client_id_placeholder: "Enter your SnapTrade Client ID"
client_id_update_placeholder: "Enter new Client ID to update"
@@ -129,17 +129,15 @@ en:
status_connected:
one: "%{count} account from SnapTrade"
other: "%{count} accounts from SnapTrade"
+ status_needs_registration: "Credentials saved. Finish setup to connect a brokerage."
needs_setup:
one: "%{count} needs setup"
other: "%{count} need setup"
status_ready: "Ready to connect brokerages"
- status_needs_registration: "Credentials saved. Go to Accounts page to connect brokerages."
- status_not_configured: "Not configured"
setup_accounts_button: "Setup Accounts"
connect_button: "Connect Brokerage"
connected_brokerages: "Connected:"
manage_connections: "Manage Connections"
- connection_limit_info: "SnapTrade free tier allows 5 brokerage connections. Delete unused connections to free up slots."
loading_connections: "Loading connections..."
connections_error: "Failed to load connections: %{message}"
accounts_count:
diff --git a/config/locales/views/snaptrade_items/es.yml b/config/locales/views/snaptrade_items/es.yml
index 47a6ca609..db087fff9 100644
--- a/config/locales/views/snaptrade_items/es.yml
+++ b/config/locales/views/snaptrade_items/es.yml
@@ -134,8 +134,6 @@ es:
one: "%{count} necesita configuración"
other: "%{count} necesitan configuración"
status_ready: "Listo para conectar brókers"
- status_needs_registration: "Credenciales guardadas. Ve a la página de Cuentas para conectar brókers."
- status_not_configured: "No configurado"
setup_accounts_button: "Configurar cuentas"
connect_button: "Conectar bróker"
connected_brokerages: "Conectados:"
diff --git a/config/locales/views/snaptrade_items/fr.yml b/config/locales/views/snaptrade_items/fr.yml
index 91a28e5ba..65183408e 100644
--- a/config/locales/views/snaptrade_items/fr.yml
+++ b/config/locales/views/snaptrade_items/fr.yml
@@ -134,8 +134,6 @@ fr:
one: "%{count} à configurer"
other: "%{count} à configurer"
status_ready: "Prêt à connecter des courtiers"
- status_needs_registration: "Identifiants enregistrés. Rendez-vous sur la page Comptes pour connecter des courtiers."
- status_not_configured: "Non configuré"
setup_accounts_button: "Configurer les comptes"
connect_button: "Connecter un courtier"
connected_brokerages: "Connectés :"
diff --git a/config/locales/views/snaptrade_items/hu.yml b/config/locales/views/snaptrade_items/hu.yml
index fd85c5f75..424ba7ca3 100644
--- a/config/locales/views/snaptrade_items/hu.yml
+++ b/config/locales/views/snaptrade_items/hu.yml
@@ -134,8 +134,6 @@ hu:
one: "%{count} beállítást igényel"
other: "%{count} beállítást igényel"
status_ready: "Készen áll brókercégek csatlakoztatásához"
- status_needs_registration: "Hitelesítő adatok mentve. Menj a Számlák oldalra brókercégek csatlakoztatásához."
- status_not_configured: "Nincs beállítva"
setup_accounts_button: "Számlák beállítása"
connect_button: "Brókercég csatlakoztatása"
connected_brokerages: "Csatlakoztatva:"
diff --git a/config/locales/views/snaptrade_items/pl.yml b/config/locales/views/snaptrade_items/pl.yml
index ba3eb7058..1f45a3aa1 100644
--- a/config/locales/views/snaptrade_items/pl.yml
+++ b/config/locales/views/snaptrade_items/pl.yml
@@ -145,8 +145,6 @@ pl:
many: "%{count} wymaga konfiguracji"
other: "%{count} wymaga konfiguracji"
status_ready: Gotowe do połączenia z biurami maklerskimi
- status_needs_registration: Dane uwierzytelniające zapisane. Przejdź do strony Konta, aby połączyć biura maklerskie.
- status_not_configured: Nieskonfigurowane
setup_accounts_button: Konfiguruj konta
connect_button: Połącz biuro maklerskie
connected_brokerages: 'Połączone:'
diff --git a/config/locales/views/sophtron_items/en.yml b/config/locales/views/sophtron_items/en.yml
index a74c76e77..5e52084bf 100644
--- a/config/locales/views/sophtron_items/en.yml
+++ b/config/locales/views/sophtron_items/en.yml
@@ -279,9 +279,6 @@ en:
placeholder: "https://api.sophtron.com/api"
save: "Save Configuration"
update: "Update Configuration"
- status:
- configured_html: 'Configured and ready to use. Visit the Accounts tab to manage and set up accounts.'
- not_configured: "Not configured"
syncer:
manual_sync_required: "Manual Sophtron sync is required for this institution; skipping those accounts during automated sync."
importing_accounts: "Importing accounts from Sophtron..."
diff --git a/config/locales/views/sophtron_items/hu.yml b/config/locales/views/sophtron_items/hu.yml
index 7589414df..a4cb2d375 100644
--- a/config/locales/views/sophtron_items/hu.yml
+++ b/config/locales/views/sophtron_items/hu.yml
@@ -233,9 +233,6 @@ hu:
placeholder: "https://api.sophtron.com/v2"
save: "Konfiguráció mentése"
update: "Konfiguráció frissítése"
- status:
- configured_html: "Beállítva és használatra kész. A számlák kezeléséhez és beállításához látogass el a Számlák lapra."
- not_configured: "Nincs beállítva"
syncer:
manual_sync_required: "A kézi Sophtron szinkronizálás engedélyezve van; automatikus szinkronizálás kihagyva."
importing_accounts: "Számlák importálása a Sophtron-ból..."
diff --git a/config/routes.rb b/config/routes.rb
index b143c64d5..3a59f340b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -205,8 +205,14 @@ Rails.application.routes.draw do
resource :ai_prompts, only: :show
resource :llm_usage, only: :show
resource :guides, only: :show
- resource :bank_sync, only: :show, controller: "bank_sync"
- resource :providers, only: %i[show update]
+ get "bank_sync", to: redirect("/settings/providers", status: 301)
+ resource :providers, only: %i[show update] do
+ collection do
+ post :sync_all
+ post ":provider_key/sync", action: :sync, as: :sync_provider
+ get ":provider_key/connect_form", action: :connect_form, as: :connect_form
+ end
+ end
end
resource :subscription, only: %i[new show create] do
diff --git a/db/migrate/20260510120000_add_last_sync_all_attempted_at_to_families.rb b/db/migrate/20260510120000_add_last_sync_all_attempted_at_to_families.rb
new file mode 100644
index 000000000..0ab431f0a
--- /dev/null
+++ b/db/migrate/20260510120000_add_last_sync_all_attempted_at_to_families.rb
@@ -0,0 +1,5 @@
+class AddLastSyncAllAttemptedAtToFamilies < ActiveRecord::Migration[7.2]
+ def change
+ add_column :families, :last_sync_all_attempted_at, :datetime
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 641c5e640..b40605ab9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.2].define(version: 2026_05_08_130000) do
+ActiveRecord::Schema[7.2].define(version: 2026_05_10_120000) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -595,6 +595,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_08_130000) do
t.string "assistant_type", default: "builtin", null: false
t.string "default_account_sharing", default: "shared", null: false
t.string "enabled_currencies", array: true
+ t.datetime "last_sync_all_attempted_at"
t.check_constraint "default_account_sharing::text = ANY (ARRAY['shared'::character varying::text, 'private'::character varying::text])", name: "chk_families_default_account_sharing"
t.check_constraint "month_start_day >= 1 AND month_start_day <= 28", name: "month_start_day_range"
end
diff --git a/test/controllers/settings/providers_controller_test.rb b/test/controllers/settings/providers_controller_test.rb
index a7358e06e..00b98e893 100644
--- a/test/controllers/settings/providers_controller_test.rb
+++ b/test/controllers/settings/providers_controller_test.rb
@@ -1,6 +1,8 @@
require "test_helper"
class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
+ include ActiveJob::TestHelper
+
setup do
sign_in users(:family_admin)
@@ -8,6 +10,12 @@ class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
Provider::Factory.ensure_adapters_loaded
end
+ test "GET /settings/bank_sync redirects permanently to /settings/providers" do
+ get "/settings/bank_sync"
+ assert_redirected_to "/settings/providers"
+ assert_equal 301, response.status
+ end
+
test "can access when self hosting is disabled (managed mode)" do
Rails.configuration.stubs(:app_mode).returns("managed".inquiry)
get settings_providers_url
@@ -298,6 +306,55 @@ class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
end
end
+ test "POST sync_all enqueues SyncAllProvidersJob" do
+ SimplefinItem.create!(
+ family: families(:dylan_family),
+ name: "Test SimpleFIN Sync All",
+ access_url: "https://bridge.simplefin.org/simplefin/access"
+ )
+ families(:dylan_family).update_column(:last_sync_all_attempted_at, nil)
+
+ assert_enqueued_with(job: SyncAllProvidersJob) do
+ post sync_all_settings_providers_path
+ end
+
+ assert_redirected_to settings_providers_path
+
+ follow_redirect!
+ assert_response :success
+ assert_match(/Syncing all connected providers/i, response.body)
+ end
+
+ test "POST sync_all respects recent sync throttle" do
+ families(:dylan_family).update_column(:last_sync_all_attempted_at, Time.current)
+
+ assert_no_enqueued_jobs only: SyncAllProvidersJob do
+ post sync_all_settings_providers_path
+ end
+
+ assert_redirected_to settings_providers_path
+ assert_equal I18n.t("settings.providers.sync_all_recently"), flash[:notice]
+ end
+
+ test "POST sync for simplefin without an active Simplefin sync enqueues SyncJob" do
+ item = SimplefinItem.create!(
+ family: families(:dylan_family),
+ name: "Test SimpleFIN Per Row Sync",
+ access_url: "https://bridge.simplefin.org/simplefin/access"
+ )
+ Sync.where(syncable_type: "SimplefinItem", syncable_id: item.id).delete_all
+
+ assert_enqueued_jobs 1, only: SyncJob do
+ post sync_provider_settings_providers_path(provider_key: "simplefin")
+ end
+
+ assert_redirected_to settings_providers_path
+
+ follow_redirect!
+ assert_response :success
+ assert_match(/Sync started/i, response.body)
+ end
+
test "non-admin users cannot update providers" do
with_self_hosting do
sign_in users(:family_member)
@@ -306,7 +363,7 @@ class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
setting: { plaid_client_id: "test" }
}
- assert_redirected_to settings_providers_path
+ assert_redirected_to root_path
assert_equal "Not authorized", flash[:alert]
# Value should not have changed
diff --git a/test/models/family/syncer_test.rb b/test/models/family/syncer_test.rb
index baada992e..48ed9ffb2 100644
--- a/test/models/family/syncer_test.rb
+++ b/test/models/family/syncer_test.rb
@@ -5,11 +5,12 @@ class Family::SyncerTest < ActiveSupport::TestCase
@family = families(:dylan_family)
end
- test "syncs plaid items and manual accounts" do
+ test "syncs provider items and manual accounts" do
family_sync = syncs(:family)
manual_accounts_count = @family.accounts.manual.count
- items_count = @family.plaid_items.count
+ plaid_items_count = @family.plaid_items.syncable.count
+ binance_items_count = @family.binance_items.syncable.count
syncer = Family::Syncer.new(@family)
@@ -19,9 +20,14 @@ class Family::SyncerTest < ActiveSupport::TestCase
.times(manual_accounts_count)
PlaidItem.any_instance
- .expects(:sync_later)
- .with(parent_sync: family_sync, window_start_date: nil, window_end_date: nil)
- .times(items_count)
+ .expects(:sync_later)
+ .with(parent_sync: family_sync, window_start_date: nil, window_end_date: nil)
+ .times(plaid_items_count)
+
+ BinanceItem.any_instance
+ .expects(:sync_later)
+ .with(parent_sync: family_sync, window_start_date: nil, window_end_date: nil)
+ .times(binance_items_count)
syncer.perform_sync(family_sync)
@@ -61,6 +67,7 @@ class Family::SyncerTest < ActiveSupport::TestCase
LunchflowItem.any_instance.stubs(:sync_later)
EnableBankingItem.any_instance.stubs(:sync_later)
SophtronItem.any_instance.stubs(:sync_later)
+ BinanceItem.any_instance.stubs(:sync_later)
syncer.perform_sync(family_sync)
syncer.perform_post_sync
diff --git a/test/system/settings/providers_test.rb b/test/system/settings/providers_test.rb
new file mode 100644
index 000000000..549925ae0
--- /dev/null
+++ b/test/system/settings/providers_test.rb
@@ -0,0 +1,213 @@
+require "application_system_test_case"
+
+class Settings::ProvidersTest < ApplicationSystemTestCase
+ setup do
+ @user = users(:family_admin)
+ @family = families(:dylan_family)
+ login_as @user
+ end
+
+ test "shows status pill on section header for a configured provider" do
+ SimplefinItem.create!(family: @family, name: "Test SimpleFIN", access_url: "https://bridge.simplefin.org/simplefin/access")
+
+ visit settings_providers_path
+
+ within("details", text: "SimpleFIN") do
+ assert_text "Connected"
+ end
+ end
+
+ test "unconfigured SimpleFIN appears in Available with a connect affordance" do
+ visit settings_providers_path
+
+ assert_no_selector "details", text: "SimpleFIN"
+
+ within available_provider_cards_container do
+ assert_text "SimpleFIN"
+ assert_selector "a[data-turbo-frame='drawer']", text: "Connect"
+ end
+ end
+
+ test "connected providers are grouped under Your connections in alphabetical title order" do
+ SimplefinItem.create!(family: @family, name: "Test SimpleFIN", access_url: "https://bridge.simplefin.org/simplefin/access")
+
+ visit settings_providers_path
+
+ titles = all("details").map { |d| d.find("summary h3", match: :first).text.squish }
+ assert_equal titles.sort_by(&:downcase), titles, "Connection panels should render alphabetically by title"
+
+ connections_heading = page.find(:xpath, "//h2[contains(translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'your connections')]")
+ available_heading = page.find(:xpath, "//h2[contains(translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'available')]")
+ connections_y = connections_heading.native.location.y
+ available_y = available_heading.native.location.y
+
+ assert_operator connections_y, :<, page.find("details", text: "SimpleFIN").native.location.y
+ assert_operator page.find("details", text: "SimpleFIN").native.location.y, :<, available_y
+ end
+
+ test "expanding a section still works as expected" do
+ SimplefinItem.create!(family: @family, name: "Test SimpleFIN", access_url: "https://bridge.simplefin.org/simplefin/access")
+
+ visit settings_providers_path
+
+ assert_selector "details:not([open])", text: "SimpleFIN"
+
+ find("details", text: "SimpleFIN").find("summary").click
+
+ assert_selector "details[open]", text: "SimpleFIN"
+ within("details[open]", text: "SimpleFIN") do
+ assert_text "Setup Token"
+ end
+ end
+
+ test "groups providers into Your connections and Available with counts" do
+ SimplefinItem.create!(family: @family, name: "Test SimpleFIN", access_url: "https://bridge.simplefin.org/simplefin/access")
+
+ visit settings_providers_path
+
+ connections_heading = find(:xpath, "//h2[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'your connections')]")
+ normalized = connections_heading.text.squish
+ assert_match(/Your connections .*· \d+/i, normalized)
+
+ connections_y = connections_heading.native.location.y
+ available_heading = find(:xpath, "//h2[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'available')]")
+ available_y = available_heading.native.location.y
+ simplefin_y = find("details", text: "SimpleFIN").native.location.y
+
+ assert_operator connections_y, :<, simplefin_y, "Your connections heading should appear above SimpleFIN section"
+ assert_operator simplefin_y, :<, available_y, "SimpleFIN should appear above Available heading"
+
+ available_grid_top = available_provider_cards_container.native.location.y
+ assert_operator available_y, :<, available_grid_top, "Available heading should appear above the card grid"
+ end
+
+ test "action needed group is absent when no providers have issues" do
+ SimplefinItem.create!(family: @family, name: "Test SimpleFIN", access_url: "https://bridge.simplefin.org/simplefin/access")
+
+ visit settings_providers_path
+
+ assert_selector "h2", text: /\AYour connections/i
+ assert_no_selector "h2", text: /\AAction needed/i
+ end
+
+ test "enable banking with expiring session appears in your connections and auto-opens" do
+ item = EnableBankingItem.new(
+ family: @family,
+ name: "Test Bank",
+ country_code: "DE",
+ application_id: "test-app-id",
+ session_id: "test-session",
+ session_expires_at: 5.days.from_now
+ )
+ # Skip certificate validation for test purposes
+ item.save!(validate: false)
+
+ visit settings_providers_path
+
+ assert_selector "h2", text: /\AYour connections/i
+
+ # Auto-expanded warning sections hide compact meta behind `group-open:hidden`;
+ # collapse once so the re-consent copy is visible again.
+ enable = find("details", text: /Enable Banking/)
+ enable.find("summary").click if enable.matches_selector?(":open")
+
+ assert_selector "details:not([open])", text: /Enable Banking/
+ assert_text "Re-consent needed in 5 days"
+ end
+
+ test "search input filters provider cards by name" do
+ visit settings_providers_path
+
+ find('[data-providers-filter-target="input"]').set("Coinbase")
+
+ assert_selector "a[data-providers-filter-target='card']", text: /Coinbase/i
+ assert_no_selector "a[data-providers-filter-target='card']", text: /Binance/i
+ end
+
+ test "kind chip narrows the grid to providers of that kind" do
+ visit settings_providers_path
+
+ click_on "Crypto"
+
+ assert_selector "a[data-providers-filter-target='card']", text: /Coinbase/i
+ assert_no_selector "a[data-providers-filter-target='card']", text: /SimpleFIN/i
+ end
+
+ test "search shows the empty filter message when no provider matches" do
+ visit settings_providers_path
+
+ find('[data-providers-filter-target="input"]').set("zzz_no_match_zzz")
+
+ assert_selector '[data-providers-filter-target="empty"]', text: I18n.t("settings.providers.empty_filter")
+ assert_no_selector "a[data-providers-filter-target='card']", visible: true
+ end
+
+ test "available providers render as a card grid" do
+ visit settings_providers_path
+
+ within available_provider_cards_container do
+ assert_text "SimpleFIN"
+ assert_selector "a[data-turbo-frame='drawer']", minimum: 1
+ end
+ end
+
+ test "clicking a provider card opens the connect drawer" do
+ visit settings_providers_path
+
+ within available_provider_cards_container do
+ find("a[data-turbo-frame='drawer']", text: "SimpleFIN").click
+ end
+
+ assert_selector "dialog[open]"
+ assert_text "Setup Token"
+ end
+
+ test "configured plaid_eu surfaces in Your connections instead of Available" do
+ Setting["plaid_eu_client_id"] = "test_eu_client"
+ Setting["plaid_eu_secret"] = "test_eu_secret"
+
+ visit settings_providers_path
+
+ assert_selector "details summary h3", text: "Plaid EU"
+ within available_provider_cards_container do
+ assert_no_text "Plaid EU"
+ end
+ end
+
+ test "clear filters button resets search input and chip state" do
+ visit settings_providers_path
+
+ find('[data-providers-filter-target="input"]').set("zzz_no_match_zzz")
+ assert_selector '[data-providers-filter-target="empty"]', visible: true
+
+ click_on I18n.t("settings.providers.clear_filter")
+
+ assert_no_selector '[data-providers-filter-target="empty"]', visible: true
+ assert_equal "", find('[data-providers-filter-target="input"]').value
+ assert_selector "a[data-providers-filter-target='card']", text: /SimpleFIN/i
+ end
+
+ test "warn-state connection row carries warning outline class" do
+ item = EnableBankingItem.new(
+ family: @family,
+ name: "Test Bank",
+ country_code: "DE",
+ application_id: "test-app-id",
+ session_id: "test-session",
+ session_expires_at: 5.days.from_now
+ )
+ item.save!(validate: false)
+
+ visit settings_providers_path
+
+ details = find("details", text: /Enable Banking/)
+ assert_includes details[:class], "border-warning/25"
+ end
+
+ private
+
+ # Card grid rendered after the `#available` group heading (following sibling div.grid)
+ def available_provider_cards_container
+ find("#available").find(:xpath, "following-sibling::div[contains(concat(' ', normalize-space(@class), ' '), ' grid ')]")
+ end
+end
diff --git a/test/system/settings_test.rb b/test/system/settings_test.rb
index 099e55f6b..4aef39d0e 100644
--- a/test/system/settings_test.rb
+++ b/test/system/settings_test.rb
@@ -6,8 +6,12 @@ class SettingsTest < ApplicationSystemTestCase
# Base settings available to all users
@settings_links = [
- [ "Accounts", accounts_path ],
- [ "Bank Sync", settings_bank_sync_path ],
+ [ "Accounts", accounts_path ]
+ ]
+
+ @settings_links << [ "Bank sync", settings_providers_path ] if @user.admin?
+
+ @settings_links += [
[ "Preferences", settings_preferences_path ],
[ "Profile Info", settings_profile_path ],
[ "Security", settings_security_path ],
@@ -87,6 +91,7 @@ class SettingsTest < ApplicationSystemTestCase
# Assert that admin-only settings are not present in the navigation
assert_no_selector "li", text: "AI Prompts"
assert_no_selector "li", text: "API Key"
+ assert_no_selector "li", text: "Bank sync"
end
end