Skip to content

Commit 969b529

Browse files
committed
feat: make all fields optional; add continue with email option
1 parent cb5c4d3 commit 969b529

File tree

3 files changed

+225
-23
lines changed

3 files changed

+225
-23
lines changed

lib/algora/accounts/schemas/user.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ defmodule Algora.Accounts.User do
7878
field :og_image_url, :string
7979

8080
field :login_token, :string, virtual: true
81+
field :signup_token, :string, virtual: true
8182

8283
has_many :identities, Identity
8384
has_many :memberships, Member, foreign_key: :user_id
@@ -280,6 +281,10 @@ defmodule Algora.Accounts.User do
280281
cast(user, params, [:email, :login_token])
281282
end
282283

284+
def signup_changeset(%User{} = user, params) do
285+
cast(user, params, [:email, :signup_token])
286+
end
287+
283288
defp validate_email(changeset) do
284289
changeset
285290
|> validate_required([:email])

lib/algora_web/live/onboarding/dev.ex

Lines changed: 188 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ defmodule AlgoraWeb.Onboarding.DevLive do
66
import Ecto.Changeset
77
import Ecto.Query
88

9+
alias Algora.Accounts.User
910
alias Algora.Github
11+
alias Algora.Organizations
1012
alias Algora.Payments.Transaction
1113
alias Algora.Repo
1214
alias AlgoraWeb.Components.Logos
1315
alias AlgoraWeb.LocalStore
16+
alias Swoosh.Email
1417

1518
require Logger
1619

@@ -77,14 +80,18 @@ defmodule AlgoraWeb.Onboarding.DevLive do
7780
limit: 10
7881
)
7982

83+
signup_form = to_form(User.signup_changeset(%User{}, %{}))
84+
8085
{:ok,
8186
socket
87+
|> assign(:secret_code, nil)
8288
|> assign(:step, Enum.at(@steps, 0))
8389
|> assign(:steps, @steps)
8490
|> assign(:total_steps, length(@steps))
8591
|> assign(:context, context)
8692
|> assign(:transactions, transactions)
87-
|> assign(:info_form, InfoForm.init())}
93+
|> assign(:info_form, InfoForm.init())
94+
|> assign(:signup_form, signup_form)}
8895
end
8996

9097
@impl true
@@ -153,6 +160,87 @@ defmodule AlgoraWeb.Onboarding.DevLive do
153160
{:noreply, push_event(socket, "open_popup", %{url: popup_url})}
154161
end
155162

163+
@impl true
164+
def handle_event("send_signup_code", %{"user" => %{"email" => email}}, socket) do
165+
code = Nanoid.generate()
166+
167+
changeset = User.signup_changeset(%User{}, %{})
168+
169+
case send_signup_code_to_email(email, code) do
170+
{:ok, _id} ->
171+
{:noreply,
172+
socket
173+
|> LocalStore.assign_cached(:secret_code, code)
174+
|> LocalStore.assign_cached(:email, email)
175+
|> assign(:signup_form, to_form(changeset))}
176+
177+
{:error, _reason} ->
178+
# capture_error reason
179+
{:noreply,
180+
put_flash(
181+
socket,
182+
:error,
183+
"We had trouble sending mail to #{email}. Please try again"
184+
)}
185+
end
186+
187+
# case Algora.Accounts.get_user_by_email(email) do
188+
# %User{} = user ->
189+
# {:noreply, socket}
190+
191+
# nil ->
192+
# throttle()
193+
# {:noreply, put_flash(socket, :error, "Email address not found.")}
194+
# end
195+
end
196+
197+
@impl true
198+
def handle_event("send_signup_code", %{"user" => %{"signup_code" => code}}, socket) do
199+
if Plug.Crypto.secure_compare(String.trim(code), socket.assigns.secret_code) do
200+
user_handle =
201+
socket.assigns.email
202+
|> String.replace(~r/[^a-zA-Z0-9]/, "-")
203+
|> String.downcase()
204+
205+
email = socket.assigns.email
206+
207+
tech_stack = get_field(socket.assigns.info_form.source, :tech_stack) || []
208+
intentions = get_field(socket.assigns.info_form.source, :intentions) || []
209+
210+
opts = [
211+
tech_stack: tech_stack,
212+
seeking_bounties: "bounties" in intentions,
213+
seeking_contracts: "contracts" in intentions,
214+
seeking_jobs: "jobs" in intentions
215+
]
216+
217+
{:ok, user} =
218+
case Repo.get_by(User, email: email) do
219+
nil ->
220+
%User{
221+
type: :individual,
222+
last_context: "personal",
223+
handle: Organizations.ensure_unique_handle(user_handle),
224+
avatar_url: Algora.Util.get_gravatar_url(email)
225+
}
226+
|> User.signup_changeset(%{email: email})
227+
|> User.generate_id()
228+
|> change(opts)
229+
|> Repo.insert()
230+
231+
existing_user ->
232+
existing_user
233+
|> change(opts)
234+
|> Repo.update()
235+
end
236+
237+
{:noreply, redirect(socket, to: AlgoraWeb.UserAuth.generate_login_path(user.email, socket.assigns[:return_to]))}
238+
else
239+
throttle()
240+
{:noreply, put_flash(socket, :error, "Invalid signup code")}
241+
end
242+
end
243+
156244
@impl true
157245
def handle_event("prev_step", _params, socket) do
158246
current_step_index = Enum.find_index(socket.assigns.steps, &(&1 == socket.assigns.step))
@@ -197,6 +285,8 @@ defmodule AlgoraWeb.Onboarding.DevLive do
197285
end
198286
end
199287

