From aebbb9a3c1c5d26e04ad95e77202085e1d13abb3 Mon Sep 17 00:00:00 2001 From: Josh Pigford Date: Thu, 1 May 2025 18:52:09 -0500 Subject: [PATCH 01/16] Enhance institution_domain method in Account model - Improved error handling for invalid institution URLs by rescuing URI::InvalidURIError and logging a warning. - Refactored the method to use safe navigation and streamline the URL parsing process. --- app/models/account.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index f07b442f..85167e22 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -62,8 +62,18 @@ class Account < ApplicationRecord end def institution_domain - return nil unless plaid_account&.plaid_item&.institution_url.present? - URI.parse(plaid_account.plaid_item.institution_url).host.gsub(/^www\./, "") + url_string = plaid_account&.plaid_item&.institution_url + return nil unless url_string.present? + + begin + uri = URI.parse(url_string) + # Use safe navigation on .host before calling gsub + uri.host&.gsub(/^www\./, "") + rescue URI::InvalidURIError + # Log a warning if the URL is invalid and return nil + Rails.logger.warn("Invalid institution URL encountered for account #{id}: #{url_string}") + nil + end end def destroy_later From 0946a1497a0865185ad943d0d82a21f71e171d31 Mon Sep 17 00:00:00 2001 From: Josh Pigford Date: Thu, 1 May 2025 19:07:50 -0500 Subject: [PATCH 02/16] Add conditional rendering for account links in transfers view - Implemented checks for the existence of accounts before rendering links in the transfers partial. - Added error messaging for missing accounts to improve user feedback and prevent broken links. --- app/views/transfers/_account_links.html.erb | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/app/views/transfers/_account_links.html.erb b/app/views/transfers/_account_links.html.erb index 33390b31..738416c6 100644 --- a/app/views/transfers/_account_links.html.erb +++ b/app/views/transfers/_account_links.html.erb @@ -1,7 +1,25 @@ <%# locals: (transfer:, is_inflow: false) %>
<% first_account, second_account = is_inflow ? [transfer.to_account, transfer.from_account] : [transfer.from_account, transfer.to_account] %> - <%= link_to first_account.name, account_path(first_account, tab: "activity"), class: "hover:underline", data: { turbo_frame: "_top" } %> + + <%# Check if first_account exists before creating link %> + <% if first_account %> + <%= link_to first_account.name, account_path(first_account, tab: "activity"), class: "hover:underline", data: { turbo_frame: "_top" } %> + <% else %> + + Data Error: Missing account + + <% end %> + + <%# Use icon helper per conventions %> <%= icon(is_inflow ? "arrow-left" : "arrow-right", size: "sm") %> - <%= link_to second_account.name, account_path(second_account, tab: "activity"), class: "hover:underline", data: { turbo_frame: "_top" } %> + + <%# Check if second_account exists before creating link %> + <% if second_account %> + <%= link_to second_account.name, account_path(second_account, tab: "activity"), class: "hover:underline", data: { turbo_frame: "_top" } %> + <% else %> + + Data Error: Missing account + + <% end %>
From cf72f1a38796badc8886eab45e582fc53666db39 Mon Sep 17 00:00:00 2001 From: Alex Hatzenbuhler Date: Fri, 2 May 2025 06:30:31 -0500 Subject: [PATCH 03/16] Add assign merchant rule for transactions (#2174) --- .../set_transaction_merchant.rb | 30 +++++++++++++++++++ .../rule/registry/transaction_resource.rb | 3 +- test/models/rule/action_test.rb | 21 +++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 app/models/rule/action_executor/set_transaction_merchant.rb diff --git a/app/models/rule/action_executor/set_transaction_merchant.rb b/app/models/rule/action_executor/set_transaction_merchant.rb new file mode 100644 index 00000000..492ece52 --- /dev/null +++ b/app/models/rule/action_executor/set_transaction_merchant.rb @@ -0,0 +1,30 @@ +class Rule::ActionExecutor::SetTransactionMerchant < Rule::ActionExecutor + def type + "select" + end + + def options + family.merchants.pluck(:name, :id) + end + + def execute(transaction_scope, value: nil, ignore_attribute_locks: false) + merchant = family.merchants.find_by_id(value) + return unless merchant + + scope = transaction_scope + unless ignore_attribute_locks + scope = scope.enrichable(:merchant_id) + end + + scope.each do |txn| + Rule.transaction do + txn.log_enrichment!( + attribute_name: "merchant_id", + attribute_value: merchant.id, + source: "rule" + ) + txn.update!(merchant: merchant) + end + end + end +end diff --git a/app/models/rule/registry/transaction_resource.rb b/app/models/rule/registry/transaction_resource.rb index 1bcdf8a7..628d8cde 100644 --- a/app/models/rule/registry/transaction_resource.rb +++ b/app/models/rule/registry/transaction_resource.rb @@ -14,7 +14,8 @@ class Rule::Registry::TransactionResource < Rule::Registry def action_executors enabled_executors = [ Rule::ActionExecutor::SetTransactionCategory.new(rule), - Rule::ActionExecutor::SetTransactionTags.new(rule) + Rule::ActionExecutor::SetTransactionTags.new(rule), + Rule::ActionExecutor::SetTransactionMerchant.new(rule) ] if ai_enabled? diff --git a/test/models/rule/action_test.rb b/test/models/rule/action_test.rb index 6cab0bd9..61ee6425 100644 --- a/test/models/rule/action_test.rb +++ b/test/models/rule/action_test.rb @@ -58,4 +58,25 @@ class Rule::ActionTest < ActiveSupport::TestCase assert_equal [ tag ], transaction.reload.tags end end + + test "set_transaction_merchant" do + merchant = @family.merchants.create!(name: "Rule test merchant") + + # Does not modify transactions that are locked (user edited them) + @txn1.lock!(:merchant_id) + + action = Rule::Action.new( + rule: @transaction_rule, + action_type: "set_transaction_merchant", + value: merchant.id + ) + + action.apply(@rule_scope) + + assert_not_equal merchant.id, @txn1.reload.merchant_id + + [ @txn2, @txn3 ].each do |transaction| + assert_equal merchant.id, transaction.reload.merchant_id + end + end end From be0d51057d70d23a7255f21a9fcca1ce472813a8 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 07:54:12 -0400 Subject: [PATCH 04/16] Fix syntax error in account template --- app/views/accounts/show/_template.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/accounts/show/_template.html.erb b/app/views/accounts/show/_template.html.erb index 20d352b5..cfac9402 100644 --- a/app/views/accounts/show/_template.html.erb +++ b/app/views/accounts/show/_template.html.erb @@ -16,7 +16,7 @@ <%= render "accounts/show/chart", account: account %> <% end %> -
<% if tabs.present? %> <%= tabs %> <% else %> From 0c79b335f11d80c0c065663a9f6a8d6b477fa92f Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 07:58:14 -0400 Subject: [PATCH 05/16] Fix self hosted subscription redirect --- app/controllers/concerns/onboardable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/onboardable.rb b/app/controllers/concerns/onboardable.rb index 66f89ba9..a5297345 100644 --- a/app/controllers/concerns/onboardable.rb +++ b/app/controllers/concerns/onboardable.rb @@ -24,7 +24,7 @@ module Onboardable if Current.user.onboarded_at.blank? && !trial_just_started redirect_to onboarding_path - elsif !Current.family.subscribed? && !Current.family.trialing? + elsif !Current.family.subscribed? && !Current.family.trialing? && !self_hosted? redirect_to upgrade_subscription_path end end From adc5bf58d7e842232decf571605684368887ab91 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 08:15:12 -0400 Subject: [PATCH 06/16] Fix clean import dark mode styles --- app/helpers/imports_helper.rb | 4 ++-- app/views/import/cleans/show.html.erb | 6 +++--- app/views/import/rows/_form.html.erb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/helpers/imports_helper.rb b/app/helpers/imports_helper.rb index 4a0dce76..cabc31ac 100644 --- a/app/helpers/imports_helper.rb +++ b/app/helpers/imports_helper.rb @@ -46,11 +46,11 @@ module ImportsHelper end def cell_class(row, field) - base = "text-sm focus:ring-gray-900 focus:border-gray-900 w-full max-w-full disabled:text-subdued" + base = "bg-container text-sm focus:ring-gray-900 theme-dark:focus:ring-gray-100 focus:border-solid w-full max-w-full disabled:text-subdued" row.valid? # populate errors - border = row.errors.key?(field) ? "border-red-500" : "border-transparent" + border = row.errors.key?(field) ? "border-destructive" : "border-transparent" [ base, border ].join(" ") end diff --git a/app/views/import/cleans/show.html.erb b/app/views/import/cleans/show.html.erb index 1412cb5e..4a743d01 100644 --- a/app/views/import/cleans/show.html.erb +++ b/app/views/import/cleans/show.html.erb @@ -14,7 +14,7 @@
<%= icon "check-circle", size: "sm", color: "success" %> -

Your data has been cleaned

+

Your data has been cleaned

<%= render LinkComponent.new( @@ -45,13 +45,13 @@
-
+
<% @import.column_keys.each do |key| %>
<%= import_col_label(key) %>
<% end %>
-
+
<% @rows.each do |row| %> <%= render "import/rows/form", row: row %> <% end %> diff --git a/app/views/import/rows/_form.html.erb b/app/views/import/rows/_form.html.erb index b5057d63..e7f5a4a9 100644 --- a/app/views/import/rows/_form.html.erb +++ b/app/views/import/rows/_form.html.erb @@ -1,6 +1,6 @@ <%# locals: (row:) %> -
+
<% row.import.column_keys.each_with_index do |key, idx| %> <%= turbo_frame_tag dom_id(row, key), title: row.valid? ? nil : row.errors.full_messages.join(", ") do %> <%= form_with( From c24ae1762f0659df709a99d3735fc629a293f127 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 08:20:31 -0400 Subject: [PATCH 07/16] Fix account value cutoff on mobile --- app/views/accounts/show/_chart.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/accounts/show/_chart.html.erb b/app/views/accounts/show/_chart.html.erb index c94adeb3..1661554c 100644 --- a/app/views/accounts/show/_chart.html.erb +++ b/app/views/accounts/show/_chart.html.erb @@ -4,14 +4,14 @@ <% default_value_title = account.asset? ? t(".balance") : t(".owed") %>
-
-
+
+
<%= tag.p title || default_value_title, class: "text-sm font-medium text-secondary" %> <%= tooltip %>
- <%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium" %> + <%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %>
<%= form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %> From a1d64d6c2ecb5fba0d4a105e5008cc3ac604ae56 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 08:23:37 -0400 Subject: [PATCH 08/16] Fix subdued text in transaction form --- app/views/transactions/_form.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/transactions/_form.html.erb b/app/views/transactions/_form.html.erb index 733af700..4132d3fd 100644 --- a/app/views/transactions/_form.html.erb +++ b/app/views/transactions/_form.html.erb @@ -1,6 +1,6 @@ <%# locals: (entry:, income_categories:, expense_categories:) %> -<%= styled_form_with model: entry, url: transactions_path, class: "space-y-4 text-subdued", data: { controller: "transaction-form" } do |f| %> +<%= styled_form_with model: entry, url: transactions_path, class: "space-y-4", data: { controller: "transaction-form" } do |f| %> <% if entry.errors.any? %> <%= render "shared/form_errors", model: entry %> <% end %> @@ -12,7 +12,7 @@ <%= f.hidden_field :entryable_type, value: "Transaction" %> -
+
<%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %> <% if @entry.account_id %> From 752835f4922482e20ddad0c6e9bb8f0a7e865272 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 08:45:17 -0400 Subject: [PATCH 09/16] Fix transaction form account pre-filling and form styles --- app/assets/tailwind/maybe-design-system.css | 2 +- app/views/shared/_transaction_type_tabs.html.erb | 14 +++++++------- app/views/transactions/_form.html.erb | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/assets/tailwind/maybe-design-system.css b/app/assets/tailwind/maybe-design-system.css index 4649a6bc..e03ccb1b 100644 --- a/app/assets/tailwind/maybe-design-system.css +++ b/app/assets/tailwind/maybe-design-system.css @@ -330,7 +330,7 @@ } .form-field__input { - @apply border-none bg-transparent text-sm opacity-100 w-full p-0; + @apply text-primary border-none bg-transparent text-sm opacity-100 w-full p-0; @apply focus:opacity-100 focus:outline-hidden focus:ring-0; @apply placeholder-shown:opacity-50; @apply disabled:text-subdued; diff --git a/app/views/shared/_transaction_type_tabs.html.erb b/app/views/shared/_transaction_type_tabs.html.erb index c2b53b2f..b2cfe0fd 100644 --- a/app/views/shared/_transaction_type_tabs.html.erb +++ b/app/views/shared/_transaction_type_tabs.html.erb @@ -1,23 +1,23 @@ -
- <% active_tab = local_assigns[:active_tab] || "expense" %> +<%# locals: (active_tab:, account_id: nil) %> - <%= link_to new_transaction_path(nature: "outflow"), +
+ <%= link_to new_transaction_path(nature: "outflow", account_id: account_id), data: { turbo_frame: :modal }, - class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued #{active_tab == 'expense' ? 'bg-container text-gray-800 shadow-sm' : 'hover:bg-container hover:text-gray-800 hover:shadow-sm'}" do %> + class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm #{active_tab == 'expense' ? 'bg-container text-primary shadow-sm' : 'hover:bg-container text-subdued hover:text-primary hover:shadow-sm'}" do %> <%= icon "minus-circle" %> <%= tag.span t("shared.transaction_tabs.expense") %> <% end %> - <%= link_to new_transaction_path(nature: "inflow"), + <%= link_to new_transaction_path(nature: "inflow", account_id: account_id), data: { turbo_frame: :modal }, - class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued #{active_tab == 'income' ? 'bg-container text-gray-800 shadow-sm' : 'hover:bg-container hover:text-gray-800 hover:shadow-sm'}" do %> + class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm #{active_tab == 'income' ? 'bg-container text-primary shadow-sm' : 'hover:bg-container text-subdued hover:text-primary hover:shadow-sm'}" do %> <%= icon "plus-circle" %> <%= tag.span t("shared.transaction_tabs.income") %> <% end %> <%= link_to new_transfer_path, data: { turbo_frame: :modal }, - class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued #{active_tab == 'transfer' ? 'bg-container text-gray-800 shadow-sm' : 'hover:bg-container hover:text-gray-800 hover:shadow-sm'}" do %> + class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm #{active_tab == 'transfer' ? 'bg-container text-primary shadow-sm' : 'hover:bg-container text-subdued hover:text-primary hover:shadow-sm'}" do %> <%= icon "arrow-right-left" %> <%= tag.span t("shared.transaction_tabs.transfer") %> <% end %> diff --git a/app/views/transactions/_form.html.erb b/app/views/transactions/_form.html.erb index 4132d3fd..2104f6c9 100644 --- a/app/views/transactions/_form.html.erb +++ b/app/views/transactions/_form.html.erb @@ -6,7 +6,7 @@ <% end %>
- <%= render "shared/transaction_type_tabs", active_tab: params[:nature] == "inflow" ? "income" : "expense" %> + <%= render "shared/transaction_type_tabs", active_tab: params[:nature] == "inflow" ? "income" : "expense", account_id: params[:account_id] %> <%= f.hidden_field :nature, value: params[:nature] || "outflow", data: { "transaction-form-target": "natureField" } %> <%= f.hidden_field :entryable_type, value: "Transaction" %> From 1210a8f3a3e12fa7618690b3b9391a8e26e39d90 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 08:52:19 -0400 Subject: [PATCH 10/16] Update docker.md Signed-off-by: Zach Gollwitzer --- docs/hosting/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hosting/docker.md b/docs/hosting/docker.md index 940a685f..13f9469a 100644 --- a/docs/hosting/docker.md +++ b/docs/hosting/docker.md @@ -155,7 +155,7 @@ NOT_ automatically update. To update your self-hosted app, run the following com cd ~/docker-apps/maybe # Navigate to whatever directory you configured the app in docker compose pull # This pulls the "latest" published image from GHCR docker compose build # This rebuilds the app with updates -docker compose up --no-deps -d app # This restarts the app using the newest version +docker compose up --no-deps -d web worker # This restarts the app using the newest version ``` ## How to change which updates your app receives From 84eb2c90d45107caa64ab17f4c62712048d08368 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 09:34:48 -0400 Subject: [PATCH 11/16] Fix self host onboarding --- app/views/onboardings/_onboarding_nav.html.erb | 3 +++ app/views/onboardings/goals.html.erb | 6 +++++- app/views/onboardings/show.html.erb | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/views/onboardings/_onboarding_nav.html.erb b/app/views/onboardings/_onboarding_nav.html.erb index 6e24cc0f..903a0f6b 100644 --- a/app/views/onboardings/_onboarding_nav.html.erb +++ b/app/views/onboardings/_onboarding_nav.html.erb @@ -7,6 +7,9 @@ { name: "Start", path: trial_onboarding_path, is_complete: user.onboarded_at.present?, step_number: 4 }, ] %> +<%# Don't show last step if self hosted %> +<% steps.pop if self_hosted? %> +
<%= form_with model: @user do |form| %> - <%= form.hidden_field :redirect_to, value: "trial" %> + <%= form.hidden_field :redirect_to, value: self_hosted? ? "home" : "trial" %> <%= form.hidden_field :set_onboarding_goals_at, value: Time.current %> + <% if self_hosted? %> + <%= form.hidden_field :onboarded_at, value: Time.current %> + <% end %> +
<% [ { icon: "layers", label: "See all my accounts in one piece", value: "unified_accounts" }, diff --git a/app/views/onboardings/show.html.erb b/app/views/onboardings/show.html.erb index 9a2837e6..f5e5a5f9 100644 --- a/app/views/onboardings/show.html.erb +++ b/app/views/onboardings/show.html.erb @@ -1,4 +1,6 @@ -<%= content_for :previous_path, onboarding_path %> +<%= content_for :prev_nav do %> + <%= image_tag "logomark-color.svg", class: "w-10 h-10" %> +<% end %> <%= content_for :header_nav do %> <%= render "onboardings/onboarding_nav", user: @user %> From 793a5d2502171615a7fd2d3a08ec3a67b2797394 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 10:09:23 -0400 Subject: [PATCH 12/16] Fix Stripe event retrieval error --- app/models/provider/stripe.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/provider/stripe.rb b/app/models/provider/stripe.rb index 09c67911..32ee7138 100644 --- a/app/models/provider/stripe.rb +++ b/app/models/provider/stripe.rb @@ -63,6 +63,6 @@ class Provider::Stripe attr_reader :client, :webhook_secret def retrieve_event(event_id) - client.v2.core.events.retrieve(event_id) + client.v1.events.retrieve(event_id) end end From 1e1ed5ca45b5369154ee1f8f4a20a8ebe3894414 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 11:36:32 -0400 Subject: [PATCH 13/16] Light / dark assistant icon --- app/assets/images/ai-dark.svg | 71 ++++++++ app/assets/images/ai.svg | 156 ++++++++---------- .../_assistant_message.html.erb | 2 +- app/views/chats/_ai_avatar.html.erb | 7 +- app/views/chats/_ai_consent.html.erb | 2 +- app/views/chats/_ai_greeting.html.erb | 2 +- app/views/chats/_thinking_indicator.html.erb | 2 +- app/views/onboardings/trial.html.erb | 6 +- 8 files changed, 153 insertions(+), 95 deletions(-) create mode 100644 app/assets/images/ai-dark.svg diff --git a/app/assets/images/ai-dark.svg b/app/assets/images/ai-dark.svg new file mode 100644 index 00000000..bdcbfea6 --- /dev/null +++ b/app/assets/images/ai-dark.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/ai.svg b/app/assets/images/ai.svg index ee2c6462..94f17808 100644 --- a/app/assets/images/ai.svg +++ b/app/assets/images/ai.svg @@ -1,85 +1,71 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/views/assistant_messages/_assistant_message.html.erb b/app/views/assistant_messages/_assistant_message.html.erb index ced5d254..59356a78 100644 --- a/app/views/assistant_messages/_assistant_message.html.erb +++ b/app/views/assistant_messages/_assistant_message.html.erb @@ -15,7 +15,7 @@ <%= render "assistant_messages/tool_calls", message: assistant_message %> <% end %> -
+
<%= render "chats/ai_avatar" %>
<%= markdown(assistant_message.content) %>
diff --git a/app/views/chats/_ai_avatar.html.erb b/app/views/chats/_ai_avatar.html.erb index baa5a6f6..0df752e2 100644 --- a/app/views/chats/_ai_avatar.html.erb +++ b/app/views/chats/_ai_avatar.html.erb @@ -1,4 +1,7 @@ -
+<%# locals: (theme: "light") %> + +
<%# Never use svg as an image tag, it appears blurry in Safari %> - <%= inline_svg_tag "ai.svg", alt: "AI", class: "w-full h-full" %> + <%= inline_svg_tag "ai-dark.svg", alt: "AI", class: "w-full h-full hidden theme-dark:block" %> + <%= inline_svg_tag "ai.svg", alt: "AI", class: "w-full h-full theme-dark:hidden" %>
diff --git a/app/views/chats/_ai_consent.html.erb b/app/views/chats/_ai_consent.html.erb index 960aa6b3..f3a97c3e 100644 --- a/app/views/chats/_ai_consent.html.erb +++ b/app/views/chats/_ai_consent.html.erb @@ -1,5 +1,5 @@
-
+
<%= render "chats/ai_avatar" %>
diff --git a/app/views/chats/_ai_greeting.html.erb b/app/views/chats/_ai_greeting.html.erb index 30243fbf..954a0f72 100644 --- a/app/views/chats/_ai_greeting.html.erb +++ b/app/views/chats/_ai_greeting.html.erb @@ -1,4 +1,4 @@ -
+
<%= render "chats/ai_avatar" %>
diff --git a/app/views/chats/_thinking_indicator.html.erb b/app/views/chats/_thinking_indicator.html.erb index efd779b7..e1ba8921 100644 --- a/app/views/chats/_thinking_indicator.html.erb +++ b/app/views/chats/_thinking_indicator.html.erb @@ -1,6 +1,6 @@ <%# locals: (chat:, message: "Thinking ...") -%> -
+
<%= render "chats/ai_avatar" %>

<%= message %>

diff --git a/app/views/onboardings/trial.html.erb b/app/views/onboardings/trial.html.erb index 4a7e1ab5..0454da3e 100644 --- a/app/views/onboardings/trial.html.erb +++ b/app/views/onboardings/trial.html.erb @@ -90,10 +90,8 @@

Comprehensive transaction tracking experience

-
-
- <%= render "chats/ai_avatar" %> -
+
+ <%= render "chats/ai_avatar" %>

Unlimited access and chats with Maybe AI

From a7a29b4780947e18c2821787e90e616d3c7d065b Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 11:42:09 -0400 Subject: [PATCH 14/16] Prevent ai icon shrinking --- app/views/chats/_ai_avatar.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/chats/_ai_avatar.html.erb b/app/views/chats/_ai_avatar.html.erb index 0df752e2..ec5b47dd 100644 --- a/app/views/chats/_ai_avatar.html.erb +++ b/app/views/chats/_ai_avatar.html.erb @@ -1,6 +1,6 @@ <%# locals: (theme: "light") %> -
+
<%# Never use svg as an image tag, it appears blurry in Safari %> <%= inline_svg_tag "ai-dark.svg", alt: "AI", class: "w-full h-full hidden theme-dark:block" %> <%= inline_svg_tag "ai.svg", alt: "AI", class: "w-full h-full theme-dark:hidden" %> From bc7e32deab8403c76f6611f53bbc6dfb65203527 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 12:05:50 -0400 Subject: [PATCH 15/16] Fix event processor api --- app/models/provider/stripe.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/provider/stripe.rb b/app/models/provider/stripe.rb index 32ee7138..d5f5242c 100644 --- a/app/models/provider/stripe.rb +++ b/app/models/provider/stripe.rb @@ -12,9 +12,9 @@ class Provider::Stripe case event.type when /^customer\.subscription\./ - SubscriptionEventProcessor.new(client).process(event) + SubscriptionEventProcessor.new(event: event, client: client).process when /^customer\./ - CustomerEventProcessor.new(client).process(event) + CustomerEventProcessor.new(event: event, client: client).process else Rails.logger.info "Unhandled event type: #{event.type}" end From 441f436187231ae4a0a696d353dcb38bbbf1eed0 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 2 May 2025 15:21:46 -0400 Subject: [PATCH 16/16] Onboarding redirect tests and trial status bar (#2197) * Onboarding redirect tests and trial status bar * use helper method * Fix time tolerance failure * Update post-onboarding message to be generic * Disable turbo frames on Trial start button * Update flash notice in test --- app/controllers/concerns/onboardable.rb | 6 +-- app/controllers/onboardings_controller.rb | 1 - app/controllers/subscriptions_controller.rb | 14 +++--- app/models/user.rb | 4 ++ app/views/layouts/application.html.erb | 27 +++++++++++- .../onboardings/_onboarding_nav.html.erb | 2 +- app/views/onboardings/trial.html.erb | 3 +- test/controllers/concerns/onboardable_test.rb | 43 +++++++++++++++++++ .../subscriptions_controller_test.rb | 28 ++++++++++++ 9 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 test/controllers/concerns/onboardable_test.rb diff --git a/app/controllers/concerns/onboardable.rb b/app/controllers/concerns/onboardable.rb index a5297345..9e7dd144 100644 --- a/app/controllers/concerns/onboardable.rb +++ b/app/controllers/concerns/onboardable.rb @@ -18,11 +18,7 @@ module Onboardable return unless Current.user return unless redirectable_path?(request.path) - # Check if trial was started VERY recently (e.g., within the last few seconds) - # If so, assume onboarding was just completed in the previous request, even if onboarded_at appears blank momentarily. - trial_just_started = Current.family.trial_started_at.present? && Current.family.trial_started_at > 10.seconds.ago - - if Current.user.onboarded_at.blank? && !trial_just_started + if !Current.user.onboarded? redirect_to onboarding_path elsif !Current.family.subscribed? && !Current.family.trialing? && !self_hosted? redirect_to upgrade_subscription_path diff --git a/app/controllers/onboardings_controller.rb b/app/controllers/onboardings_controller.rb index 9b98be3b..0f616ce4 100644 --- a/app/controllers/onboardings_controller.rb +++ b/app/controllers/onboardings_controller.rb @@ -14,7 +14,6 @@ class OnboardingsController < ApplicationController end private - def set_user @user = Current.user end diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb index 895c7192..45f4548a 100644 --- a/app/controllers/subscriptions_controller.rb +++ b/app/controllers/subscriptions_controller.rb @@ -8,16 +8,14 @@ class SubscriptionsController < ApplicationController end def start_trial - if Current.family.trial_started_at.present? - redirect_to root_path, alert: "You've already started or completed your trial" - else - Family.transaction do - Current.family.update(trial_started_at: Time.current) - Current.user.update(onboarded_at: Time.current) + unless Current.family.trialing? + ActiveRecord::Base.transaction do + Current.user.update!(onboarded_at: Time.current) + Current.family.update!(trial_started_at: Time.current) end - - redirect_to root_path, notice: "Your trial has started" end + + redirect_to root_path, notice: "Welcome to Maybe!" end def new diff --git a/app/models/user.rb b/app/models/user.rb index 00dfd5af..d211878a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -152,6 +152,10 @@ class User < ApplicationRecord totp.provisioning_uri(email) end + def onboarded? + onboarded_at.present? + end + private def ensure_valid_profile_image return unless profile_image.attached? diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e77f1916..25432ff0 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -79,8 +79,31 @@ <% if content_for?(:sidebar) %> <%= yield :sidebar %> <% else %> -
- <%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: params[:account_group_tab] || "assets" %> +
+
+ <%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: params[:account_group_tab] || "assets" %> +
+ + <% if Current.family.trialing? && !self_hosted? %> +
+
+
+

Free trial

+

<%= Current.family.trial_remaining_days %> days remaining

+
+ + <%= render LinkComponent.new( + text: "Upgrade", + href: upgrade_subscription_path, + ) %> +
+ +
+
+
+
+
+ <% end %>
<% end %> <% end %> diff --git a/app/views/onboardings/_onboarding_nav.html.erb b/app/views/onboardings/_onboarding_nav.html.erb index 903a0f6b..7a2e010a 100644 --- a/app/views/onboardings/_onboarding_nav.html.erb +++ b/app/views/onboardings/_onboarding_nav.html.erb @@ -4,7 +4,7 @@ { name: "Setup", path: onboarding_path, is_complete: user.first_name.present?, step_number: 1 }, { name: "Preferences", path: preferences_onboarding_path, is_complete: user.set_onboarding_preferences_at.present?, step_number: 2 }, { name: "Goals", path: goals_onboarding_path , is_complete: user.set_onboarding_goals_at.present?, step_number: 3 }, - { name: "Start", path: trial_onboarding_path, is_complete: user.onboarded_at.present?, step_number: 4 }, + { name: "Start", path: trial_onboarding_path, is_complete: user.onboarded?, step_number: 4 }, ] %> <%# Don't show last step if self hosted %> diff --git a/app/views/onboardings/trial.html.erb b/app/views/onboardings/trial.html.erb index 0454da3e..19c7310f 100644 --- a/app/views/onboardings/trial.html.erb +++ b/app/views/onboardings/trial.html.erb @@ -32,7 +32,8 @@ <%= render ButtonComponent.new( text: "Try Maybe for 14 days", href: start_trial_subscription_path, - full_width: true + full_width: true, + data: { turbo: false } ) %>
diff --git a/test/controllers/concerns/onboardable_test.rb b/test/controllers/concerns/onboardable_test.rb new file mode 100644 index 00000000..c4d313ae --- /dev/null +++ b/test/controllers/concerns/onboardable_test.rb @@ -0,0 +1,43 @@ +require "test_helper" + +class OnboardableTest < ActionDispatch::IntegrationTest + setup do + sign_in @user = users(:empty) + end + + test "must complete onboarding before any other action" do + @user.update!(onboarded_at: nil) + + get root_path + assert_redirected_to onboarding_path + + @user.family.update!(trial_started_at: 1.day.ago, stripe_subscription_status: "active") + + get root_path + assert_redirected_to onboarding_path + end + + test "must subscribe if onboarding complete and no trial or subscription is active" do + @user.update!(onboarded_at: 1.day.ago) + @user.family.update!(trial_started_at: nil, stripe_subscription_status: "incomplete") + + get root_path + assert_redirected_to upgrade_subscription_path + end + + test "onboarded trial user can visit dashboard" do + @user.update!(onboarded_at: 1.day.ago) + @user.family.update!(trial_started_at: 1.day.ago, stripe_subscription_status: "incomplete") + + get root_path + assert_response :success + end + + test "onboarded subscribed user can visit dashboard" do + @user.update!(onboarded_at: 1.day.ago) + @user.family.update!(stripe_subscription_status: "active") + + get root_path + assert_response :success + end +end diff --git a/test/controllers/subscriptions_controller_test.rb b/test/controllers/subscriptions_controller_test.rb index 952aed3b..abd7473f 100644 --- a/test/controllers/subscriptions_controller_test.rb +++ b/test/controllers/subscriptions_controller_test.rb @@ -5,6 +5,34 @@ class SubscriptionsControllerTest < ActionDispatch::IntegrationTest sign_in @user = users(:family_admin) end + test "can start trial" do + @user.update!(onboarded_at: nil) + @user.family.update!(trial_started_at: nil, stripe_subscription_status: "incomplete") + + assert_nil @user.onboarded_at + assert_nil @user.family.trial_started_at + + post start_trial_subscription_path + assert_redirected_to root_path + assert_equal "Welcome to Maybe!", flash[:notice] + + assert @user.reload.onboarded? + assert @user.family.reload.trial_started_at.present? + end + + test "if user re-enters onboarding, don't restart trial" do + onboard_time = 1.day.ago + trial_start_time = 1.day.ago + + @user.update!(onboarded_at: onboard_time) + @user.family.update!(trial_started_at: trial_start_time, stripe_subscription_status: "incomplete") + + post start_trial_subscription_path + assert_redirected_to root_path + + assert @user.reload.family.trial_started_at < Date.current + end + test "redirects to settings if self hosting" do Rails.application.config.app_mode.stubs(:self_hosted?).returns(true) get subscription_path