diff --git a/bats/core/notifications/notifications.bats b/bats/core/notifications/notifications.bats index 11fac536d5..421a51b523 100644 --- a/bats/core/notifications/notifications.bats +++ b/bats/core/notifications/notifications.bats @@ -16,7 +16,36 @@ setup_file() { jq -n \ '{input: { channel: "PUSH" }}') - exec_graphql "$token_name" 'account-disable-notification-channel-alt' "$variables" - channel_enabled="$(graphql_output '.data.accountDisableNotificationChannelAlt.notificationSettings.push.enabled')" + exec_graphql "$token_name" 'user-disable-notification-channel' "$variables" + channel_enabled="$(graphql_output '.data.userDisableNotificationChannel.notificationSettings.push.enabled')" + [[ "$channel_enabled" == "false" ]] || exit 1 + + # Ensure notification settings exist on user + exec_graphql "$token_name" 'user-notification-settings' + user_channel_enabled="$(graphql_output '.data.me.notificationSettings.push.enabled')" + + [[ "$user_channel_enabled" == "false" ]] || exit 1 + + + exec_graphql "$token_name" 'user-enable-notification-channel' "$variables" + channel_enabled="$(graphql_output '.data.userEnableNotificationChannel.notificationSettings.push.enabled')" + [[ "$channel_enabled" == "true" ]] || exit 1 +} + +@test "notifications: disable/enable notification category" { + token_name='alice' + + variables=$( + jq -n \ + '{input: { channel: "PUSH", category: "CIRCLES" }}') + + exec_graphql "$token_name" 'user-disable-notification-category' "$variables" + disabled_category="$(graphql_output '.data.userDisableNotificationCategory.notificationSettings.push.disabledCategories[0]')" + + [[ "$disabled_category" == "CIRCLES" ]] || exit 1 + + exec_graphql "$token_name" 'user-enable-notification-category' "$variables" + disabled_length="$(graphql_output '.data.userEnableNotificationCategory.notificationSettings.push.disabledCategories | length')" + [[ "$disabled_length" == "0" ]] || exit 1 } diff --git a/bats/gql/account-disable-notification-channel-alt.gql b/bats/gql/account-disable-notification-channel-alt.gql deleted file mode 100644 index fb2fea641a..0000000000 --- a/bats/gql/account-disable-notification-channel-alt.gql +++ /dev/null @@ -1,10 +0,0 @@ -mutation accountDisableNotificationChannel($input: AccountDisableNotificationChannelInputAlt!) { - accountDisableNotificationChannelAlt(input: $input) { - notificationSettings { - push { - enabled - disabledCategories - } - } - } -} diff --git a/bats/gql/user-disable-notification-category.gql b/bats/gql/user-disable-notification-category.gql new file mode 100644 index 0000000000..5a7ac5c7e6 --- /dev/null +++ b/bats/gql/user-disable-notification-category.gql @@ -0,0 +1,12 @@ +mutation userDisableNotificationCategory( + $input: UserDisableNotificationCategoryInput! +) { + userDisableNotificationCategory(input: $input) { + notificationSettings { + push { + enabled + disabledCategories + } + } + } +} diff --git a/bats/gql/user-disable-notification-channel.gql b/bats/gql/user-disable-notification-channel.gql new file mode 100644 index 0000000000..b9ad7df580 --- /dev/null +++ b/bats/gql/user-disable-notification-channel.gql @@ -0,0 +1,12 @@ +mutation userDisableNotificationChannel( + $input: UserDisableNotificationChannelInput! +) { + userDisableNotificationChannel(input: $input) { + notificationSettings { + push { + enabled + disabledCategories + } + } + } +} diff --git a/bats/gql/user-enable-notification-category.gql b/bats/gql/user-enable-notification-category.gql new file mode 100644 index 0000000000..e613b1b742 --- /dev/null +++ b/bats/gql/user-enable-notification-category.gql @@ -0,0 +1,12 @@ +mutation userEnableNotificationCategory( + $input: UserEnableNotificationCategoryInput! +) { + userEnableNotificationCategory(input: $input) { + notificationSettings { + push { + enabled + disabledCategories + } + } + } +} diff --git a/bats/gql/user-enable-notification-channel.gql b/bats/gql/user-enable-notification-channel.gql new file mode 100644 index 0000000000..d52dfdd58f --- /dev/null +++ b/bats/gql/user-enable-notification-channel.gql @@ -0,0 +1,12 @@ +mutation userEnableNotificationChannel( + $input: UserEnableNotificationChannelInput! +) { + userEnableNotificationChannel(input: $input) { + notificationSettings { + push { + enabled + disabledCategories + } + } + } +} diff --git a/bats/gql/user-notification-settings.gql b/bats/gql/user-notification-settings.gql new file mode 100644 index 0000000000..f412ddd833 --- /dev/null +++ b/bats/gql/user-notification-settings.gql @@ -0,0 +1,10 @@ +query userNotificationSettings { + me { + id + notificationSettings { + push { + enabled + } + } + } +} diff --git a/core/notifications/.sqlx/query-0d5f56e2f99fb2d8e60e63d1bc16342b53b6aff5793ea99caac3c7cb09139762.json b/core/notifications/.sqlx/query-0d5f56e2f99fb2d8e60e63d1bc16342b53b6aff5793ea99caac3c7cb09139762.json deleted file mode 100644 index 736c70b3db..0000000000 --- a/core/notifications/.sqlx/query-0d5f56e2f99fb2d8e60e63d1bc16342b53b6aff5793ea99caac3c7cb09139762.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO account_notification_settings (id, galoy_account_id)\n VALUES ($1, $2) ON CONFLICT DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Varchar" - ] - }, - "nullable": [] - }, - "hash": "0d5f56e2f99fb2d8e60e63d1bc16342b53b6aff5793ea99caac3c7cb09139762" -} diff --git a/core/notifications/.sqlx/query-94f56f9bf9625b0109f038f4157d3f3a3ef55c10681f8d4e0606e4c24c8259f4.json b/core/notifications/.sqlx/query-38788b6bfb3d437147ea78bcada68610b5278cb86bd649474d4eb663e98f4497.json similarity index 59% rename from core/notifications/.sqlx/query-94f56f9bf9625b0109f038f4157d3f3a3ef55c10681f8d4e0606e4c24c8259f4.json rename to core/notifications/.sqlx/query-38788b6bfb3d437147ea78bcada68610b5278cb86bd649474d4eb663e98f4497.json index 0c1b757bc6..0502a54422 100644 --- a/core/notifications/.sqlx/query-94f56f9bf9625b0109f038f4157d3f3a3ef55c10681f8d4e0606e4c24c8259f4.json +++ b/core/notifications/.sqlx/query-38788b6bfb3d437147ea78bcada68610b5278cb86bd649474d4eb663e98f4497.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT a.id, e.sequence, e.event\n FROM account_notification_settings a\n JOIN account_notification_settings_events e ON a.id = e.id\n WHERE a.galoy_account_id = $1\n ORDER BY e.sequence", + "query": "SELECT a.id, e.sequence, e.event\n FROM user_notification_settings a\n JOIN user_notification_settings_events e ON a.id = e.id\n WHERE a.galoy_user_id = $1\n ORDER BY e.sequence", "describe": { "columns": [ { @@ -30,5 +30,5 @@ false ] }, - "hash": "94f56f9bf9625b0109f038f4157d3f3a3ef55c10681f8d4e0606e4c24c8259f4" + "hash": "38788b6bfb3d437147ea78bcada68610b5278cb86bd649474d4eb663e98f4497" } diff --git a/core/notifications/.sqlx/query-529104c49014a0c003388fb84a45073a1b9178a3da720708b860c2c3a626bd8b.json b/core/notifications/.sqlx/query-529104c49014a0c003388fb84a45073a1b9178a3da720708b860c2c3a626bd8b.json new file mode 100644 index 0000000000..6182c0981c --- /dev/null +++ b/core/notifications/.sqlx/query-529104c49014a0c003388fb84a45073a1b9178a3da720708b860c2c3a626bd8b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_notification_settings (id, galoy_user_id)\n VALUES ($1, $2) ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "529104c49014a0c003388fb84a45073a1b9178a3da720708b860c2c3a626bd8b" +} diff --git a/core/notifications/migrations/20240111102028_notifications_setup.sql b/core/notifications/migrations/20240111102028_notifications_setup.sql index 09b8113e06..0151cb0850 100644 --- a/core/notifications/migrations/20240111102028_notifications_setup.sql +++ b/core/notifications/migrations/20240111102028_notifications_setup.sql @@ -1,11 +1,11 @@ -CREATE TABLE account_notification_settings ( +CREATE TABLE user_notification_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - galoy_account_id VARCHAR UNIQUE NOT NULL, + galoy_user_id VARCHAR UNIQUE NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE TABLE account_notification_settings_events ( - id UUID REFERENCES account_notification_settings(id) NOT NULL, +CREATE TABLE user_notification_settings_events ( + id UUID REFERENCES user_notification_settings(id) NOT NULL, sequence INT NOT NULL, event_type VARCHAR NOT NULL, event JSONB NOT NULL, diff --git a/core/notifications/src/account_notification_settings/entity.rs b/core/notifications/src/account_notification_settings/entity.rs deleted file mode 100644 index 06f08c805e..0000000000 --- a/core/notifications/src/account_notification_settings/entity.rs +++ /dev/null @@ -1,238 +0,0 @@ -use derive_builder::Builder; -use es_entity::*; -use serde::{Deserialize, Serialize}; - -use std::collections::HashSet; - -use crate::primitives::*; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum AccountNotificationSettingsEvent { - Initialized { - id: AccountNotificationSettingsId, - galoy_account_id: GaloyAccountId, - }, - ChannelDisabled { - channel: NotificationChannel, - }, - ChannelEnabled { - channel: NotificationChannel, - }, - CategoryDisabled { - channel: NotificationChannel, - category: NotificationCategory, - }, - CategoryEnabled { - channel: NotificationChannel, - category: NotificationCategory, - }, -} - -impl EntityEvent for AccountNotificationSettingsEvent { - type EntityId = AccountNotificationSettingsId; - fn event_table_name() -> &'static str { - "account_notification_settings_events" - } -} - -#[derive(Builder)] -#[builder(pattern = "owned", build_fn(error = "EntityError"))] -pub struct AccountNotificationSettings { - pub id: AccountNotificationSettingsId, - pub galoy_account_id: GaloyAccountId, - pub(super) events: EntityEvents, -} - -impl EsEntity for AccountNotificationSettings { - type Event = AccountNotificationSettingsEvent; -} - -impl AccountNotificationSettings { - pub fn new(galoy_account_id: GaloyAccountId) -> Self { - let id = AccountNotificationSettingsId::new(); - Self::try_from(EntityEvents::init( - id, - [AccountNotificationSettingsEvent::Initialized { - id, - galoy_account_id, - }], - )) - .expect("Could not create default") - } - - pub fn disable_channel(&mut self, channel: NotificationChannel) { - if !self.is_channel_enabled(channel) { - return; - } - self.events - .push(AccountNotificationSettingsEvent::ChannelDisabled { channel }); - } - - pub fn enable_channel(&mut self, channel: NotificationChannel) { - if self.is_channel_enabled(channel) { - return; - } - self.events - .push(AccountNotificationSettingsEvent::ChannelEnabled { channel }); - } - - pub fn is_channel_enabled(&self, channel: NotificationChannel) -> bool { - self.events.iter().fold(true, |acc, event| match event { - AccountNotificationSettingsEvent::ChannelDisabled { channel: c } if c == &channel => { - false - } - AccountNotificationSettingsEvent::ChannelEnabled { channel: c } if c == &channel => { - true - } - _ => acc, - }) - } - - pub fn disable_category( - &mut self, - channel: NotificationChannel, - category: NotificationCategory, - ) { - if self.disabled_categories_for(channel).contains(&category) { - return; - } - self.events - .push(AccountNotificationSettingsEvent::CategoryDisabled { channel, category }); - } - - pub fn enable_category( - &mut self, - channel: NotificationChannel, - category: NotificationCategory, - ) { - if !self.disabled_categories_for(channel).contains(&category) { - return; - } - self.events - .push(AccountNotificationSettingsEvent::CategoryEnabled { channel, category }); - } - - pub fn disabled_categories_for( - &self, - channel: NotificationChannel, - ) -> HashSet { - self.events.iter().fold(HashSet::new(), |mut acc, event| { - match event { - AccountNotificationSettingsEvent::CategoryDisabled { - channel: c, - category, - } if c == &channel => { - acc.insert(*category); - } - AccountNotificationSettingsEvent::CategoryEnabled { - channel: c, - category, - } if c == &channel => { - acc.remove(category); - } - _ => (), - } - acc - }) - } -} - -impl TryFrom> for AccountNotificationSettings { - type Error = EntityError; - - fn try_from( - events: EntityEvents, - ) -> Result { - let mut builder = AccountNotificationSettingsBuilder::default(); - for event in events.iter() { - if let AccountNotificationSettingsEvent::Initialized { - id, - galoy_account_id, - } = event - { - builder = builder.id(*id); - builder = builder.galoy_account_id(galoy_account_id.clone()); - } - } - builder.events(events).build() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn initial_events() -> EntityEvents { - let id = AccountNotificationSettingsId::new(); - EntityEvents::init( - id, - [AccountNotificationSettingsEvent::Initialized { - id, - galoy_account_id: GaloyAccountId::from("galoy_id".to_string()), - }], - ) - } - - #[test] - fn channel_is_initially_enabled() { - let events = initial_events(); - let settings = AccountNotificationSettings::try_from(events).expect("Could not hydrate"); - assert!(settings.is_channel_enabled(NotificationChannel::Push)); - } - - #[test] - fn can_disable_channel() { - let events = initial_events(); - let mut settings = - AccountNotificationSettings::try_from(events).expect("Could not hydrate"); - settings.disable_channel(NotificationChannel::Push); - assert!(!settings.is_channel_enabled(NotificationChannel::Push)); - } - - #[test] - fn can_reenable_channel() { - let events = initial_events(); - let mut settings = - AccountNotificationSettings::try_from(events).expect("Could not hydrate"); - settings.disable_channel(NotificationChannel::Push); - settings.enable_channel(NotificationChannel::Push); - assert!(settings.is_channel_enabled(NotificationChannel::Push)); - } - - #[test] - fn no_categories_initially_disabled() { - let events = initial_events(); - let settings = AccountNotificationSettings::try_from(events).expect("Could not hydrate"); - assert_eq!( - settings.disabled_categories_for(NotificationChannel::Push), - HashSet::new(), - ); - } - - #[test] - fn can_disable_categories() { - let events = initial_events(); - let mut settings = - AccountNotificationSettings::try_from(events).expect("Could not hydrate"); - settings.disable_category(NotificationChannel::Push, NotificationCategory::Circles); - assert_eq!( - settings.disabled_categories_for(NotificationChannel::Push), - HashSet::from([NotificationCategory::Circles]) - ); - } - - #[test] - fn can_enable_categories() { - let events = initial_events(); - let mut settings = - AccountNotificationSettings::try_from(events).expect("Could not hydrate"); - settings.disable_category(NotificationChannel::Push, NotificationCategory::Circles); - settings.disable_category(NotificationChannel::Push, NotificationCategory::Payments); - settings.enable_category(NotificationChannel::Push, NotificationCategory::Circles); - assert_eq!( - settings.disabled_categories_for(NotificationChannel::Push), - HashSet::from([NotificationCategory::Payments]) - ); - } -} diff --git a/core/notifications/src/account_notification_settings/error.rs b/core/notifications/src/account_notification_settings/error.rs deleted file mode 100644 index 140d93a498..0000000000 --- a/core/notifications/src/account_notification_settings/error.rs +++ /dev/null @@ -1,9 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum AccountNotificationSettingsError { - #[error("AccountNotificationSettingsError - Sqlx: {0}")] - Sqlx(#[from] sqlx::Error), - #[error("AccountNotificationSettingsError - EntityError: {0}")] - EntityError(#[from] es_entity::EntityError), -} diff --git a/core/notifications/src/app/error.rs b/core/notifications/src/app/error.rs index 57ff3eecce..45097a2936 100644 --- a/core/notifications/src/app/error.rs +++ b/core/notifications/src/app/error.rs @@ -1,9 +1,9 @@ use thiserror::Error; -use crate::account_notification_settings::error::*; +use crate::user_notification_settings::error::*; #[derive(Error, Debug)] pub enum ApplicationError { #[error("{0}")] - AccountNotificationSettingsError(#[from] AccountNotificationSettingsError), + UserNotificationSettingsError(#[from] UserNotificationSettingsError), } diff --git a/core/notifications/src/app/mod.rs b/core/notifications/src/app/mod.rs index eb64ae4eb3..7ef7880590 100644 --- a/core/notifications/src/app/mod.rs +++ b/core/notifications/src/app/mod.rs @@ -3,7 +3,7 @@ mod error; use sqlx::{Pool, Postgres}; -use crate::{account_notification_settings::*, primitives::*}; +use crate::{primitives::*, user_notification_settings::*}; pub use config::*; pub use error::*; @@ -11,13 +11,13 @@ pub use error::*; #[derive(Clone)] pub struct NotificationsApp { _config: AppConfig, - settings: AccountNotificationSettingsRepo, + settings: UserNotificationSettingsRepo, _pool: Pool, } impl NotificationsApp { pub fn new(pool: Pool, config: AppConfig) -> Self { - let settings = AccountNotificationSettingsRepo::new(&pool); + let settings = UserNotificationSettingsRepo::new(&pool); Self { _config: config, _pool: pool, @@ -25,19 +25,79 @@ impl NotificationsApp { } } - pub async fn disable_channel_on_account( + pub async fn notification_settings_for_user( &self, - account_id: GaloyAccountId, - channel: NotificationChannel, - ) -> Result { - let mut account_settings = - if let Some(settings) = self.settings.find_for_account_id(&account_id).await? { - settings - } else { - AccountNotificationSettings::new(account_id) - }; - account_settings.disable_channel(channel); - self.settings.persist(&mut account_settings).await?; - Ok(account_settings) + user_id: GaloyUserId, + ) -> Result { + let user_settings = self + .settings + .find_for_user_id(&user_id) + .await? + .unwrap_or_else(|| UserNotificationSettings::new(user_id)); + + Ok(user_settings) + } + + pub async fn disable_channel_on_user( + &self, + user_id: GaloyUserId, + channel: UserNotificationChannel, + ) -> Result { + let mut user_settings = self + .settings + .find_for_user_id(&user_id) + .await? + .unwrap_or_else(|| UserNotificationSettings::new(user_id)); + user_settings.disable_channel(channel); + self.settings.persist(&mut user_settings).await?; + Ok(user_settings) + } + + pub async fn enable_channel_on_user( + &self, + user_id: GaloyUserId, + channel: UserNotificationChannel, + ) -> Result { + let mut user_settings = self + .settings + .find_for_user_id(&user_id) + .await? + .unwrap_or_else(|| UserNotificationSettings::new(user_id)); + + user_settings.enable_channel(channel); + self.settings.persist(&mut user_settings).await?; + Ok(user_settings) + } + + pub async fn disable_category_on_user( + &self, + user_id: GaloyUserId, + channel: UserNotificationChannel, + category: UserNotificationCategory, + ) -> Result { + let mut user_settings = self + .settings + .find_for_user_id(&user_id) + .await? + .unwrap_or_else(|| UserNotificationSettings::new(user_id)); + user_settings.disable_category(channel, category); + self.settings.persist(&mut user_settings).await?; + Ok(user_settings) + } + + pub async fn enable_category_on_user( + &self, + user_id: GaloyUserId, + channel: UserNotificationChannel, + category: UserNotificationCategory, + ) -> Result { + let mut user_settings = self + .settings + .find_for_user_id(&user_id) + .await? + .unwrap_or_else(|| UserNotificationSettings::new(user_id)); + user_settings.enable_category(channel, category); + self.settings.persist(&mut user_settings).await?; + Ok(user_settings) } } diff --git a/core/notifications/src/graphql/convert.rs b/core/notifications/src/graphql/convert.rs index 6f5dfe9adc..384ed31299 100644 --- a/core/notifications/src/graphql/convert.rs +++ b/core/notifications/src/graphql/convert.rs @@ -1,13 +1,15 @@ -use super::types::*; -use crate::{account_notification_settings::*, primitives::*}; +use super::types; +use crate::{primitives::*, user_notification_settings}; -impl From for NotificationSettingsAlt { - fn from(settings: AccountNotificationSettings) -> Self { - NotificationSettingsAlt { - push: NotificationChannelSettingsAlt { - enabled: settings.is_channel_enabled(NotificationChannel::Push), +impl From + for types::UserNotificationSettings +{ + fn from(settings: user_notification_settings::UserNotificationSettings) -> Self { + Self { + push: types::UserNotificationChannelSettings { + enabled: settings.is_channel_enabled(UserNotificationChannel::Push), disabled_categories: settings - .disabled_categories_for(NotificationChannel::Push) + .disabled_categories_for(UserNotificationChannel::Push) .into_iter() .collect(), }, diff --git a/core/notifications/src/graphql/schema.rs b/core/notifications/src/graphql/schema.rs index bf21ebc331..9bf5b7ee68 100644 --- a/core/notifications/src/graphql/schema.rs +++ b/core/notifications/src/graphql/schema.rs @@ -13,50 +13,148 @@ pub struct Query; #[Object] impl Query { #[graphql(entity)] - async fn consumer_account(&self, id: ID) -> Option { - Some(ConsumerAccount { id }) + async fn user(&self, id: ID) -> Option { + Some(User { id }) } } #[derive(SimpleObject)] #[graphql(extends)] #[graphql(complex)] -struct ConsumerAccount { +struct User { #[graphql(external)] id: ID, } #[ComplexObject] -impl ConsumerAccount {} +impl User { + async fn notification_settings( + &self, + ctx: &Context<'_>, + ) -> async_graphql::Result { + let app = ctx.data_unchecked::(); + + let settings = app + .notification_settings_for_user(GaloyUserId::from(self.id.0.clone())) + .await?; + + Ok(UserNotificationSettings::from(settings)) + } +} #[derive(SimpleObject)] -pub struct AccountUpdateNotificationSettingsPayloadAlt { - notification_settings: NotificationSettingsAlt, +pub struct UserUpdateNotificationSettingsPayload { + notification_settings: UserNotificationSettings, +} + +#[derive(InputObject)] +struct UserDisableNotificationChannelInput { + channel: UserNotificationChannel, +} + +#[derive(InputObject)] +struct UserEnableNotificationChannelInput { + channel: UserNotificationChannel, } #[derive(InputObject)] -struct AccountDisableNotificationChannelInputAlt { - channel: NotificationChannel, +struct UserEnableNotificationCategoryInput { + channel: UserNotificationChannel, + category: UserNotificationCategory, +} + +#[derive(InputObject)] +struct UserDisableNotificationCategoryInput { + channel: UserNotificationChannel, + category: UserNotificationCategory, } pub struct Mutation; #[Object] impl Mutation { - async fn account_disable_notification_channel_alt( + async fn user_disable_notification_channel( + &self, + ctx: &Context<'_>, + input: UserDisableNotificationChannelInput, + ) -> async_graphql::Result { + let subject = ctx.data::()?; + if subject.read_only { + return Err("Permission denied".into()); + } + let app = ctx.data_unchecked::(); + let settings = app + .disable_channel_on_user(GaloyUserId::from(subject.id.clone()), input.channel) + .await?; + Ok(UserUpdateNotificationSettingsPayload { + notification_settings: UserNotificationSettings::from(settings), + }) + } + + async fn user_enable_notification_channel( &self, ctx: &Context<'_>, - input: AccountDisableNotificationChannelInputAlt, - ) -> async_graphql::Result { + input: UserEnableNotificationChannelInput, + ) -> async_graphql::Result { let subject = ctx.data::()?; if subject.read_only { return Err("Permission denied".into()); } let app = ctx.data_unchecked::(); + + let settings = app + .enable_channel_on_user(GaloyUserId::from(subject.id.clone()), input.channel) + .await?; + + Ok(UserUpdateNotificationSettingsPayload { + notification_settings: UserNotificationSettings::from(settings), + }) + } + + async fn user_disable_notification_category( + &self, + ctx: &Context<'_>, + input: UserDisableNotificationCategoryInput, + ) -> async_graphql::Result { + let subject = ctx.data::()?; + if subject.read_only { + return Err("Permission denied".into()); + } + let app = ctx.data_unchecked::(); + let settings = app - .disable_channel_on_account(GaloyAccountId::from(subject.id.clone()), input.channel) + .disable_category_on_user( + GaloyUserId::from(subject.id.clone()), + input.channel, + input.category, + ) .await?; - Ok(AccountUpdateNotificationSettingsPayloadAlt { - notification_settings: NotificationSettingsAlt::from(settings), + + Ok(UserUpdateNotificationSettingsPayload { + notification_settings: UserNotificationSettings::from(settings), + }) + } + + async fn user_enable_notification_category( + &self, + ctx: &Context<'_>, + input: UserEnableNotificationCategoryInput, + ) -> async_graphql::Result { + let subject = ctx.data::()?; + if subject.read_only { + return Err("Permission denied".into()); + } + let app = ctx.data_unchecked::(); + + let settings = app + .enable_category_on_user( + GaloyUserId::from(subject.id.clone()), + input.channel, + input.category, + ) + .await?; + + Ok(UserUpdateNotificationSettingsPayload { + notification_settings: UserNotificationSettings::from(settings), }) } } diff --git a/core/notifications/src/graphql/types.rs b/core/notifications/src/graphql/types.rs index 9036bd7910..9cb19416cb 100644 --- a/core/notifications/src/graphql/types.rs +++ b/core/notifications/src/graphql/types.rs @@ -3,12 +3,12 @@ use async_graphql::*; use crate::primitives::*; #[derive(SimpleObject)] -pub(super) struct NotificationSettingsAlt { - pub push: NotificationChannelSettingsAlt, +pub(super) struct UserNotificationSettings { + pub push: UserNotificationChannelSettings, } #[derive(SimpleObject)] -pub(super) struct NotificationChannelSettingsAlt { +pub(super) struct UserNotificationChannelSettings { pub enabled: bool, - pub disabled_categories: Vec, + pub disabled_categories: Vec, } diff --git a/core/notifications/src/lib.rs b/core/notifications/src/lib.rs index a24ec76694..02a26eefd2 100644 --- a/core/notifications/src/lib.rs +++ b/core/notifications/src/lib.rs @@ -1,10 +1,10 @@ #![cfg_attr(feature = "fail-on-warnings", deny(warnings))] #![cfg_attr(feature = "fail-on-warnings", deny(clippy::all))] -mod account_notification_settings; mod app; mod data_import; mod primitives; +mod user_notification_settings; pub mod cli; pub mod graphql; diff --git a/core/notifications/src/primitives.rs b/core/notifications/src/primitives.rs index eef63c087d..a405ca98a7 100644 --- a/core/notifications/src/primitives.rs +++ b/core/notifications/src/primitives.rs @@ -1,36 +1,36 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct GaloyAccountId(String); -impl From for GaloyAccountId { +pub struct GaloyUserId(String); +impl From for GaloyUserId { fn from(s: String) -> Self { Self(s) } } -impl AsRef for GaloyAccountId { +impl AsRef for GaloyUserId { fn as_ref(&self) -> &str { &self.0 } } -impl std::fmt::Display for GaloyAccountId { +impl std::fmt::Display for GaloyUserId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } -es_entity::entity_id! { AccountNotificationSettingsId } +es_entity::entity_id! { UserNotificationSettingsId } #[derive(async_graphql::Enum, Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[graphql(name = "NotificationChannelAlt")] -pub enum NotificationChannel { +#[graphql(name = "UserNotificationChannel")] +pub enum UserNotificationChannel { Push, } #[derive(async_graphql::Enum, Debug, Hash, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[graphql(name = "NotificationCategoryAlt")] -pub enum NotificationCategory { +#[graphql(name = "UserNotificationCategory")] +pub enum UserNotificationCategory { Circles, Payments, } diff --git a/core/notifications/src/user_notification_settings/entity.rs b/core/notifications/src/user_notification_settings/entity.rs new file mode 100644 index 0000000000..874a43d7d2 --- /dev/null +++ b/core/notifications/src/user_notification_settings/entity.rs @@ -0,0 +1,233 @@ +use derive_builder::Builder; +use es_entity::*; +use serde::{Deserialize, Serialize}; + +use std::collections::HashSet; + +use crate::primitives::*; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum UserNotificationSettingsEvent { + Initialized { + id: UserNotificationSettingsId, + galoy_user_id: GaloyUserId, + }, + ChannelDisabled { + channel: UserNotificationChannel, + }, + ChannelEnabled { + channel: UserNotificationChannel, + }, + CategoryDisabled { + channel: UserNotificationChannel, + category: UserNotificationCategory, + }, + CategoryEnabled { + channel: UserNotificationChannel, + category: UserNotificationCategory, + }, +} + +impl EntityEvent for UserNotificationSettingsEvent { + type EntityId = UserNotificationSettingsId; + fn event_table_name() -> &'static str { + "user_notification_settings_events" + } +} + +#[derive(Builder)] +#[builder(pattern = "owned", build_fn(error = "EntityError"))] +pub struct UserNotificationSettings { + pub id: UserNotificationSettingsId, + pub galoy_user_id: GaloyUserId, + pub(super) events: EntityEvents, +} + +impl EsEntity for UserNotificationSettings { + type Event = UserNotificationSettingsEvent; +} + +impl UserNotificationSettings { + pub fn new(galoy_user_id: GaloyUserId) -> Self { + let id = UserNotificationSettingsId::new(); + Self::try_from(EntityEvents::init( + id, + [UserNotificationSettingsEvent::Initialized { id, galoy_user_id }], + )) + .expect("Could not create default") + } + + pub fn disable_channel(&mut self, channel: UserNotificationChannel) { + if !self.is_channel_enabled(channel) { + return; + } + self.events + .push(UserNotificationSettingsEvent::ChannelDisabled { channel }); + } + + pub fn enable_channel(&mut self, channel: UserNotificationChannel) { + if self.is_channel_enabled(channel) { + return; + } + self.events + .push(UserNotificationSettingsEvent::ChannelEnabled { channel }); + } + + pub fn is_channel_enabled(&self, channel: UserNotificationChannel) -> bool { + self.events.iter().fold(true, |acc, event| match event { + UserNotificationSettingsEvent::ChannelDisabled { channel: c } if c == &channel => false, + UserNotificationSettingsEvent::ChannelEnabled { channel: c } if c == &channel => true, + _ => acc, + }) + } + + pub fn disable_category( + &mut self, + channel: UserNotificationChannel, + category: UserNotificationCategory, + ) { + if self.disabled_categories_for(channel).contains(&category) { + return; + } + self.events + .push(UserNotificationSettingsEvent::CategoryDisabled { channel, category }); + } + + pub fn enable_category( + &mut self, + channel: UserNotificationChannel, + category: UserNotificationCategory, + ) { + if !self.disabled_categories_for(channel).contains(&category) { + return; + } + self.events + .push(UserNotificationSettingsEvent::CategoryEnabled { channel, category }); + } + + pub fn disabled_categories_for( + &self, + channel: UserNotificationChannel, + ) -> HashSet { + self.events.iter().fold(HashSet::new(), |mut acc, event| { + match event { + UserNotificationSettingsEvent::CategoryDisabled { + channel: c, + category, + } if c == &channel => { + acc.insert(*category); + } + UserNotificationSettingsEvent::CategoryEnabled { + channel: c, + category, + } if c == &channel => { + acc.remove(category); + } + _ => (), + } + acc + }) + } +} + +impl TryFrom> for UserNotificationSettings { + type Error = EntityError; + + fn try_from(events: EntityEvents) -> Result { + let mut builder = UserNotificationSettingsBuilder::default(); + for event in events.iter() { + if let UserNotificationSettingsEvent::Initialized { id, galoy_user_id } = event { + builder = builder.id(*id); + builder = builder.galoy_user_id(galoy_user_id.clone()); + } + } + builder.events(events).build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn initial_events() -> EntityEvents { + let id = UserNotificationSettingsId::new(); + EntityEvents::init( + id, + [UserNotificationSettingsEvent::Initialized { + id, + galoy_user_id: GaloyUserId::from("galoy_id".to_string()), + }], + ) + } + + #[test] + fn channel_is_initially_enabled() { + let events = initial_events(); + let settings = UserNotificationSettings::try_from(events).expect("Could not hydrate"); + assert!(settings.is_channel_enabled(UserNotificationChannel::Push)); + } + + #[test] + fn can_disable_channel() { + let events = initial_events(); + let mut settings = UserNotificationSettings::try_from(events).expect("Could not hydrate"); + settings.disable_channel(UserNotificationChannel::Push); + assert!(!settings.is_channel_enabled(UserNotificationChannel::Push)); + } + + #[test] + fn can_reenable_channel() { + let events = initial_events(); + let mut settings = UserNotificationSettings::try_from(events).expect("Could not hydrate"); + settings.disable_channel(UserNotificationChannel::Push); + settings.enable_channel(UserNotificationChannel::Push); + assert!(settings.is_channel_enabled(UserNotificationChannel::Push)); + } + + #[test] + fn no_categories_initially_disabled() { + let events = initial_events(); + let settings = UserNotificationSettings::try_from(events).expect("Could not hydrate"); + assert_eq!( + settings.disabled_categories_for(UserNotificationChannel::Push), + HashSet::new(), + ); + } + + #[test] + fn can_disable_categories() { + let events = initial_events(); + let mut settings = UserNotificationSettings::try_from(events).expect("Could not hydrate"); + settings.disable_category( + UserNotificationChannel::Push, + UserNotificationCategory::Circles, + ); + assert_eq!( + settings.disabled_categories_for(UserNotificationChannel::Push), + HashSet::from([UserNotificationCategory::Circles]) + ); + } + + #[test] + fn can_enable_categories() { + let events = initial_events(); + let mut settings = UserNotificationSettings::try_from(events).expect("Could not hydrate"); + settings.disable_category( + UserNotificationChannel::Push, + UserNotificationCategory::Circles, + ); + settings.disable_category( + UserNotificationChannel::Push, + UserNotificationCategory::Payments, + ); + settings.enable_category( + UserNotificationChannel::Push, + UserNotificationCategory::Circles, + ); + assert_eq!( + settings.disabled_categories_for(UserNotificationChannel::Push), + HashSet::from([UserNotificationCategory::Payments]) + ); + } +} diff --git a/core/notifications/src/user_notification_settings/error.rs b/core/notifications/src/user_notification_settings/error.rs new file mode 100644 index 0000000000..769f08d1f8 --- /dev/null +++ b/core/notifications/src/user_notification_settings/error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum UserNotificationSettingsError { + #[error("UserNotificationSettingsError - Sqlx: {0}")] + Sqlx(#[from] sqlx::Error), + #[error("UserNotificationSettingsError - EntityError: {0}")] + EntityError(#[from] es_entity::EntityError), +} diff --git a/core/notifications/src/account_notification_settings/mod.rs b/core/notifications/src/user_notification_settings/mod.rs similarity index 100% rename from core/notifications/src/account_notification_settings/mod.rs rename to core/notifications/src/user_notification_settings/mod.rs diff --git a/core/notifications/src/account_notification_settings/repo.rs b/core/notifications/src/user_notification_settings/repo.rs similarity index 52% rename from core/notifications/src/account_notification_settings/repo.rs rename to core/notifications/src/user_notification_settings/repo.rs index 11ec795417..83c4503bdd 100644 --- a/core/notifications/src/account_notification_settings/repo.rs +++ b/core/notifications/src/user_notification_settings/repo.rs @@ -6,31 +6,31 @@ use super::{entity::*, error::*}; use crate::primitives::*; #[derive(Debug, Clone)] -pub struct AccountNotificationSettingsRepo { +pub struct UserNotificationSettingsRepo { pool: PgPool, } -impl AccountNotificationSettingsRepo { +impl UserNotificationSettingsRepo { pub fn new(pool: &PgPool) -> Self { Self { pool: pool.clone() } } - pub async fn find_for_account_id( + pub async fn find_for_user_id( &self, - account_id: &GaloyAccountId, - ) -> Result, AccountNotificationSettingsError> { + user_id: &GaloyUserId, + ) -> Result, UserNotificationSettingsError> { let rows = sqlx::query_as!( GenericEvent, r#"SELECT a.id, e.sequence, e.event - FROM account_notification_settings a - JOIN account_notification_settings_events e ON a.id = e.id - WHERE a.galoy_account_id = $1 + FROM user_notification_settings a + JOIN user_notification_settings_events e ON a.id = e.id + WHERE a.galoy_user_id = $1 ORDER BY e.sequence"#, - account_id.as_ref(), + user_id.as_ref(), ) .fetch_all(&self.pool) .await?; - let res = EntityEvents::load_first::(rows); + let res = EntityEvents::load_first::(rows); if matches!(res, Err(EntityError::NoEntityEventsPresent)) { return Ok(None); } @@ -39,14 +39,14 @@ impl AccountNotificationSettingsRepo { pub async fn persist( &self, - settings: &mut AccountNotificationSettings, - ) -> Result<(), AccountNotificationSettingsError> { + settings: &mut UserNotificationSettings, + ) -> Result<(), UserNotificationSettingsError> { let mut tx = self.pool.begin().await?; sqlx::query!( - r#"INSERT INTO account_notification_settings (id, galoy_account_id) + r#"INSERT INTO user_notification_settings (id, galoy_user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING"#, - settings.id as AccountNotificationSettingsId, - settings.galoy_account_id.as_ref(), + settings.id as UserNotificationSettingsId, + settings.galoy_user_id.as_ref(), ) .execute(&mut *tx) .await?; diff --git a/core/notifications/src/account_notification_settings/value.rs b/core/notifications/src/user_notification_settings/value.rs similarity index 58% rename from core/notifications/src/account_notification_settings/value.rs rename to core/notifications/src/user_notification_settings/value.rs index 0ed0a79c1b..be9210e2ee 100644 --- a/core/notifications/src/account_notification_settings/value.rs +++ b/core/notifications/src/user_notification_settings/value.rs @@ -1,15 +1,15 @@ #[derive(Clone, Default, Debug)] -pub struct NotificationSettings { - push: NotificationChannelSettings, +pub struct UserNotificationSettings { + push: UserNotificationChannelSettings, } #[derive(Clone, Debug)] -pub struct NotificationChannelSettings { +pub struct UserNotificationChannelSettings { enabled: bool, disabled_categories: Vec, } -impl Default for NotificationChannelSettings { +impl Default for UserNotificationChannelSettings { fn default() -> Self { Self { enabled: true, diff --git a/core/notifications/subgraph/schema.graphql b/core/notifications/subgraph/schema.graphql index d4d795002e..68845a705b 100644 --- a/core/notifications/subgraph/schema.graphql +++ b/core/notifications/subgraph/schema.graphql @@ -1,42 +1,56 @@ -input AccountDisableNotificationChannelInputAlt { - channel: NotificationChannelAlt! +type Mutation { + userDisableNotificationChannel(input: UserDisableNotificationChannelInput!): UserUpdateNotificationSettingsPayload! + userEnableNotificationChannel(input: UserEnableNotificationChannelInput!): UserUpdateNotificationSettingsPayload! + userDisableNotificationCategory(input: UserDisableNotificationCategoryInput!): UserUpdateNotificationSettingsPayload! + userEnableNotificationCategory(input: UserEnableNotificationCategoryInput!): UserUpdateNotificationSettingsPayload! } -type AccountUpdateNotificationSettingsPayloadAlt { - notificationSettings: NotificationSettingsAlt! -} -extend type ConsumerAccount @key(fields: "id") { +extend type User @key(fields: "id") { id: ID! @external + notificationSettings: UserNotificationSettings! } +input UserDisableNotificationCategoryInput { + channel: UserNotificationChannel! + category: UserNotificationCategory! +} +input UserDisableNotificationChannelInput { + channel: UserNotificationChannel! +} +input UserEnableNotificationCategoryInput { + channel: UserNotificationChannel! + category: UserNotificationCategory! +} -type Mutation { - accountDisableNotificationChannelAlt(input: AccountDisableNotificationChannelInputAlt!): AccountUpdateNotificationSettingsPayloadAlt! +input UserEnableNotificationChannelInput { + channel: UserNotificationChannel! } -enum NotificationCategoryAlt { +enum UserNotificationCategory { CIRCLES PAYMENTS } -enum NotificationChannelAlt { +enum UserNotificationChannel { PUSH } -type NotificationChannelSettingsAlt { +type UserNotificationChannelSettings { enabled: Boolean! - disabledCategories: [NotificationCategoryAlt!]! + disabledCategories: [UserNotificationCategory!]! } -type NotificationSettingsAlt { - push: NotificationChannelSettingsAlt! +type UserNotificationSettings { + push: UserNotificationChannelSettings! } - +type UserUpdateNotificationSettingsPayload { + notificationSettings: UserNotificationSettings! +} extend schema @link( url: "https://specs.apollo.dev/federation/v2.3", diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index 1d3a2c7b49..5a4f87225e 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -87,12 +87,6 @@ input AccountDisableNotificationChannelInput channel: NotificationChannel! } -input AccountDisableNotificationChannelInputAlt - @join__type(graph: NOTIFICATIONS) -{ - channel: NotificationChannelAlt! -} - input AccountEnableNotificationCategoryInput @join__type(graph: PUBLIC) { @@ -177,12 +171,6 @@ type AccountUpdateNotificationSettingsPayload errors: [Error!]! } -type AccountUpdateNotificationSettingsPayloadAlt - @join__type(graph: NOTIFICATIONS) -{ - notificationSettings: NotificationSettingsAlt! -} - type ApiKey @join__type(graph: API_KEYS) { @@ -380,19 +368,18 @@ type CentAmountPayload type ConsumerAccount implements Account @join__implements(graph: PUBLIC, interface: "Account") - @join__type(graph: NOTIFICATIONS, key: "id", extension: true) @join__type(graph: PUBLIC) { - id: ID! - callbackEndpoints: [CallbackEndpoint!]! @join__field(graph: PUBLIC) + callbackEndpoints: [CallbackEndpoint!]! """ return CSV stream, base64 encoded, of the list of transactions in the wallet """ - csvTransactions(walletIds: [WalletId!]!): String! @join__field(graph: PUBLIC) - defaultWallet: PublicWallet! @join__field(graph: PUBLIC) - defaultWalletId: WalletId! @join__field(graph: PUBLIC) - displayCurrency: DisplayCurrency! @join__field(graph: PUBLIC) + csvTransactions(walletIds: [WalletId!]!): String! + defaultWallet: PublicWallet! + defaultWalletId: WalletId! + displayCurrency: DisplayCurrency! + id: ID! """A list of all invoices associated with walletIds optionally passed.""" invoices( @@ -408,15 +395,15 @@ type ConsumerAccount implements Account """Returns the last n items from the list.""" last: Int walletIds: [WalletId] - ): InvoiceConnection @join__field(graph: PUBLIC) - level: AccountLevel! @join__field(graph: PUBLIC) - limits: AccountLimits! @join__field(graph: PUBLIC) - notificationSettings: NotificationSettings! @join__field(graph: PUBLIC) - pendingIncomingTransactions(walletIds: [WalletId]): [Transaction!]! @join__field(graph: PUBLIC) + ): InvoiceConnection + level: AccountLevel! + limits: AccountLimits! + notificationSettings: NotificationSettings! + pendingIncomingTransactions(walletIds: [WalletId]): [Transaction!]! """List the quiz questions of the consumer account""" - quiz: [Quiz!]! @join__field(graph: PUBLIC) - realtimePrice: RealtimePrice! @join__field(graph: PUBLIC) + quiz: [Quiz!]! + realtimePrice: RealtimePrice! """ A list of all transactions associated with walletIds optionally passed. @@ -434,9 +421,9 @@ type ConsumerAccount implements Account """Returns the last n items from the list.""" last: Int walletIds: [WalletId] - ): TransactionConnection @join__field(graph: PUBLIC) - walletById(walletId: WalletId!): Wallet! @join__field(graph: PUBLIC) - wallets: [Wallet!]! @join__field(graph: PUBLIC) + ): TransactionConnection + walletById(walletId: WalletId!): Wallet! + wallets: [Wallet!]! } """ @@ -1067,7 +1054,10 @@ type Mutation { apiKeyCreate(input: ApiKeyCreateInput!): ApiKeyCreatePayload! @join__field(graph: API_KEYS) apiKeyRevoke(input: ApiKeyRevokeInput!): ApiKeyRevokePayload! @join__field(graph: API_KEYS) - accountDisableNotificationChannelAlt(input: AccountDisableNotificationChannelInputAlt!): AccountUpdateNotificationSettingsPayloadAlt! @join__field(graph: NOTIFICATIONS) + userDisableNotificationChannel(input: UserDisableNotificationChannelInput!): UserUpdateNotificationSettingsPayload! @join__field(graph: NOTIFICATIONS) + userEnableNotificationChannel(input: UserEnableNotificationChannelInput!): UserUpdateNotificationSettingsPayload! @join__field(graph: NOTIFICATIONS) + userDisableNotificationCategory(input: UserDisableNotificationCategoryInput!): UserUpdateNotificationSettingsPayload! @join__field(graph: NOTIFICATIONS) + userEnableNotificationCategory(input: UserEnableNotificationCategoryInput!): UserUpdateNotificationSettingsPayload! @join__field(graph: NOTIFICATIONS) accountDelete: AccountDeletePayload! @join__field(graph: PUBLIC) accountDisableNotificationCategory(input: AccountDisableNotificationCategoryInput!): AccountUpdateNotificationSettingsPayload! @join__field(graph: PUBLIC) accountDisableNotificationChannel(input: AccountDisableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! @join__field(graph: PUBLIC) @@ -1223,25 +1213,12 @@ enum Network scalar NotificationCategory @join__type(graph: PUBLIC) -enum NotificationCategoryAlt - @join__type(graph: NOTIFICATIONS) -{ - CIRCLES @join__enumValue(graph: NOTIFICATIONS) - PAYMENTS @join__enumValue(graph: NOTIFICATIONS) -} - enum NotificationChannel @join__type(graph: PUBLIC) { PUSH @join__enumValue(graph: PUBLIC) } -enum NotificationChannelAlt - @join__type(graph: NOTIFICATIONS) -{ - PUSH @join__enumValue(graph: NOTIFICATIONS) -} - type NotificationChannelSettings @join__type(graph: PUBLIC) { @@ -1249,25 +1226,12 @@ type NotificationChannelSettings enabled: Boolean! } -type NotificationChannelSettingsAlt - @join__type(graph: NOTIFICATIONS) -{ - enabled: Boolean! - disabledCategories: [NotificationCategoryAlt!]! -} - type NotificationSettings @join__type(graph: PUBLIC) { push: NotificationChannelSettings! } -type NotificationSettingsAlt - @join__type(graph: NOTIFICATIONS) -{ - push: NotificationChannelSettingsAlt! -} - """An address for an on-chain bitcoin destination""" scalar OnChainAddress @join__type(graph: PUBLIC) @@ -1885,10 +1849,12 @@ type UsdWallet implements Wallet type User @join__type(graph: API_KEYS, key: "id", extension: true) + @join__type(graph: NOTIFICATIONS, key: "id", extension: true) @join__type(graph: PUBLIC) { id: ID! apiKeys: [ApiKey!]! @join__field(graph: API_KEYS) + notificationSettings: UserNotificationSettings! @join__field(graph: NOTIFICATIONS) """ Get single contact details. @@ -1967,6 +1933,19 @@ type UserContactUpdateAliasPayload errors: [Error!]! } +input UserDisableNotificationCategoryInput + @join__type(graph: NOTIFICATIONS) +{ + channel: UserNotificationChannel! + category: UserNotificationCategory! +} + +input UserDisableNotificationChannelInput + @join__type(graph: NOTIFICATIONS) +{ + channel: UserNotificationChannel! +} + type UserEmailDeletePayload @join__type(graph: PUBLIC) { @@ -2002,6 +1981,19 @@ type UserEmailRegistrationValidatePayload me: User } +input UserEnableNotificationCategoryInput + @join__type(graph: NOTIFICATIONS) +{ + channel: UserNotificationChannel! + category: UserNotificationCategory! +} + +input UserEnableNotificationChannelInput + @join__type(graph: NOTIFICATIONS) +{ + channel: UserNotificationChannel! +} + input UserLoginInput @join__type(graph: PUBLIC) { @@ -2026,6 +2018,32 @@ input UserLogoutInput scalar Username @join__type(graph: PUBLIC) +enum UserNotificationCategory + @join__type(graph: NOTIFICATIONS) +{ + CIRCLES @join__enumValue(graph: NOTIFICATIONS) + PAYMENTS @join__enumValue(graph: NOTIFICATIONS) +} + +enum UserNotificationChannel + @join__type(graph: NOTIFICATIONS) +{ + PUSH @join__enumValue(graph: NOTIFICATIONS) +} + +type UserNotificationChannelSettings + @join__type(graph: NOTIFICATIONS) +{ + enabled: Boolean! + disabledCategories: [UserNotificationCategory!]! +} + +type UserNotificationSettings + @join__type(graph: NOTIFICATIONS) +{ + push: UserNotificationChannelSettings! +} + type UserPhoneDeletePayload @join__type(graph: PUBLIC) { @@ -2106,6 +2124,12 @@ type UserUpdateLanguagePayload user: User } +type UserUpdateNotificationSettingsPayload + @join__type(graph: NOTIFICATIONS) +{ + notificationSettings: UserNotificationSettings! +} + input UserUpdateUsernameInput @join__type(graph: PUBLIC) {