diff --git a/assets/js/app.ts b/assets/js/app.ts
index 1e9546461..e0da6e681 100644
--- a/assets/js/app.ts
+++ b/assets/js/app.ts
@@ -623,3 +623,23 @@ window.addEventListener("phx:js-exec", ({ detail }) => {
liveSocket.execJS(el, el.getAttribute(detail.attr));
});
});
+
+window.addEventListener("phx:open_popup", (e: CustomEvent) => {
+ const url = e.detail.url;
+ if (!url) return;
+
+ const width = e.detail.width || 600;
+ const height = e.detail.height || 600;
+ const left = e.detail.left || window.screen.width / 2 - width / 2;
+ const top = e.detail.top || window.screen.height / 2 - height / 2;
+
+ const newWindow = window.open(
+ url,
+ "oauth",
+ `width=${width},height=${height},left=${left},top=${top},toolbar=0,scrollbars=1,status=1`
+ );
+
+ if (window.focus && newWindow) {
+ newWindow.focus();
+ }
+});
diff --git a/lib/algora_web/controllers/oauth_callback_controller.ex b/lib/algora_web/controllers/oauth_callback_controller.ex
index d431777f5..b555c4cd2 100644
--- a/lib/algora_web/controllers/oauth_callback_controller.ex
+++ b/lib/algora_web/controllers/oauth_callback_controller.ex
@@ -14,44 +14,54 @@ defmodule AlgoraWeb.OAuthCallbackController do
end
end
+ def translate_error(:invalid), do: "Unable to verify your login request. Please try signing in again"
+ def translate_error(:expired), do: "Your login link has expired. Please request a new one to continue"
+ def translate_error(%Ecto.Changeset{}), do: "We were unable to fetch the necessary information from your GitHub account"
+ def translate_error(_reason), do: "We were unable to contact GitHub. Please try again later"
+
def new(conn, %{"provider" => "github", "code" => code, "state" => state}) do
- with {:ok, data} <- Github.verify_oauth_state(state),
- {:ok, info} <- Github.OAuth.exchange_access_token(code: code, state: state),
- %{info: info, primary_email: primary, emails: emails, token: token} = info,
- {:ok, user} <- Accounts.register_github_user(primary, info, emails, token) do
- conn =
- case data[:return_to] do
- nil -> conn
- return_to -> put_session(conn, :user_return_to, return_to)
- end
+ res = Github.verify_oauth_state(state)
- conn
- |> put_flash(:info, welcome_message(user))
- |> AlgoraWeb.UserAuth.log_in_user(user)
- else
- {:error, :invalid} ->
- conn
- |> put_flash(:error, "Unable to verify your login request. Please try signing in again.")
- |> redirect(to: "/")
+ socket_id =
+ case res do
+ {:ok, %{socket_id: socket_id}} -> socket_id
+ _ -> nil
+ end
- {:error, :expired} ->
- conn
- |> put_flash(:error, "Your login link has expired. Please request a new one to continue.")
- |> redirect(to: "/")
+ type = if(socket_id, do: :popup, else: :redirect)
- {:error, %Ecto.Changeset{} = changeset} ->
- Logger.debug("failed GitHub insert #{inspect(changeset.errors)}")
+ with {:ok, data} <- res,
+ {:ok, info} <- Github.OAuth.exchange_access_token(code: code, state: state),
+ %{info: info, primary_email: primary, emails: emails, token: token} = info,
+ {:ok, user} <- Accounts.register_github_user(primary, info, emails, token) do
+ if socket_id do
+ Phoenix.PubSub.broadcast(Algora.PubSub, "auth:#{socket_id}", {:authenticated, user})
+ end
- conn
- |> put_flash(:error, "We were unable to fetch the necessary information from your GitHub account")
- |> redirect(to: "/")
+ case type do
+ :popup ->
+ conn
+ |> AlgoraWeb.UserAuth.put_current_user(user)
+ |> render(:success)
+ :redirect ->
+ conn
+ |> put_flash(:info, welcome_message(user))
+ |> AlgoraWeb.UserAuth.put_current_user(user)
+ |> redirect(to: data[:return_to] || AlgoraWeb.UserAuth.signed_in_path(conn))
+ end
+ else
{:error, reason} ->
Logger.debug("failed GitHub exchange #{inspect(reason)}")
+ conn = put_flash(conn, :error, translate_error(reason))
- conn
- |> put_flash(:error, "We were unable to contact GitHub. Please try again later")
- |> redirect(to: "/")
+ case type do
+ :popup ->
+ render(conn, :error)
+
+ :redirect ->
+ redirect(conn, to: "/")
+ end
end
end
diff --git a/lib/algora_web/controllers/oauth_callback_html.ex b/lib/algora_web/controllers/oauth_callback_html.ex
new file mode 100644
index 000000000..2e18319a8
--- /dev/null
+++ b/lib/algora_web/controllers/oauth_callback_html.ex
@@ -0,0 +1,5 @@
+defmodule AlgoraWeb.OAuthCallbackHTML do
+ use AlgoraWeb, :html
+
+ embed_templates "oauth_callback_html/*"
+end
diff --git a/lib/algora_web/controllers/oauth_callback_html/error.html.heex b/lib/algora_web/controllers/oauth_callback_html/error.html.heex
new file mode 100644
index 000000000..0d212d667
--- /dev/null
+++ b/lib/algora_web/controllers/oauth_callback_html/error.html.heex
@@ -0,0 +1,4 @@
+
+
Authentication Failed
+
Please try again.
+
diff --git a/lib/algora_web/controllers/oauth_callback_html/success.html.heex b/lib/algora_web/controllers/oauth_callback_html/success.html.heex
new file mode 100644
index 000000000..3ece0e759
--- /dev/null
+++ b/lib/algora_web/controllers/oauth_callback_html/success.html.heex
@@ -0,0 +1,7 @@
+
+
+
Authentication Successful!
+
You can close this window.
+
diff --git a/lib/algora_web/controllers/user_auth.ex b/lib/algora_web/controllers/user_auth.ex
index eafed9833..cdc4b11f5 100644
--- a/lib/algora_web/controllers/user_auth.ex
+++ b/lib/algora_web/controllers/user_auth.ex
@@ -86,16 +86,20 @@ defmodule AlgoraWeb.UserAuth do
"""
def log_in_user(conn, user) do
user_return_to = get_session(conn, :user_return_to)
- conn = assign(conn, :current_user, user)
- conn =
- conn
- |> renew_session()
- |> put_session(:user_id, user.id)
- |> put_session(:last_context, user.last_context)
- |> put_session(:live_socket_id, "users_sessions:#{user.id}")
+ conn
+ |> put_current_user(user)
+ |> redirect(to: user_return_to || signed_in_path(conn))
+ end
- redirect(conn, to: user_return_to || signed_in_path(conn))
+ def put_current_user(conn, user) do
+ conn = assign(conn, :current_user, user)
+
+ conn
+ |> renew_session()
+ |> put_session(:user_id, user.id)
+ |> put_session(:last_context, user.last_context)
+ |> put_session(:live_socket_id, "users_sessions:#{user.id}")
end
defp renew_session(conn) do
diff --git a/lib/algora_web/live/swift_bounties_live.ex b/lib/algora_web/live/swift_bounties_live.ex
index 802c80146..24b721229 100644
--- a/lib/algora_web/live/swift_bounties_live.ex
+++ b/lib/algora_web/live/swift_bounties_live.ex
@@ -4,15 +4,70 @@ defmodule AlgoraWeb.SwiftBountiesLive do
import AlgoraWeb.Components.Bounties
import AlgoraWeb.Components.Footer
+ import Ecto.Changeset
import Ecto.Query
+ alias Algora.Accounts
alias Algora.Bounties
+ alias Algora.Github
alias Algora.Repo
+ alias Algora.Types.USD
+ alias Algora.Validations
+ alias Algora.Workspace
alias AlgoraWeb.Components.Logos
+ require Logger
+
+ defmodule BountyForm do
+ @moduledoc false
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ embedded_schema do
+ field :url, :string
+ field :amount, USD
+
+ embeds_one :ticket_ref, TicketRef, primary_key: false do
+ field :owner, :string
+ field :repo, :string
+ field :number, :integer
+ field :type, :string
+ end
+ end
+
+ def changeset(form, attrs \\ %{}) do
+ form
+ |> cast(attrs, [:url, :amount])
+ |> validate_required([:url, :amount])
+ |> Validations.validate_money_positive(:amount)
+ |> Validations.validate_ticket_ref(:url, :ticket_ref)
+ end
+ end
+
+ defmodule TipForm do
+ @moduledoc false
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ embedded_schema do
+ field :github_handle, :string
+ field :amount, USD
+ end
+
+ def changeset(form, attrs \\ %{}) do
+ form
+ |> cast(attrs, [:github_handle, :amount])
+ |> validate_required([:github_handle, :amount])
+ |> Validations.validate_money_positive(:amount)
+ end
+ end
+
def mount(_params, _session, socket) do
if connected?(socket) do
Bounties.subscribe()
+ Phoenix.PubSub.subscribe(Algora.PubSub, "auth:#{socket.id}")
end
socket =
@@ -23,6 +78,10 @@ defmodule AlgoraWeb.SwiftBountiesLive do
|> assign(:page_title, "Fund Swift Together")
|> assign(:page_description, "Help grow the Swift ecosystem by funding the work we all depend on.")
|> assign(:page_image, "#{AlgoraWeb.Endpoint.url()}/images/og/swift.png")
+ |> assign(:bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{})))
+ |> assign(:tip_form, to_form(TipForm.changeset(%TipForm{}, %{})))
+ |> assign(:oauth_url, Github.authorize_url(%{socket_id: socket.id}))
+ |> assign(:pending_action, nil)
|> assign_tickets()
|> assign_active_repos()
end
@@ -196,6 +255,21 @@ defmodule AlgoraWeb.SwiftBountiesLive do
+
+
+
+ Start Contributing
+
+
+ Fund Swift Development in Seconds
+
+
+ {create_bounty(assigns)}
+ {create_tip(assigns)}
+
+
+
+
@@ -464,6 +538,147 @@ defmodule AlgoraWeb.SwiftBountiesLive do
"""
end
+ defp create_bounty(assigns) do
+ ~H"""
+ <.card class="bg-muted/30">
+ <.card_header>
+
+ <.icon name="tabler-diamond" class="h-8 w-8" />
+
Post a bounty
+
+
+ <.card_content>
+ <.simple_form for={@bounty_form} phx-submit="create_bounty">
+
+ <.input
+ label="URL"
+ field={@bounty_form[:url]}
+ placeholder="https://github.com/swift-lang/swift/issues/1337"
+ />
+ <.input label="Amount" icon="tabler-currency-dollar" field={@bounty_form[:amount]} />
+
+ <.button variant="subtle">Submit
+
+
+
+
+
+ """
+ end
+
+ defp create_tip(assigns) do
+ ~H"""
+ <.card class="bg-muted/30">
+ <.card_header>
+
+ <.icon name="tabler-gift" class="h-8 w-8" />
+
Tip a developer
+
+
+ <.card_content>
+ <.simple_form for={@tip_form} phx-submit="create_tip">
+
+ <.input label="GitHub handle" field={@tip_form[:github_handle]} placeholder="jsmith" />
+ <.input label="Amount" icon="tabler-currency-dollar" field={@tip_form[:amount]} />
+
+ <.button variant="subtle">Submit
+
+
+
+
+
+ """
+ end
+
+ def handle_event("create_bounty" = event, %{"bounty_form" => params} = unsigned_params, socket) do
+ changeset =
+ %BountyForm{}
+ |> BountyForm.changeset(params)
+ |> Map.put(:action, :validate)
+
+ amount = get_field(changeset, :amount)
+ ticket_ref = get_field(changeset, :ticket_ref)
+
+ if changeset.valid? do
+ if socket.assigns[:current_user] do
+ case Bounties.create_bounty(%{
+ creator: socket.assigns.current_user,
+ owner: socket.assigns.current_user,
+ amount: amount,
+ ticket_ref: ticket_ref
+ }) do
+ {:ok, bounty} ->
+ Bounties.notify_bounty(%{owner: socket.assigns.current_user, bounty: bounty, ticket_ref: ticket_ref})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Bounty created")
+ |> push_navigate(to: ~p"/home")}
+
+ {:error, :already_exists} ->
+ {:noreply, put_flash(socket, :warning, "You have already created a bounty for this ticket")}
+
+ {:error, _reason} ->
+ {:noreply, put_flash(socket, :error, "Something went wrong")}
+ end
+ else
+ {:noreply,
+ socket
+ |> assign(:pending_action, {event, unsigned_params})
+ |> push_event("open_popup", %{url: socket.assigns.oauth_url})}
+ end
+ else
+ {:noreply, assign(socket, :bounty_form, to_form(changeset))}
+ end
+ end
+
+ def handle_event("create_tip" = event, %{"tip_form" => params} = unsigned_params, socket) do
+ changeset =
+ %TipForm{}
+ |> TipForm.changeset(params)
+ |> Map.put(:action, :validate)
+
+ if changeset.valid? do
+ if socket.assigns[:current_user] do
+ with {:ok, token} <- Accounts.get_access_token(socket.assigns.current_user),
+ {:ok, recipient} <- Workspace.ensure_user(token, get_field(changeset, :github_handle)),
+ {:ok, checkout_url} <-
+ Bounties.create_tip(%{
+ creator: socket.assigns.current_user,
+ owner: socket.assigns.current_user,
+ recipient: recipient,
+ amount: get_field(changeset, :amount)
+ }) do
+ {:noreply, redirect(socket, external: checkout_url)}
+ else
+ {:error, reason} ->
+ Logger.error("Failed to create tip: #{inspect(reason)}")
+ {:noreply, put_flash(socket, :error, "Something went wrong")}
+ end
+ else
+ {:noreply,
+ socket
+ |> assign(:pending_action, {event, unsigned_params})
+ |> push_event("open_popup", %{url: socket.assigns.oauth_url})}
+ end
+ else
+ {:noreply, assign(socket, :tip_form, to_form(changeset))}
+ end
+ end
+
+ def handle_info({:authenticated, user}, socket) do
+ socket = assign(socket, :current_user, user)
+
+ case socket.assigns.pending_action do
+ {event, params} ->
+ socket = assign(socket, :pending_action, nil)
+ handle_event(event, params, socket)
+
+ nil ->
+ {:noreply, socket}
+ end
+ end
+
def handle_info(:bounties_updated, socket) do
{:noreply, assign_tickets(socket)}
end