# `PhoenixKit.Users.QrLogin`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.13.9/lib/phoenix_kit/users/qr_login.ex#L1)

QR device-handoff login ("scan to sign in") for PhoenixKit.

This is the PhoenixKit-side integration of the [`keyfob`](https://hex.pm/packages/keyfob)
library. Keyfob owns the rendezvous — request lifecycle, expiry,
single-use enforcement, the secret split, PubSub signaling. This module
wires it into PhoenixKit: the internal PubSub, the `qr_login_enabled`
setting, device-metadata extraction for the confirm screen, and activity
logging on approval.

## The flow

1. A signed-out browser opens `/users/qr-login`
   (`PhoenixKitWeb.Users.QrLogin`) and a QR encoding the phone-confirm URL
   is shown. The LiveView waits.
2. The user's already-signed-in phone scans it, opens the confirm URL
   (`PhoenixKitWeb.Users.QrLoginConfirm`), reviews the requesting device,
   and taps Approve — which calls `approve/2` with the phone user's uuid.
3. Approval mints a one-time login token delivered over PubSub only to
   the waiting browser, which navigates to the completion controller
   (`PhoenixKitWeb.Users.QrLoginComplete`).
4. The controller calls `consume/1` (`{:ok, user_uuid}`, exactly once) and
   logs that user in with PhoenixKit's own session machinery.

Approval always happens on the trusted (phone) device; the browser gets
nothing until the phone approves. See `keyfob`'s docs for the threat
model (QR-jacking, the secret split).

All keyfob calls are routed through PhoenixKit's internal PubSub
(`:phoenix_kit_internal_pubsub`) so the desktop LiveView and
the phone confirm LiveView rendezvous on the same bus. The default
`Keyfob.Store.ETS` (started in `PhoenixKit.Supervisor`) holds requests.

# `approve`

See `Keyfob.approve/3`. `user_ref` is the approving user's uuid.

# `consume`

See `Keyfob.consume/2`. Returns `{:ok, user_uuid}` exactly once.

# `create_request`

See `Keyfob.create_request/1`. Merges the shared PubSub option.

# `deny`

See `Keyfob.deny/2`.

# `device_meta`

```elixir
@spec device_meta(Phoenix.LiveView.Socket.t()) :: map()
```

Extracts a device-description map from the requesting browser's connected
socket, shown verbatim on the phone confirm screen so the human can
recognise (or reject) the sign-in.

Keys: `:browser`, `:os`, `:ip`, `:location` — any of which may be absent
when the underlying connect-info (or geo lookup) is unavailable — plus
`:requested_at`, an absolute UTC timestamp of when the code was minted so
the approver can sanity-check "did I just do this?".

The geo lookup is a best-effort, timeout-bounded call on the requesting
browser's IP (same machinery registration/login-alerts already use); it
runs at QR-mint time so the location is baked into the request the phone
later reads.

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

Whether QR device-handoff login is enabled (setting `qr_login_enabled`,
default `false`).

# `keyfob_opts`

```elixir
@spec keyfob_opts() :: keyword()
```

Shared keyfob options — routes every call through PhoenixKit's internal PubSub.

# `location_for`

```elixir
@spec location_for(String.t() | nil) :: String.t() | nil
```

Formats a best-effort `"City, Country"` (or just `"Country"`) string for
an IP, or `nil` when the lookup fails, is unavailable, or is still running
after 1500ms. Never raises — a geo backend hiccup must
not crash (or stall) the QR mint that shows the code.

# `log_approval`

```elixir
@spec log_approval(map(), map()) :: :ok
```

Records a `user.qr_login_approved` activity for the approving user.

Actor and target are the same user (they approved their own sign-in), so
no notification is generated — this is an audit-trail entry only. Errors
are swallowed: a logging failure must never block the sign-in.

# `peek`

See `Keyfob.peek/2`.

# `pubsub`

```elixir
@spec pubsub() :: atom()
```

The PubSub name every keyfob call in this integration uses.

# `subscribe`

See `Keyfob.subscribe/2`.

# `unsubscribe`

See `Keyfob.unsubscribe/2`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
