1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-05 13:35:21 +02:00

Family invites (#1397)

* Initial pass at household invites

* Invitee setup

* Clean up add member form

* Lint and other tweaks

* Security cleanup

* Lint

* i18n fixes

* More i18n cleanup

* Show pending invites

* Don't use turbo on the form

* Improved email design

* Basic tests

* Lint

* Update onboardings_controller.rb

* Registration + invite cleanup

* Lint

* Update brakeman.ignore

* Update brakeman.ignore

* Self host invite links

* Test tweaks

* Address missing param error
This commit is contained in:
Josh Pigford 2024-11-01 10:23:27 -05:00 committed by GitHub
parent 09b269273a
commit 793bd852a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 502 additions and 45 deletions

View file

@ -0,0 +1,11 @@
<h1><%= t(".greeting") %></h1>
<p>
<%= t(".body",
inviter: @invitation.inviter.display_name,
family: @invitation.family.name).html_safe %>
</p>
<%= link_to t(".accept_button"), @accept_url, class: "button" %>
<p class="footer"><%= t(".expiry_notice", days: 3) %></p>

View file

@ -0,0 +1,20 @@
<%= modal_form_wrapper title: t(".title"), subtitle: t(".subtitle") do %>
<%= styled_form_with model: @invitation, class: "space-y-4", data: { turbo: false } do |form| %>
<%= form.email_field :email,
required: true,
placeholder: t(".email_placeholder"),
label: t(".email_label") %>
<%= form.select :role,
options_for_select([
[t(".role_member"), "member"],
[t(".role_admin"), "admin"]
]),
{},
{ label: t(".role_label") } %>
<div class="w-full">
<%= form.submit t(".submit"), class: "bg-gray-900 text-white rounded-lg px-4 py-2 w-full" %>
</div>
<% end %>
<% end %>

View file

@ -2,12 +2,56 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* Email styles need to be inline */
/* Email-safe styles that work across clients */
body {
background-color: #f8fafc;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.5;
margin: 0;
padding: 0;
}
.container {
background-color: #ffffff;
border-radius: 8px;
margin: 20px auto;
max-width: 600px;
padding: 32px;
text-align: center;
}
h1 {
color: #1e293b;
font-size: 24px;
margin-bottom: 24px;
}
p {
color: #475569;
font-size: 16px;
margin-bottom: 16px;
}
.button {
background-color: #3b82f6;
border-radius: 6px;
color: #ffffff;
display: inline-block;
font-weight: 600;
margin: 16px 0;
padding: 12px 24px;
text-decoration: none;
}
.footer {
color: #64748b;
font-size: 14px;
margin-top: 32px;
text-align: center;
}
</style>
</head>
<body>
<%= yield %>
<div class="container">
<%= yield %>
</div>
</body>
</html>

View file

@ -9,7 +9,8 @@
</div>
<%= styled_form_with model: @user do |form| %>
<%= form.hidden_field :redirect_to, value: "onboarding_preferences" %>
<%= 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-gray-500 text-xs"><%= t(".profile_image") %></p>
@ -20,16 +21,17 @@
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name"), container_class: "bg-white w-1/2", required: true %>
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name"), container_class: "bg-white w-1/2", 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") %>
<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>
<%= family_form.select :country,
country_options,
{ label: t(".country") }, required: true %>
<% end %>
</div>
<% end %>
<%= form.submit t(".submit") %>
<% end %>

View file