288+
defp throttle, do: :timer.sleep(1000)
289+
200290
defp main_content(%{step: :info} = assigns) do
201291
~H"""
202292
<div class="space-y-8">
@@ -266,28 +356,78 @@ defmodule AlgoraWeb.Onboarding.DevLive do
266356

267357
defp main_content(%{step: :oauth} = assigns) do
268358
~H"""
269-
<div class="space-y-8">
270-
<div>
271-
<h2 class="mb-2 text-3xl sm:text-4xl font-semibold">
272-
Connect your GitHub
273-
</h2>
274-
<p class="mb-6 text-muted-foreground">
275-
Join our community and start earning bounties
276-
</p>
277-
278-
<p class="text-sm text-muted-foreground/75">
279-
By continuing, you agree to Algora's
280-
<.link href="/terms" class="text-foreground hover:underline">terms</.link>
281-
and <.link href="/privacy" class="text-foreground hover:underline">privacy</.link>.
282-
</p>
283-
</div>
284-
<div class="flex justify-between">
285-
<.button phx-click="prev_step" variant="secondary">
286-
Previous
287-
</.button>
288-
<.button phx-click="sign_in_with_github" class="inline-flex items-center">
289-
<Logos.github class="mr-2 h-5 w-5" /> Sign in with GitHub
359+
<div class="max-w-sm">
360+
<h2 class="mb-2 text-3xl sm:text-4xl font-semibold">
361+
Complete your signup
362+
</h2>
363+
<p class="mb-6 text-muted-foreground">
364+
Join our community and start earning bounties
365+
</p>
366+
367+
<div class="mt-8">
368+
<.button :if={!@secret_code} phx-click="sign_in_with_github" class="w-full py-5">
369+
<Logos.github class="size-5 mr-2 -ml-1 shrink-0" /> Continue with GitHub
290370
</.button>
371+
372+
<div :if={!@secret_code} class="relative mt-6">
373+
<div class="absolute inset-0 flex items-center" aria-hidden="true">
374+
<div class="w-full border-t border-muted-foreground/50"></div>
375+
</div>
376+
<div class="relative flex justify-center text-sm/6 font-medium">
377+
<span class="bg-background px-6 text-muted-foreground">or</span>
378+
</div>
379+
</div>
380+
381+
<div class="mt-4">
382+
<.simple_form
383+
:if={!@secret_code}
384+
for={@signup_form}
385+
id="send_signup_code_form"
386+
phx-submit="send_signup_code"
387+
>
388+
<div class="space-y-4">
389+
<.input
390+
field={@signup_form[:email]}
391+
type="email"
392+
label="Email"
393+
placeholder="[email protected]"
394+
required
395+
/>
396+
<.button phx-disable-with="Signing up..." class="w-full py-5" variant="secondary">
397+
Continue with email
398+
</.button>
399+
</div>
400+
</.simple_form>
401+
</div>
402+
403+
<.simple_form
404+
:if={@secret_code}
405+
for={@signup_form}
406+
id="send_signup_code_form"
407+
phx-submit="send_signup_code"
408+
>
409+
<.input field={@signup_form[:signup_code]} type="text" label="Signup code" required />
410+
<.button phx-disable-with="Signing up..." class="w-full py-5">
411+
Submit
412+
</.button>
413+
</.simple_form>
414+
</div>
415+
416+
<div class="mt-4 text-xs sm:text-sm text-muted-foreground w-full">
417+
By continuing, you agree to our
418+
<.link
419+
href={AlgoraWeb.Constants.get(:terms_url)}
420+
class="font-medium text-foreground/90 hover:text-foreground"
421+
>
422+
terms
423+
</.link>
424+
{" "} and
425+
<.link
426+
href={AlgoraWeb.Constants.get(:privacy_url)}
427+
class="font-medium text-foreground/90 hover:text-foreground"
428+
>
429+
privacy policy.
430+
</.link>
291431
</div>
292432
</div>
293433
"""
@@ -341,4 +481,30 @@ defmodule AlgoraWeb.Onboarding.DevLive do
341481
<% end %>
342482
"""
343483
end
484+
485+
@from_name "Algora"
486+
@from_email "[email protected]"
487+
488+
defp send_signup_code_to_email(email, code) do
489+
email =
490+
Email.new()
491+
|> Email.to(email)
492+
|> Email.from({@from_name, @from_email})
493+
|> Email.subject("Signup code for Algora")
494+
|> Email.text_body("""
495+
Here is your signup code for Algora!
496+
497+
#{code}
498+
499+
If you didn't request this link, you can safely ignore this email.
500+
501+
--------------------------------------------------------------------------------
502+
503+
For correspondence, please email the Algora founders at [email protected] and [email protected]
504+
505+
© 2025 Algora PBC.
506+
""")
507+
508+
Algora.Mailer.deliver(email)
509+
end
344510
end

