Allow reliable handover code copy and paste
Some checks are pending
Quality / full-local-gates (push) Waiting to run
Some checks are pending
Quality / full-local-gates (push) Waiting to run
This commit is contained in:
parent
66802bfa7a
commit
9853cc021c
|
|
@ -204,6 +204,70 @@ export const mountStaticAidMaps = root => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Hooks = {
|
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: {
|
AidMap: {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.aidMap = createAidMap(this.el)
|
this.aidMap = createAidMap(this.el)
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,8 @@ defmodule WhoNeedHelp.Help do
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_handover(%Scope{user: user}, assignment_id, code) do
|
def verify_handover(%Scope{user: user}, assignment_id, code) do
|
||||||
|
code = normalize_handover_code(code)
|
||||||
|
|
||||||
with {:ok, assignment_id} <- cast_id(assignment_id),
|
with {:ok, assignment_id} <- cast_id(assignment_id),
|
||||||
true <- valid_handover_code?(code),
|
true <- valid_handover_code?(code),
|
||||||
{:ok, _limit} <- Trust.authorize_action(Scope.for_user(user), :verify_handover) do
|
{: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 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 code_hash(code) when is_binary(code), do: :crypto.hash(:sha256, code)
|
||||||
|
|
||||||
defp after_transition({:ok, assignment}) do
|
defp after_transition({:ok, assignment}) do
|
||||||
|
|
|
||||||
|
|
@ -1010,9 +1010,22 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
<div class="text-xs opacity-65">
|
<div class="text-xs opacity-65">
|
||||||
{gettext("HANDOVER CODE FOR THE HELPER")}
|
{gettext("HANDOVER CODE FOR THE HELPER")}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 font-mono text-3xl font-black tracking-[.25em]">
|
<code class="mt-1 block select-all font-mono text-3xl font-black tracking-normal">
|
||||||
{Help.handover_code(@request.id)}
|
{Help.handover_code(@request.id)}
|
||||||
</div>
|
</code>
|
||||||
|
<button
|
||||||
|
id="copy-handover-code"
|
||||||
|
type="button"
|
||||||
|
phx-hook="CopyToClipboard"
|
||||||
|
data-copy-value={Help.handover_code(@request.id)}
|
||||||
|
data-default-label={gettext("Copy code")}
|
||||||
|
data-copied-label={gettext("Copied")}
|
||||||
|
data-error-label={gettext("Copy failed")}
|
||||||
|
class="btn btn-sm btn-outline mt-3"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{gettext("Copy code")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.form
|
<.form
|
||||||
|
|
@ -1028,11 +1041,10 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
field={@handover_form[:code]}
|
field={@handover_form[:code]}
|
||||||
label={gettext("Enter handover code")}
|
label={gettext("Enter handover code")}
|
||||||
type="text"
|
type="text"
|
||||||
|
phx-hook="HandoverCode"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
autocomplete="one-time-code"
|
autocomplete="one-time-code"
|
||||||
minlength="6"
|
maxlength="32"
|
||||||
maxlength="6"
|
|
||||||
pattern="[0-9]{6}"
|
|
||||||
placeholder="000000"
|
placeholder="000000"
|
||||||
required
|
required
|
||||||
aria-describedby="handover-code-hint"
|
aria-describedby="handover-code-hint"
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,14 @@ defmodule WhoNeedHelp.MutualAidFlowTest do
|
||||||
assert {:error, :invalid_code} =
|
assert {:error, :invalid_code} =
|
||||||
Help.verify_handover(context.helper_scope, assignment.id, code)
|
Help.verify_handover(context.helper_scope, assignment.id, code)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
test "request location labels are validated before the database write", context do
|
test "request location labels are validated before the database write", context do
|
||||||
|
|
|
||||||
|
|
@ -660,13 +660,13 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
||||||
{:ok, view, _html} =
|
{:ok, view, _html} =
|
||||||
build_conn() |> log_in_user(helper) |> live(~p"/requests/#{request.id}")
|
build_conn() |> log_in_user(helper) |> live(~p"/requests/#{request.id}")
|
||||||
|
|
||||||
assert has_element?(
|
assert has_element?(view, "#handover-form input[phx-hook='HandoverCode'][maxlength='32']")
|
||||||
view,
|
|
||||||
"#handover-form input[inputmode='numeric'][autocomplete='one-time-code'][pattern='[0-9]{6}']"
|
code = Help.handover_code(request.id)
|
||||||
)
|
pasted_code = String.slice(code, 0, 3) <> " " <> String.slice(code, 3, 3)
|
||||||
|
|
||||||
view
|
view
|
||||||
|> form("#handover-form", handover: %{code: Help.handover_code(request.id)})
|
|> form("#handover-form", handover: %{code: pasted_code})
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
assert render(view) =~ "Handover code verified."
|
assert render(view) =~ "Handover code verified."
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user