Packages
phoenix_kit
1.7.113
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/notifications/prefs.ex
defmodule PhoenixKit.Notifications.Prefs do
@moduledoc """
Per-user notification preferences.
Preferences live inside the user's existing `custom_fields` JSONB column
under the `"notification_preferences"` key — a flat `%{type_key =>
boolean}` map. Unset keys default to the type's own `:default` flag via
`PhoenixKit.Notifications.Types.default_for/1`, so behaviour before a
user has opted in anywhere is unchanged.
The filter function `user_wants?/2` is called once per notification
fan-out from `PhoenixKit.Notifications.maybe_create_from_activity/1`. It's
designed to fail open: any lookup error, unknown action, or malformed
prefs map returns `true` so the system never silently drops a
notification due to a bad row.
"""
require Logger
alias PhoenixKit.Notifications.Types
alias PhoenixKit.Users.Auth
alias PhoenixKit.Users.Auth.User
@prefs_key "notification_preferences"
@doc """
Returns the user's raw preference map.
Accepts either a loaded `%User{}` (zero DB work) or a UUID (one lookup).
Missing preferences return `%{}`.
"""
@spec get(User.t() | String.t()) :: %{optional(String.t()) => boolean()}
def get(%User{custom_fields: fields}) when is_map(fields) do
case Map.get(fields, @prefs_key) do
map when is_map(map) -> map
_ -> %{}
end
end
def get(%User{}), do: %{}
def get(uuid) when is_binary(uuid) do
case Auth.get_user(uuid) do
%User{} = user -> get(user)
_ -> %{}
end
end
@doc """
Saves a preference map into the user's `custom_fields`.
`prefs` is the full map of `%{type_key => boolean}` — callers should
include entries for every rendered toggle, since the storage is a
replace, not a merge-at-the-key level. Other custom-field keys are
preserved (we merge at the `custom_fields` level, not inside it).
"""
@spec update(User.t(), %{optional(String.t()) => boolean()}) ::
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
def update(%User{} = user, prefs) when is_map(prefs) do
sanitized =
prefs
|> Enum.reduce(%{}, fn
{k, v}, acc when is_binary(k) -> Map.put(acc, k, !!v)
_, acc -> acc
end)
merged = Map.put(user.custom_fields || %{}, @prefs_key, sanitized)
Auth.update_user_custom_fields(user, merged)
end
@doc """
Answers "would this user want a notification for this action?"
Returns `true` on any ambiguity so new actions, unknown prefs, or a
failing user lookup never cause a silent drop.
Order of checks:
1. Unknown action (no registered type) → `true` (fail open)
2. Prefs map has the type key set to `true` or `false` → that value
3. No entry → the type's `:default` from `Types.default_for/1`
4. Any raise → logged as a warning and returns `true`
"""
@spec user_wants?(String.t(), String.t()) :: boolean()
def user_wants?(user_uuid, action) when is_binary(user_uuid) and is_binary(action) do
do_user_wants?(user_uuid, action)
rescue
e ->
Logger.warning("Notifications.Prefs.user_wants? crashed: #{inspect(e)}")
true
end
defp do_user_wants?(user_uuid, action) do
case Types.type_for_action(action) do
nil ->
true
type_key ->
prefs = get(user_uuid)
case Map.get(prefs, type_key) do
true -> true
false -> false
_ -> Types.default_for(type_key)
end
end
end
end