lib/algora_web/live/user/dashboard_live.ex

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule AlgoraWeb.User.DashboardLive do
1010
alias Algora.Accounts
1111
alias Algora.Accounts.User
1212
alias Algora.Bounties
13+
alias Algora.Github
1314
alias Algora.Payments
1415
alias Algora.Payments.Account
1516
alias Algora.Repo
@@ -86,8 +87,10 @@ defmodule AlgoraWeb.User.DashboardLive do
8687

8788
socket =
8889
socket
90+
|> assign(:authorize_url, Github.authorize_url())
8991
|> assign(:view_mode, "compact")
9092
|> assign(:contracts, contracts)
93+
|> assign(:has_fresh_token?, Accounts.has_fresh_token?(socket.assigns.current_user))
9194
|> assign(:has_active_account, has_active_account)
9295
|> assign(:settings_form, settings_form)
9396
|> assign(:availability_form, availability_form)
@@ -103,7 +106,26 @@ defmodule AlgoraWeb.User.DashboardLive do
103106
~H"""
104107
<div class="flex lg:flex-row flex-col-reverse">
105108
<div class="flex-1 bg-background text-foreground lg:pr-96">
106-
<div :if={not @has_active_account} class="p-4 sm:p-6 md:p-8">
109+
<div :if={not @has_fresh_token?} class="p-4 sm:p-6 md:p-8">
110+
<.section>
111+
<.card>
112+
<.card_header>
113+
<.card_title>Connect GitHub account</.card_title>
114+
<.card_description>
115+
Connect your GitHub account to personalize your experience and discover more opportunities
116+
</.card_description>
117+
</.card_header>
118+
<.card_content>
119+
<div class="flex flex-col gap-3">
120+
<.button href={@authorize_url} class="ml-auto">
121+
Connect with GitHub <.icon name="tabler-arrow-right" class="w-4 h-4 ml-2 -mr-1" />
122+
</.button>
123+
</div>
124+
</.card_content>
125+
</.card>
126+
</.section>
127+
</div>
128+
<div :if={@has_fresh_token? and not @has_active_account} class="p-4 sm:p-6 md:p-8">
107129
<.section>
108130
<.card>
109131
<.card_header>
@@ -311,6 +333,7 @@ defmodule AlgoraWeb.User.DashboardLive do
311333
defp assign_achievements(socket) do
312334
achievements = [
313335
{&personalize_status/1, "Personalize Algora", nil},
336+
{&connect_github_status/1, "Connect Github account", socket.assigns.authorize_url},
314337
{&setup_stripe_status/1, "Create Stripe account", ~p"/user/transactions"},
315338
{&earn_first_bounty_status/1, "Earn first bounty", ~p"/bounties"},
316339
{&share_with_friend_status/1, "Share Algora with a friend", nil}
@@ -340,6 +363,14 @@ defmodule AlgoraWeb.User.DashboardLive do
340363

341364
defp personalize_status(_socket), do: :completed
342365

366+
defp connect_github_status(socket) do
367+
if socket.assigns.has_fresh_token? do
368+
:completed
369+
else
370+
:upcoming
371+
end
372+
end
373+
343374
defp setup_stripe_status(socket) do
344375
if socket.assigns.has_active_account do
345376
:completed

0 commit comments

Comments
 (0)