# `PhoenixKit.Users.WelcomeEmail`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.55.1/lib/phoenix_kit/users/welcome_email.ex#L1)

The welcome email: sent once, after a user confirms their address.

Off by default — the `email_welcome_enabled` setting, switched on under
Settings → Emails Transactional → Branding. Its copy is core's `welcome`
email (`PhoenixKit.Email.CoreTemplates.welcome_defaults/0`), overridable
with files like any other.

## When it is sent

`after_confirmation/1` is called where a user's own action confirms their
address:

  * `Auth.confirm_user/1` — the link in the confirmation email;
  * `Auth.confirm_user_from_external_proof/1` — signing in with a magic
    link, or with an OAuth provider that verified the address, while the
    account is unconfirmed (a new OAuth account included);
  * `Auth.update_user_email/2` — an unconfirmed account that corrects its
    address ("Wrong email?") and so confirms the new one;
  * magic-link registration, which confirms the new account itself.

An administrator confirming an account by hand (`Auth.toggle_user_confirmation/2`,
`Auth.admin_confirm_user/1`) sends nothing: the reader did not do anything,
and an operator confirming imported accounts must not mail every one of
them. `admin_confirm_user/1` stays silent for the same reason — magic-link
registration calls this module itself, after it.

## Only a real transition

The transactional paths record, before they confirm, whether the row is
still unconfirmed as their transaction sees it (`track_transition/2`, a
`SELECT … FOR NO KEY UPDATE`), and enqueue only then (`multi/1`). A confirmation
link clicked after an administrator already confirmed the account, or a
second tab holding a stale unconfirmed struct, confirms nothing new and
enqueues nothing. Magic-link registration confirms an account it has just
created, so its transition is real by construction.

## After the confirmation commits

`after_confirmation/1` only enqueues a job
(`PhoenixKit.Users.WelcomeEmailWorker`, queue `notifications`). The
confirmation link, the external-proof confirmation and the address
correction enqueue it as a step of the transaction that confirms the
address (`multi/1`), so the job exists exactly when the confirmation does:
a rollback — of the confirmation, or of a caller's transaction around it,
such as the OAuth callback's — takes the job with it. Magic-link
registration enqueues it right after `admin_confirm_user/1` has committed,
outside any transaction. Nothing is claimed and nothing is sent while a
transaction is open, and no mail server ever holds a row lock — except
under Oban's `testing: :inline`, which runs a job the moment it is
inserted (a host's test setup, never production).

## Once — at most

One job is enqueued per real transition, and before sending it claims
the account with one conditional `UPDATE` that records
`welcome_email_sent_at` in its `custom_fields` only if it is not there
yet. A confirmation repeated later (an address unconfirmed and confirmed
again) finds the mark and sends nothing. The mark is cleared for a retry
only when the email is known not to have gone out; see
`PhoenixKit.Users.WelcomeEmailWorker` for why a failure during delivery,
or a node dying between the claim and the send, loses the email instead
of risking a second one.

## Never in the way

Enqueueing the welcome email never fails a confirmation that would
otherwise succeed. A node without Oban running logs that the job could not
be enqueued. Inside a transaction the insert runs under its own savepoint,
with a two-second `lock_timeout`: if it fails — refused, or kept
waiting on a locked `oban_jobs` — the error is logged, only the savepoint
is rolled back, and the confirmation commits without a job. (A connection
lost in the middle of the transaction fails the confirmation either way.)

# `after_confirmation`

```elixir
@spec after_confirmation(PhoenixKit.Users.Auth.User.t() | term()) ::
  :enqueued | :skipped | :error
```

Enqueues the welcome email for a user who has just confirmed their
address, unless it is switched off.

Returns `:enqueued`, `:skipped` (off, not confirmed) or `:error` (logged).

# `enabled?`

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

Whether the welcome email is switched on. Off unless set.

# `multi`

```elixir
@spec multi(Ecto.Multi.t()) :: Ecto.Multi.t()
```

`after_confirmation/1` as a step of `multi`, after the step that confirms
the user (`:user`) — so the job is inserted in the confirmation's own
transaction — when `track_transition/2` saw the row unconfirmed. Without
that step nothing is enqueued.

# `sent_key`

```elixir
@spec sent_key() :: String.t()
```

The `custom_fields` key that records when the welcome email was sent.

# `setting_key`

```elixir
@spec setting_key() :: String.t()
```

The setting that switches the welcome email on: `"true"` or `"false"`.

# `track_transition`

```elixir
@spec track_transition(Ecto.Multi.t(), PhoenixKit.Users.Auth.User.t()) ::
  Ecto.Multi.t()
```

Records, as a step of `multi` placed **before** the step that confirms
`user`, whether the user's row is still unconfirmed — locked
(`FOR NO KEY UPDATE`: it serialises confirmations of the row without
blocking foreign-key checks that only share its key), so of two
transactions confirming the same row only the first sees the transition.
`multi/1` reads it.

---

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