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 = {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1010,9 +1010,22 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
<div class="text-xs opacity-65">
|
||||
{gettext("HANDOVER CODE FOR THE HELPER")}
|
||||
</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)}
|
||||
</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>
|
||||
|
||||
<.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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user