1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-08 23:15:24 +02:00

New onboarding, trials, Stripe integration (#2185)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions

* New onboarding, trials, Stripe integration

* Fix tests

* Lint fixes

* Fix subscription endpoints
This commit is contained in:
Zach Gollwitzer 2025-05-01 16:47:14 -04:00 committed by GitHub
parent 79b4a3769b
commit a51c4d2cba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 847 additions and 372 deletions

View file

@ -1,7 +0,0 @@
<header class="flex justify-between items-center p-4">
<%= image_tag "logo.svg", class: "h-[22px]" %>
<div class="flex items-center gap-2">
<%= icon("log-in", color: "secondary") %>
<%= button_to t(".sign_out"), session_path(Current.session), method: :delete, class: "text-sm text-primary font-medium" %>
</div>
</header>

View file

@ -0,0 +1,8 @@
<%= render ButtonComponent.new(
text: "Sign out",
icon: "log-out",
icon_position: :right,
variant: "ghost",
href: session_path(Current.session),
method: :delete
) %>

View file

@ -0,0 +1,39 @@
<%# locals: (user:) %>
<% steps = [
{ 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 },
] %>
<ul class="hidden md:flex items-center gap-2">
<% steps.each_with_index do |step, idx| %>
<li class="flex items-center gap-2 group">
<% is_current = request.path == step[:path] %>
<% text_class = if is_current
"text-primary"
else
step[:is_complete] ? "text-green-600" : "text-secondary"
end %>
<% step_class = if is_current
"bg-surface-inset text-primary"
else
step[:is_complete] ? "bg-green-600/10 border-alpha-black-25" : "bg-container-inset"
end %>
<%= link_to step[:path], class: "flex items-center gap-3" do %>
<div class="flex items-center gap-2 text-sm font-medium <%= text_class %>">
<span class="<%= step_class %> w-7 h-7 rounded-full shrink-0 inline-flex items-center justify-center border border-transparent">
<%= step[:is_complete] && !is_current ? icon("check", size: "sm", color: "current") : idx + 1 %>
</span>
<span><%= step[:name] %></span>
</div>
<% end %>
<hr class="border border-secondary w-12 group-last:hidden">
</li>
<% end %>
</ul>

View file

@ -0,0 +1,53 @@
<%= content_for :previous_path, preferences_onboarding_path %>
<%= content_for :header_nav do %>
<%= render "onboardings/onboarding_nav", user: @user %>
<% end %>
<%= content_for :cancel_action do %>
<%= render "onboardings/logout" %>
<% end %>
<%= content_for :footer do %>
<%= render "layouts/shared/footer" %>
<% end %>
<div class="grow max-w-lg w-full mx-auto bg-surface flex flex-col justify-center md:py-0 py-6 px-4 md:px-0">
<div>
<div class="space-y-1 mb-6 text-center">
<h1 class="text-2xl font-medium md:text-2xl">What brings you to Maybe?</h1>
<p class="text-secondary text-sm">Select one or more goals that you have with using Maybe as your personal finance tool.</p>
</div>
<%= form_with model: @user do |form| %>
<%= form.hidden_field :redirect_to, value: "trial" %>
<%= form.hidden_field :set_onboarding_goals_at, value: Time.current %>
<div class="space-y-3">
<% [
{ icon: "layers", label: "See all my accounts in one piece", value: "unified_accounts" },
{ icon: "banknote", label: "Understand cashflow and expenses", value: "cashflow" },
{ icon: "pie-chart", label: "Manage financial plans and budgeting", value: "budgeting" },
{ icon: "users", label: "Manage finances with a partner", value: "partner" },
{ icon: "area-chart", label: "Track investments", value: "investments" },
{ icon: "bot", label: "Let AI help me understand my finances", value: "ai_insights" },
{ icon: "settings-2", label: "Analyze and optimize accounts", value: "optimization" },
{ icon: "frown", label: "Reduce financial stress or anxiety", value: "reduce_stress" }
].each do |goal| %>
<label class="flex items-center gap-2.5 p-4 rounded-lg border border-tertiary cursor-pointer hover:bg-container transition-colors [&:has(input:checked)]:border-solid [&:has(input:checked)]:bg-container">
<%= form.check_box :goals, { multiple: true, class: "sr-only" }, goal[:value], nil %>
<%= icon goal[:icon] %>
<span class="text-primary text-sm"><%= goal[:label] %></span>
</label>
<% end %>
</div>
<div class="mt-6">
<%= render ButtonComponent.new(
text: "Next",
full_width: true
) %>
</div>
<% end %>
</div>
</div>

View file

@ -1,26 +1,33 @@
<div class="bg-surface h-screen flex flex-col justify-between">
<%= render "onboardings/header" %>
<%= content_for :previous_path, onboarding_path %>
<div class="grow max-w-lg w-full mx-auto bg-surface flex flex-col justify-center md:py-0 py-6 px-4 md:px-0" data-controller="onboarding">
<div>
<div class="space-y-1 mb-6">
<h1 class="text-2xl font-medium"><%= t(".title") %></h1>
<p class="text-secondary text-sm"><%= t(".subtitle") %></p>
</div>
<%= content_for :header_nav do %>
<%= render "onboardings/onboarding_nav", user: @user %>
<% end %>
<div class="p-1 bg-alpha-black-25 mb-2 rounded-lg">
<div class="bg-container p-4 rounded-lg flex gap-8 shadow-border-xs">
<div class="space-y-2">
<%= tag.p t(".example"), class: "text-secondary text-sm" %>
<%= tag.p format_money(Money.new(2325.25, params[:currency] || @user.family.currency)), class: "text-primary font-medium text-2xl" %>
<p class="text-sm">
<span class="text-green-500 font-medium">+<%= format_money(Money.new(78.90, params[:currency] || @user.family.currency)) %></span>
<span class="text-green-500 font-medium">(+<%= format_money(Money.new(6.39, params[:currency] || @user.family.currency)) %>)</span>
<span class="text-secondary">as of <%= format_date(Date.parse("2024-10-23"), :default, format_code: params[:date_format] || @user.family.date_format) %></span>
</p>
</div>
<%= content_for :cancel_action do %>
<%= render "onboardings/logout" %>
<% end %>
<% placeholder_series_data = [
<div class="grow max-w-lg w-full mx-auto bg-surface flex flex-col justify-center md:py-0 py-6 px-4 md:px-0" data-controller="onboarding">
<div>
<div class="space-y-1 mb-6">
<h1 class="text-2xl font-medium"><%= t(".title") %></h1>
<p class="text-secondary text-sm"><%= t(".subtitle") %></p>
</div>
<div class="p-1 bg-alpha-black-25 mb-2 rounded-lg">
<div class="bg-container p-4 rounded-lg flex gap-8 shadow-border-xs">
<div class="space-y-2">
<%= tag.p t(".example"), class: "text-secondary text-sm" %>
<%= tag.p format_money(Money.new(2325.25, params[:currency] || @user.family.currency)), class: "text-primary font-medium text-2xl" %>
<p class="text-sm">
<span class="text-green-500 font-medium">+<%= format_money(Money.new(78.90, params[:currency] || @user.family.currency)) %></span>
<span class="text-green-500 font-medium">(+<%= format_money(Money.new(6.39, params[:currency] || @user.family.currency)) %>)</span>
<span class="text-secondary">as of <%= format_date(Date.parse("2024-10-23"), :default, format_code: params[:date_format] || @user.family.date_format) %></span>
</p>
</div>
<% placeholder_series_data = [
{ date: Date.current - 14.days, value: 200 },
{ date: Date.current - 13.days, value: 200 },
{ date: Date.current - 12.days, value: 220 },
@ -38,53 +45,54 @@
{ date: Date.current, value: 265 }
] %>
<% placeholder_series = Series.from_raw_values(placeholder_series_data) %>
<% placeholder_series = Series.from_raw_values(placeholder_series_data) %>
<div class="flex items-center w-2/5">
<div class="h-12 w-full">
<div
<div class="flex items-center w-2/5">
<div class="h-12 w-full">
<div
id="previewChart"
class="h-full w-full"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= placeholder_series.to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
</div>
</div>
</div>
</div>
<p class="text-secondary text-xs mb-4"><%= t(".preview") %></p>
<p class="text-secondary text-xs mb-4"><%= t(".preview") %></p>
<%= styled_form_with model: @user, data: { turbo: false } do |form| %>
<%= form.hidden_field :onboarded_at, value: Time.current %>
<%= form.hidden_field :redirect_to, value: "home" %>
<%= styled_form_with model: @user, data: { turbo: false } do |form| %>
<%= form.hidden_field :set_onboarding_preferences_at, value: Time.current %>
<%= form.hidden_field :redirect_to, value: "goals" %>
<div class="space-y-4 mb-4">
<%= form.fields_for :family do |family_form| %>
<div class="mb-4">
<%= form.select :theme, [["System", "system"], ["Light", "light"], ["Dark", "dark"]], { label: "Color theme" }, data: { action: "onboarding#setTheme" } %>
</div>
<%= family_form.select :locale,
<div class="space-y-4 mb-4">
<%= form.fields_for :family do |family_form| %>
<%= family_form.select :locale,
language_options,
{ label: t(".locale"), required: true, selected: params[:locale] || @user.family.locale },
{ data: { action: "onboarding#setLocale" } } %>
<%= family_form.select :currency,
<%= family_form.select :currency,
Money::Currency.as_options.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
{ label: t(".currency"), required: true, selected: params[:currency] || @user.family.currency },
{ data: { action: "onboarding#setCurrency" } } %>
<%= family_form.select :date_format,
<%= family_form.select :date_format,
Family::DATE_FORMATS,
{ label: t(".date_format"), required: true, selected: params[:date_format] || @user.family.date_format },
{ data: { action: "onboarding#setDateFormat" } } %>
<% end %>
</div>
<% end %>
</div>
<%= form.submit t(".submit") %>
<% end %>
</div>
<%= form.submit t(".submit") %>
<% end %>
</div>
<%= render "layouts/shared/footer" %>
</div>

View file

@ -1,42 +0,0 @@
<div class="bg-surface min-h-screen flex flex-col justify-between">
<%= render "onboardings/header" %>
<div class="grow max-w-lg w-full mx-auto bg-surface flex flex-col justify-center md:py-0 py-6 px-4 md:px-0">
<div>
<div class="space-y-1 mb-6">
<h1 class="text-2xl font-medium md:text-2xl"><%= t(".title") %></h1>
<p class="text-secondary text-sm"><%= t(".subtitle") %></p>
</div>
<%= styled_form_with model: @user do |form| %>
<%= form.hidden_field :redirect_to, value: @invitation ? "home" : "onboarding_preferences" %>
<%= form.hidden_field :onboarded_at, value: Time.current if @invitation %>
<div class="space-y-4 mb-4">
<p class="text-secondary text-xs hidden md:block"><%= t(".profile_image") %></p>
<%= render "settings/user_avatar_field", form: form, user: @user %>
</div>
<div class="flex flex-col md:flex-row md:justify-between md:items-center md:gap-4 space-y-4 md:space-y-0 mb-4">
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name"), container_class: "bg-container md:w-1/2 w-full", required: true %>
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name"), container_class: "bg-container md:w-1/2 w-full", required: true %>
</div>
<% unless @invitation %>
<div class="space-y-4 mb-4">
<%= form.fields_for :family do |family_form| %>
<%= family_form.text_field :name, placeholder: t(".household_name"), label: t(".household_name") %>
<%= family_form.select :country,
country_options,
{ label: t(".country") }, required: true %>
<% end %>
</div>
<% end %>
<%= form.submit t(".submit") %>
<% end %>
</div>
</div>
<%= render "layouts/shared/footer" %>
</div>

View file

@ -1,16 +1,51 @@
<div class="bg-surface">
<div class="h-screen flex flex-col items-center py-6">
<div class="grow flex justify-center items-center flex-col max-w-sm w-full text-center">
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
<%= tag.h1 t(".title"), class: "text-3xl font-medium mb-2" %>
<%= tag.p t(".message"), class: "text-sm text-secondary mb-6" %>
<%= content_for :previous_path, onboarding_path %>
<%= render LinkComponent.new(
text: t(".setup"),
href: profile_onboarding_path,
variant: "primary",
full_width: true
) %>
<%= content_for :header_nav do %>
<%= render "onboardings/onboarding_nav", user: @user %>
<% end %>
<%= content_for :cancel_action do %>
<%= render "onboardings/logout" %>
<% end %>
<div class="grow max-w-lg w-full mx-auto bg-surface flex flex-col justify-center md:py-0 py-6 px-4 md:px-0">
<div>
<div class="space-y-1 mb-6 text-center">
<h1 class="text-2xl font-medium md:text-2xl">Let's set up your account</h1>
<p class="text-secondary text-sm">First things first, let's get your profile set up.</p>
</div>
<%= styled_form_with model: @user do |form| %>
<%= form.hidden_field :redirect_to, value: @invitation ? "home" : "onboarding_preferences" %>
<%= form.hidden_field :onboarded_at, value: Time.current if @invitation %>
<div class="mb-6">
<%= render "settings/user_avatar_field", form: form, user: @user %>
</div>
<div class="flex flex-col md:flex-row md:justify-between md:items-center md:gap-4 space-y-4 md:space-y-0 mb-4">
<%= form.text_field :first_name, placeholder: "First name", label: "First name", container_class: "bg-container md:w-1/2 w-full", required: true %>
<%= form.text_field :last_name, placeholder: "Last name", label: "Last name", container_class: "bg-container md:w-1/2 w-full", required: true %>
</div>
<% unless @invitation %>
<div class="space-y-4 mb-4">
<%= form.fields_for :family do |family_form| %>
<%= family_form.text_field :name, placeholder: "Household name", label: "Household name" %>
<%= family_form.select :country,
country_options,
{ label: "Country" },
required: true
%>
<% end %>
</div>
<% end %>
<%= form.submit "Continue" %>
<% end %>
</div>
</div>
<%= render "layouts/shared/footer" %>
</div>

View file

@ -0,0 +1,121 @@
<%= content_for :previous_path, goals_onboarding_path %>
<%= content_for :header_nav do %>
<%= render "onboardings/onboarding_nav", user: @user %>
<% end %>
<%= content_for :cancel_action do %>
<%= render "onboardings/logout" %>
<% end %>
<%= content_for :footer do %>
<%= render "layouts/shared/footer" %>
<% end %>
<div class="grow flex flex-col gap-12 items-center justify-center">
<div class="max-w-sm mx-auto flex flex-col items-center">
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
<p class="text-xl lg:text-3xl text-primary font-display font-medium">
Try Maybe for 14 days.
</p>
<h2 class="text-xl lg:text-3xl font-display text-secondary font-medium mb-2">
No credit card required
</h2>
<p class="text-sm text-secondary text-center mb-8">
Starting the trial activates your account for Maybe. You won't need to enter payment details.
</p>
<div class="w-full">
<%= render ButtonComponent.new(
text: "Try Maybe for 14 days",
href: start_trial_subscription_path,
full_width: true
) %>
</div>
</div>
<div class="space-y-8">
<h2 class="text-center text-lg lg:text-2xl font-medium text-primary">How your trial will work</h2>
<div class="flex gap-3">
<div class="rounded-xl p-1 bg-gray-400/20 theme-dark:bg-gray-500/20 flex flex-col justify-between items-center text-secondary">
<%= render FilledIconComponent.new(icon: "unlock-keyhole", variant: :inverse) %>
<%= render FilledIconComponent.new(icon: "bell", variant: :inverse) %>
<%= render FilledIconComponent.new(icon: "credit-card", variant: :inverse) %>
</div>
<div class="space-y-12">
<div class="space-y-1.5 text-sm">
<p class="text-primary font-medium">Today</p>
<p class="text-secondary">You'll get free access to Maybe for 14 days</p>
</div>
<div class="space-y-1.5 text-sm">
<p class="text-primary font-medium">In 13 days (<%= 13.days.from_now.strftime("%B %d") %>)</p>
<p class="text-secondary">We'll notify you to remind you when your trial will end.</p>
</div>
<div class="space-y-1.5 text-sm">
<p class="text-primary font-medium">In 14 days (<%= 14.days.from_now.strftime("%B %d") %>)</p>
<p class="text-secondary">Your trial ends &mdash; subscribe to continue using Maybe</p>
</div>
</div>
</div>
</div>
<div class="space-y-8 max-w-2xl mx-auto">
<h2 class="text-center text-lg lg:text-2xl font-medium text-primary">Here's what's included</h2>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-x-12 gap-y-6 text-secondary">
<div class="flex flex-col gap-4 items-center">
<%= render FilledIconComponent.new(icon: "landmark", variant: :surface) %>
<p class="text-sm text-primary text-center">More than 10,000 institutions to connect to</p>
</div>
<div class="flex flex-col gap-4 items-center">
<%= render FilledIconComponent.new(icon: "layers", variant: :surface) %>
<p class="text-sm text-primary text-center">Connect unlimited accounts and account types</p>
</div>
<div class="flex flex-col gap-4 items-center">
<%= render FilledIconComponent.new(icon: "line-chart", variant: :surface) %>
<p class="text-sm text-primary text-center">Performance and investment returns across portfolio</p>
</div>
<div class="flex flex-col gap-4 items-center">
<%= render FilledIconComponent.new(icon: "credit-card", variant: :surface) %>
<p class="text-sm text-primary text-center">Comprehensive transaction tracking experience</p>
</div>
<div class="flex flex-col items-center">
<div class="pl-2">
<%= render "chats/ai_avatar" %>
</div>
<p class="text-sm text-primary text-center">Unlimited access and chats with Maybe AI</p>
</div>
<div class="flex flex-col gap-4 items-center">
<%= render FilledIconComponent.new(icon: "keyboard", variant: :surface) %>
<p class="text-sm text-primary text-center">Manual account tracking that works well</p>
</div>
<div class="flex flex-col gap-4 items-center">
<%= render FilledIconComponent.new(icon: "globe-2", variant: :surface) %>
<p class="text-sm text-primary text-center">Multiple currencies and near global coverage</p>
</div>
<div class="flex flex-col gap-4 items-center">
<%= render FilledIconComponent.new(icon: "ship", variant: :surface) %>
<p class="text-sm text-primary text-center">Early access to newly released features</p>
</div>
<div class="flex flex-col gap-4 items-center">
<%= render FilledIconComponent.new(icon: "messages-square", variant: :surface) %>
<p class="text-sm text-primary text-center">Priority human support from team</p>
</div>
</div>
</div>
</div>