@ -1,5 +1,5 @@
<%
header_title t(".title")
header_title @invitation ? t(".join_family_title", family: @invitation.family.name) : t(".title")
%>
<% if self_hosted_first_login? %>
@ -7,14 +7,29 @@
<h2 class="font-bold text-xl"><%= t(".welcome_title") %></h2>
<p class="text-gray-500 text-sm"><%= t(".welcome_body") %></p>
</div>
<% elsif @invitation %>
<div class="space-y-1 mb-6 text-center">
<p class="text-gray-500">
<%= t(".invitation_message",
inviter: @invitation.inviter.display_name,
role: t(".role_#{@invitation.role}")) %>
</p>
</div>
<% end %>
<%= styled_form_with model: @user, url: registration_path, class: "space-y-4" do |form| %>
<%= form.email_field :email, autofocus: false, autocomplete: "email", required: "required", placeholder: "you@example.com", label: true %>
<%= form.email_field :email,
autofocus: false,
autocomplete: "email",
required: "required",
placeholder: "you@example.com",
label: true,
disabled: @invitation.present? %>
<%= form.password_field :password, autocomplete: "new-password", required: "required", label: true %>
<%= form.password_field :password_confirmation, autocomplete: "new-password", required: "required", label: true %>
<% if invite_code_required? %>
<% if invite_code_required? && !@invitation %>
<%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %>
<% end %>
<%= form.hidden_field :invitation, value: @invitation&.token %>
<%= form.submit t(".submit") %>
<% end %>

View file

@ -34,15 +34,60 @@
<div class="px-4 py-2">
<p class="uppercase text-xs text-gray-500 font-medium"><%= Current.family.name %> &middot; <%= Current.family.users.size %></p>
</div>
<div class="flex gap-2 items-center bg-white p-4 border border-alpha-black-25 rounded-lg">
<div class="mr-1 flex justify-center items-center bg-gray-50 w-8 h-8 rounded-full border border-alpha-black-25">
<p class="uppercase text-xs text-gray-500"><%= Current.user.initial %></p>
<% @users.each do |user| %>
<div class="flex gap-2 items-center bg-white p-4 border border-alpha-black-25 rounded-lg">
<div class="w-9 h-9 shrink-0">
<%= render "settings/user_avatar", user: user %>
</div>
<p class="text-gray-900 font-medium text-sm"><%= user.display_name %></p>
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
<p class="uppercase text-gray-500 font-medium text-xs"><%= user.role %></p>
</div>
</div>
<p class="text-gray-900 font-medium text-sm"><%= Current.user.display_name %></p>
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
<p class="uppercase text-gray-500 font-medium text-xs"><%= Current.user.role %></p>
</div>
</div>
<% end %>
<% if @pending_invitations.any? %>
<% @pending_invitations.each do |invitation| %>
<div class="flex gap-2 items-center justify-between bg-white p-4 border border-alpha-black-25 rounded-lg">
<div class="flex gap-2 items-center">
<div class="w-9 h-9 shrink-0">
<div class="text-white w-full h-full bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= invitation.email[0] %></div>
</div>
<div class="flex">
<p class="text-gray-900 font-medium text-sm"><%= invitation.email %></p>
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
<p class="uppercase text-gray-500 font-medium text-xs"><%= t(".pending") %></p>
</div>
</div>
</div>
<% if self_hosted? %>
<div class="flex items-center gap-2" data-controller="clipboard">
<p class="text-gray-500 text-sm"><%= t(".invitation_link") %></p>
<span data-clipboard-target="source" class="hidden"><%= accept_invitation_url(invitation.token) %></span>
<input type="text"
readonly
value="<%= accept_invitation_url(invitation.token) %>"
class="text-sm bg-gray-50 px-2 py-1 rounded border border-gray-200 w-72">
<button data-action="clipboard#copy" class="text-gray-500 hover:text-gray-700">
<span data-clipboard-target="iconDefault">
<%= lucide_icon "copy", class: "w-5 h-5" %>
</span>
<span class="hidden" data-clipboard-target="iconSuccess">
<%= lucide_icon "check", class: "w-5 h-5" %>
</span>
</button>
</div>
<% end %>
</div>
<% end %>
<% end %>
<% if Current.user.admin? %>
<%= link_to new_invitation_path,
class: "bg-gray-100 flex items-center justify-center gap-2 text-gray-500 mt-1 hover:bg-gray-200 rounded-lg px-4 py-2 w-full text-center",
data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
<%= t(".invite_member") %>
<% end %>
<% end %>
</div>
</div>
<% end %>