From 9853cc021cf1a1a7f72448221ee117e90c9f9b24 Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Wed, 22 Jul 2026 02:02:39 +0300 Subject: [PATCH] Allow reliable handover code copy and paste --- assets/js/hooks.js | 64 +++++++++++++++++++ lib/who_need_help/help.ex | 10 +++ .../live/request_live/show.ex | 22 +++++-- test/who_need_help/mutual_aid_flow_test.exs | 8 +++ .../live/mutual_aid_live_test.exs | 10 +-- 5 files changed, 104 insertions(+), 10 deletions(-) diff --git a/assets/js/hooks.js b/assets/js/hooks.js index 10d8fb1..a4f79a9 100644 --- a/assets/js/hooks.js +++ b/assets/js/hooks.js @@ -204,6 +204,70 @@ export const mountStaticAidMaps = root => { } export const Hooks = { + CopyToClipboard: { + mounted() { + this.resetLabel = () => { + this.el.textContent = this.el.dataset.defaultLabel + } + + this.copy = async () => { + const value = this.el.dataset.copyValue || "" + + const fallbackCopy = () => { + const input = document.createElement("textarea") + input.value = value + input.setAttribute("readonly", "") + input.style.position = "fixed" + input.style.opacity = "0" + document.body.append(input) + input.select() + const copied = document.execCommand("copy") + input.remove() + + if (!copied) throw new Error("Clipboard copy failed") + } + + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value) + } else { + fallbackCopy() + } + this.el.textContent = this.el.dataset.copiedLabel + } catch (_error) { + try { + fallbackCopy() + this.el.textContent = this.el.dataset.copiedLabel + } catch (_fallbackError) { + this.el.textContent = this.el.dataset.errorLabel + } + } + + window.clearTimeout(this.labelTimer) + this.labelTimer = window.setTimeout(this.resetLabel, 1800) + } + + this.el.addEventListener("click", this.copy) + }, + destroyed() { + this.el.removeEventListener("click", this.copy) + window.clearTimeout(this.labelTimer) + } + }, + + HandoverCode: { + mounted() { + this.normalize = () => { + this.el.value = this.el.value.replace(/[^0-9]/g, "").slice(0, 6) + } + + this.el.addEventListener("input", this.normalize) + }, + destroyed() { + this.el.removeEventListener("input", this.normalize) + } + }, + AidMap: { mounted() { this.aidMap = createAidMap(this.el) diff --git a/lib/who_need_help/help.ex b/lib/who_need_help/help.ex index ad58e7b..49267c9 100644 --- a/lib/who_need_help/help.ex +++ b/lib/who_need_help/help.ex @@ -290,6 +290,8 @@ defmodule WhoNeedHelp.Help do end def verify_handover(%Scope{user: user}, assignment_id, code) do + code = normalize_handover_code(code) + with {:ok, assignment_id} <- cast_id(assignment_id), true <- valid_handover_code?(code), {:ok, _limit} <- Trust.authorize_action(Scope.for_user(user), :verify_handover) do @@ -619,6 +621,14 @@ defmodule WhoNeedHelp.Help do defp valid_handover_code?(_code), do: false + defp normalize_handover_code(code) when is_binary(code) do + code + |> String.slice(0, 64) + |> String.replace(~r/[^0-9]/u, "") + end + + defp normalize_handover_code(code), do: code + defp code_hash(code) when is_binary(code), do: :crypto.hash(:sha256, code) defp after_transition({:ok, assignment}) do diff --git a/lib/who_need_help_web/live/request_live/show.ex b/lib/who_need_help_web/live/request_live/show.ex index 4288507..d980f44 100644 --- a/lib/who_need_help_web/live/request_live/show.ex +++ b/lib/who_need_help_web/live/request_live/show.ex @@ -1010,9 +1010,22 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
{gettext("HANDOVER CODE FOR THE HELPER")}
-
+ {Help.handover_code(@request.id)} -
+ + <.form @@ -1028,11 +1041,10 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do field={@handover_form[:code]} label={gettext("Enter handover code")} type="text" + phx-hook="HandoverCode" inputmode="numeric" autocomplete="one-time-code" - minlength="6" - maxlength="6" - pattern="[0-9]{6}" + maxlength="32" placeholder="000000" required aria-describedby="handover-code-hint" diff --git a/test/who_need_help/mutual_aid_flow_test.exs b/test/who_need_help/mutual_aid_flow_test.exs index fbf916b..eef8741 100644 --- a/test/who_need_help/mutual_aid_flow_test.exs +++ b/test/who_need_help/mutual_aid_flow_test.exs @@ -100,6 +100,14 @@ defmodule WhoNeedHelp.MutualAidFlowTest do assert {:error, :invalid_code} = Help.verify_handover(context.helper_scope, assignment.id, code) end + + code = Help.handover_code(request.id) + pasted_code = String.slice(code, 0, 3) <> " " <> String.slice(code, 3, 3) + + assert {:ok, verified_assignment} = + Help.verify_handover(context.helper_scope, assignment.id, pasted_code) + + assert verified_assignment.handover_verified_at end test "request location labels are validated before the database write", context do diff --git a/test/who_need_help_web/live/mutual_aid_live_test.exs b/test/who_need_help_web/live/mutual_aid_live_test.exs index 376a215..e1eb03e 100644 --- a/test/who_need_help_web/live/mutual_aid_live_test.exs +++ b/test/who_need_help_web/live/mutual_aid_live_test.exs @@ -660,13 +660,13 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do {:ok, view, _html} = build_conn() |> log_in_user(helper) |> live(~p"/requests/#{request.id}") - assert has_element?( - view, - "#handover-form input[inputmode='numeric'][autocomplete='one-time-code'][pattern='[0-9]{6}']" - ) + assert has_element?(view, "#handover-form input[phx-hook='HandoverCode'][maxlength='32']") + + code = Help.handover_code(request.id) + pasted_code = String.slice(code, 0, 3) <> " " <> String.slice(code, 3, 3) view - |> form("#handover-form", handover: %{code: Help.handover_code(request.id)}) + |> form("#handover-form", handover: %{code: pasted_code}) |> render_submit() assert render(view) =~ "Handover code verified."