From be7631752e2059ed6dea6877e4c2af54e352a25a Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Wed, 13 Sep 2023 12:48:02 -0500 Subject: [PATCH 01/12] chore: initial set notifications settings --- bats | 0 dev/apollo-federation/supergraph.graphql | 29 ++++++++++++ src/domain/primitives/index.types.d.ts | 2 + src/graphql/public/mutations.ts | 3 ++ ...count-update-push-notification-settings.ts | 41 +++++++++++++++++ src/graphql/public/schema.graphql | 21 +++++++++ ...count-update-push-notification-settings.ts | 18 ++++++++ .../scalar/push-notification-sub-type.ts | 30 +++++++++++++ .../types/scalar/push-notification-type.ts | 28 ++++++++++++ ...ount-update-push-notification-settings.gql | 10 +++++ test/bats/push-notification-settings.bats | 44 +++++++++++++++++++ 11 files changed, 226 insertions(+) create mode 100644 bats create mode 100644 src/graphql/public/root/mutation/account-update-push-notification-settings.ts create mode 100644 src/graphql/public/types/payload/account-update-push-notification-settings.ts create mode 100644 src/graphql/shared/types/scalar/push-notification-sub-type.ts create mode 100644 src/graphql/shared/types/scalar/push-notification-type.ts create mode 100644 test/bats/gql/account-update-push-notification-settings.gql create mode 100644 test/bats/push-notification-settings.bats diff --git a/bats b/bats new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dev/apollo-federation/supergraph.graphql b/dev/apollo-federation/supergraph.graphql index 20fab3f618..8d0478e944 100644 --- a/dev/apollo-federation/supergraph.graphql +++ b/dev/apollo-federation/supergraph.graphql @@ -120,6 +120,20 @@ type AccountUpdateDisplayCurrencyPayload errors: [Error!]! } +input AccountUpdatePushNotificationSettingsInput + @join__type(graph: PUBLIC) +{ + notificationSettings: [PushNotifcationSettingsInput]! + notificationsEnabled: Boolean! +} + +type AccountUpdatePushNotificationSettingsPayload + @join__type(graph: PUBLIC) +{ + account: ConsumerAccount + errors: [Error!]! +} + """An Opaque Bearer token""" scalar AuthToken @join__type(graph: PUBLIC) @@ -818,6 +832,7 @@ type Mutation accountDelete: AccountDeletePayload! accountUpdateDefaultWalletId(input: AccountUpdateDefaultWalletIdInput!): AccountUpdateDefaultWalletIdPayload! accountUpdateDisplayCurrency(input: AccountUpdateDisplayCurrencyInput!): AccountUpdateDisplayCurrencyPayload! + accountUpdatePushNotificationSettings(input: AccountUpdatePushNotificationSettingsInput!): AccountUpdatePushNotificationSettingsPayload! callbackEndpointAdd(input: CallbackEndpointAddInput!): CallbackEndpointAddPayload! callbackEndpointDelete(input: CallbackEndpointDeleteInput!): SuccessPayload! captchaCreateChallenge: CaptchaCreateChallengePayload! @@ -1214,6 +1229,20 @@ type PublicWallet walletCurrency: WalletCurrency! } +input PushNotifcationSettingsInput + @join__type(graph: PUBLIC) +{ + disabledSubtypes: [PushNotificationSubType]! + enabled: Boolean! + type: PushNotificationType! +} + +scalar PushNotificationSubType + @join__type(graph: PUBLIC) + +scalar PushNotificationType + @join__type(graph: PUBLIC) + type Query @join__type(graph: PUBLIC) { diff --git a/src/domain/primitives/index.types.d.ts b/src/domain/primitives/index.types.d.ts index dc599ebed3..f6322b8575 100644 --- a/src/domain/primitives/index.types.d.ts +++ b/src/domain/primitives/index.types.d.ts @@ -11,6 +11,8 @@ type MilliSeconds = number & { readonly brand: unique symbol } type Days = number & { readonly brand: unique symbol } type JwtToken = string & { readonly brand: unique symbol } // short lived asymmetric token from oathkeeper type Memo = string & { readonly brand: unique symbol } +type PushNotificationType = string & { readonly brand: unique symbol } +type PushNotificationSubType = string & { readonly brand: unique symbol } type XOR = | (T1 & { [k in Exclude]?: never }) diff --git a/src/graphql/public/mutations.ts b/src/graphql/public/mutations.ts index f3655b8e4c..9188aea593 100644 --- a/src/graphql/public/mutations.ts +++ b/src/graphql/public/mutations.ts @@ -49,6 +49,7 @@ import UserTotpDeleteMutation from "@graphql/public/root/mutation/user-totp-dele import CallbackEndpointAdd from "./root/mutation/callback-endpoint-add" import CallbackEndpointDelete from "./root/mutation/callback-endpoint-delete" +import AccountUpdatePushNotificationSettingsMutation from "./root/mutation/account-update-push-notification-settings" // TODO: // const fields: { [key: string]: GraphQLFieldConfig } export const mutationFields = { @@ -86,6 +87,8 @@ export const mutationFields = { userContactUpdateAlias: UserContactUpdateAliasMutation, accountUpdateDefaultWalletId: AccountUpdateDefaultWalletIdMutation, accountUpdateDisplayCurrency: AccountUpdateDisplayCurrencyMutation, + accountUpdatePushNotificationSettings: + AccountUpdatePushNotificationSettingsMutation, accountDelete: AccountDeleteMutation, feedbackSubmit: FeedbackSubmitMutation, diff --git a/src/graphql/public/root/mutation/account-update-push-notification-settings.ts b/src/graphql/public/root/mutation/account-update-push-notification-settings.ts new file mode 100644 index 0000000000..8323f646df --- /dev/null +++ b/src/graphql/public/root/mutation/account-update-push-notification-settings.ts @@ -0,0 +1,41 @@ +import { GT } from "@graphql/index" + +import PushNotificationType from "@graphql/shared/types/scalar/push-notification-type" +import PushNotificationSubType from "@graphql/shared/types/scalar/push-notification-sub-type" +import AccountUpdatePushNotificationSettingsPayload from "@graphql/public/types/payload/account-update-push-notification-settings" + +const PushNotifcationSettingsInput = GT.Input({ + name: "PushNotifcationSettingsInput", + fields: () => ({ + type: { type: GT.NonNull(PushNotificationType) }, + enabled: { type: GT.NonNull(GT.Boolean) }, + disabledSubtypes: { type: GT.NonNull(GT.List(PushNotificationSubType)) }, + }), +}) + +const AccountUpdatePushNotificationSettingsInput = GT.Input({ + name: "AccountUpdatePushNotificationSettingsInput", + fields: () => ({ + notificationsEnabled: { type: GT.NonNull(GT.Boolean) }, + notificationSettings: { + type: GT.NonNull(GT.List(PushNotifcationSettingsInput)), + }, + }), +}) + +const AccountUpdatePushNotificationSettingsMutation = GT.Field({ + extensions: { + complexity: 120, + }, + type: GT.NonNull(AccountUpdatePushNotificationSettingsPayload), + args: { + input: { type: GT.NonNull(AccountUpdatePushNotificationSettingsInput) }, + }, + resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { + return { + errors: [], + } + }, +}) + +export default AccountUpdatePushNotificationSettingsMutation diff --git a/src/graphql/public/schema.graphql b/src/graphql/public/schema.graphql index aceb12b117..1dfaf56d31 100644 --- a/src/graphql/public/schema.graphql +++ b/src/graphql/public/schema.graphql @@ -79,6 +79,16 @@ type AccountUpdateDisplayCurrencyPayload { errors: [Error!]! } +input AccountUpdatePushNotificationSettingsInput { + notificationSettings: [PushNotifcationSettingsInput]! + notificationsEnabled: Boolean! +} + +type AccountUpdatePushNotificationSettingsPayload { + account: ConsumerAccount + errors: [Error!]! +} + """An Opaque Bearer token""" scalar AuthToken @@ -622,6 +632,7 @@ type Mutation { accountDelete: AccountDeletePayload! accountUpdateDefaultWalletId(input: AccountUpdateDefaultWalletIdInput!): AccountUpdateDefaultWalletIdPayload! accountUpdateDisplayCurrency(input: AccountUpdateDisplayCurrencyInput!): AccountUpdateDisplayCurrencyPayload! + accountUpdatePushNotificationSettings(input: AccountUpdatePushNotificationSettingsInput!): AccountUpdatePushNotificationSettingsPayload! callbackEndpointAdd(input: CallbackEndpointAddInput!): CallbackEndpointAddPayload! callbackEndpointDelete(input: CallbackEndpointDeleteInput!): SuccessPayload! captchaCreateChallenge: CaptchaCreateChallengePayload! @@ -953,6 +964,16 @@ type PublicWallet { walletCurrency: WalletCurrency! } +input PushNotifcationSettingsInput { + disabledSubtypes: [PushNotificationSubType]! + enabled: Boolean! + type: PushNotificationType! +} + +scalar PushNotificationSubType + +scalar PushNotificationType + type Query { accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet! btcPrice(currency: DisplayCurrency! = "USD"): Price @deprecated(reason: "Deprecated in favor of realtimePrice") diff --git a/src/graphql/public/types/payload/account-update-push-notification-settings.ts b/src/graphql/public/types/payload/account-update-push-notification-settings.ts new file mode 100644 index 0000000000..61bf2a84df --- /dev/null +++ b/src/graphql/public/types/payload/account-update-push-notification-settings.ts @@ -0,0 +1,18 @@ +import { GT } from "@graphql/index" + +import IError from "../../../shared/types/abstract/error" +import ConsumerAccount from "../object/consumer-account" + +const AccountUpdatePushNotificationSettingsPayload = GT.Object({ + name: "AccountUpdatePushNotificationSettingsPayload", + fields: () => ({ + errors: { + type: GT.NonNullList(IError), + }, + account: { + type: ConsumerAccount, + }, + }), +}) + +export default AccountUpdatePushNotificationSettingsPayload diff --git a/src/graphql/shared/types/scalar/push-notification-sub-type.ts b/src/graphql/shared/types/scalar/push-notification-sub-type.ts new file mode 100644 index 0000000000..c695b5c36e --- /dev/null +++ b/src/graphql/shared/types/scalar/push-notification-sub-type.ts @@ -0,0 +1,30 @@ +import { InputValidationError } from "@graphql/error" +import { GT } from "@graphql/index" + +const PushNotificationSubType = GT.Scalar({ + name: "PushNotificationSubType", + parseValue(value) { + if (typeof value !== "string") { + return new InputValidationError({ + message: "Invalid type for PushNotificationSubType", + }) + } + return validPushNotificationSubType(value) + }, + parseLiteral(ast) { + if (ast.kind === GT.Kind.STRING) { + return validPushNotificationSubType(ast.value) + } + return new InputValidationError({ + message: "Invalid type for PushNotificationSubType", + }) + }, +}) + +function validPushNotificationSubType( + value: string, +): PushNotificationSubType | InputValidationError { + return value as PushNotificationSubType +} + +export default PushNotificationSubType diff --git a/src/graphql/shared/types/scalar/push-notification-type.ts b/src/graphql/shared/types/scalar/push-notification-type.ts new file mode 100644 index 0000000000..4731c2f84a --- /dev/null +++ b/src/graphql/shared/types/scalar/push-notification-type.ts @@ -0,0 +1,28 @@ +import { InputValidationError } from "@graphql/error" +import { GT } from "@graphql/index" + +const PushNotificationType = GT.Scalar({ + name: "PushNotificationType", + parseValue(value) { + if (typeof value !== "string") { + return new InputValidationError({ + message: "Invalid type for PushNotificationType", + }) + } + return validPushNotificationType(value) + }, + parseLiteral(ast) { + if (ast.kind === GT.Kind.STRING) { + return validPushNotificationType(ast.value) + } + return new InputValidationError({ message: "Invalid type for PushNotificationType" }) + }, +}) + +function validPushNotificationType( + value: string, +): PushNotificationType | InputValidationError { + return value as PushNotificationType +} + +export default PushNotificationType diff --git a/test/bats/gql/account-update-push-notification-settings.gql b/test/bats/gql/account-update-push-notification-settings.gql new file mode 100644 index 0000000000..8aa9d826d3 --- /dev/null +++ b/test/bats/gql/account-update-push-notification-settings.gql @@ -0,0 +1,10 @@ +mutation accountUpdatePushNotificationSettings($input: AccountUpdatePushNotificationSettingsInput!) { + accountUpdatePushNotificationSettings(input: $input) { + errors { + message + } + account { + defaultWalletId + } + } +} diff --git a/test/bats/push-notification-settings.bats b/test/bats/push-notification-settings.bats new file mode 100644 index 0000000000..93d37d043c --- /dev/null +++ b/test/bats/push-notification-settings.bats @@ -0,0 +1,44 @@ +#!/usr/bin/env bats + +load "helpers/setup-and-teardown" + +setup_file() { + clear_cache + reset_redis + + bitcoind_init + start_trigger + start_server + + initialize_user_from_onchain "$ALICE_TOKEN_NAME" "$ALICE_PHONE" "$CODE" +} + +teardown_file() { + stop_trigger + stop_server +} + + +@test "push-notification-settings: set and get" { + token_name="$ALICE_TOKEN_NAME" + + # notification_setting="{type: \"Circles\", enabled: false, disabledSubtypes: []}" + notification_setting=$( + jq -n \ + --arg type "Circles" \ + --argjson enabled false \ + --argjson disabledSubtypes "[]" \ + '{type: $type, enabled: $enabled, disabledSubtypes: $disabledSubtypes}' + ) + + variables=$( + jq -n \ + --argjson notification_setting "$notification_setting" \ + '{input: {notificationsEnabled: true, notificationSettings: [$notification_setting]}}' + ) + + exec_graphql "$token_name" 'account-update-push-notification-settings' "$variables" + + wallet_id="$(graphql_output '.data.accountUpdatePushNotificationSettings.account.defaultWalletId')" + [[ "$wallet_id" == "1232" ]] || exit 1 +} From 305c74bf70529605789039c09f10d5110e81fd19 Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Wed, 13 Sep 2023 14:55:01 -0500 Subject: [PATCH 02/12] feat: set push notification settings --- bats | 0 dev/apollo-federation/supergraph.graphql | 23 ++++++++-- src/app/accounts/index.ts | 1 + .../update-push-notification-settings.ts | 31 +++++++++++++ src/domain/accounts/index.types.d.ts | 1 + src/domain/notifications/errors.ts | 2 + src/domain/notifications/index.ts | 43 +++++++++++++++++++ src/domain/notifications/index.types.d.ts | 11 +++++ src/graphql/error-map.ts | 4 ++ ...count-update-push-notification-settings.ts | 37 +++++++++++++--- src/graphql/public/schema.graphql | 19 ++++++-- src/graphql/public/types/abstract/account.ts | 4 ++ .../public/types/object/business-account.ts | 5 +++ .../public/types/object/consumer-account.ts | 6 +++ .../object/push-notification-settings.ts | 42 ++++++++++++++++++ src/services/mongoose/accounts.ts | 8 ++++ src/services/mongoose/schema.ts | 28 ++++++++++++ src/services/mongoose/schema.types.d.ts | 1 + ...ount-update-push-notification-settings.gql | 7 ++- test/bats/push-notification-settings.bats | 7 ++- .../wallets/payment-input-validator.spec.ts | 4 ++ 21 files changed, 267 insertions(+), 17 deletions(-) delete mode 100644 bats create mode 100644 src/app/accounts/update-push-notification-settings.ts create mode 100644 src/graphql/public/types/object/push-notification-settings.ts diff --git a/bats b/bats deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dev/apollo-federation/supergraph.graphql b/dev/apollo-federation/supergraph.graphql index 8d0478e944..73e86d6534 100644 --- a/dev/apollo-federation/supergraph.graphql +++ b/dev/apollo-federation/supergraph.graphql @@ -31,6 +31,7 @@ interface Account id: ID! level: AccountLevel! limits: AccountLimits! + pushNotificationSettings: PushNotificationSettings! realtimePrice: RealtimePrice! transactions( """Returns the items in the list that come after the specified cursor.""" @@ -123,8 +124,8 @@ type AccountUpdateDisplayCurrencyPayload input AccountUpdatePushNotificationSettingsInput @join__type(graph: PUBLIC) { - notificationSettings: [PushNotifcationSettingsInput]! - notificationsEnabled: Boolean! + enabled: Boolean! + settings: [PushNotificationSettingsInput]! } type AccountUpdatePushNotificationSettingsPayload @@ -281,6 +282,7 @@ type ConsumerAccount implements Account id: ID! level: AccountLevel! limits: AccountLimits! + pushNotificationSettings: PushNotificationSettings! """List the quiz questions of the consumer account""" quiz: [Quiz!]! @@ -1229,7 +1231,22 @@ type PublicWallet walletCurrency: WalletCurrency! } -input PushNotifcationSettingsInput +type PushNotificationSetting + @join__type(graph: PUBLIC) +{ + disabledSubtypes: [PushNotificationSubType]! + enabled: Boolean! + type: PushNotificationType! +} + +type PushNotificationSettings + @join__type(graph: PUBLIC) +{ + enabled: Boolean! + settings: [PushNotificationSetting]! +} + +input PushNotificationSettingsInput @join__type(graph: PUBLIC) { disabledSubtypes: [PushNotificationSubType]! diff --git a/src/app/accounts/index.ts b/src/app/accounts/index.ts index 285cdb3953..60192e4401 100644 --- a/src/app/accounts/index.ts +++ b/src/app/accounts/index.ts @@ -21,6 +21,7 @@ export * from "./update-display-currency" export * from "./username-available" export * from "./delete-business-map-info" export * from "./upgrade-device-account" +export * from "./update-push-notification-settings" const accounts = AccountsRepository() diff --git a/src/app/accounts/update-push-notification-settings.ts b/src/app/accounts/update-push-notification-settings.ts new file mode 100644 index 0000000000..d0fee86829 --- /dev/null +++ b/src/app/accounts/update-push-notification-settings.ts @@ -0,0 +1,31 @@ +import { checkedToPushNotificationSettings } from "@domain/notifications" + +import { AccountsRepository } from "@services/mongoose" + +export const updatePushNotificationSettings = async ({ + accountId, + notificationSettings, +}: { + accountId: AccountId + notificationSettings: { + enabled: boolean + settings: { + type: string + enabled: boolean + disabledSubtypes: string[] + }[] + } +}): Promise => { + const checkedPushNotificationSettings = + checkedToPushNotificationSettings(notificationSettings) + + if (checkedPushNotificationSettings instanceof Error) + return checkedPushNotificationSettings + + const account = await AccountsRepository().findById(accountId) + if (account instanceof Error) return account + + account.pushNotificationSettings = checkedPushNotificationSettings + + return AccountsRepository().update(account) +} diff --git a/src/domain/accounts/index.types.d.ts b/src/domain/accounts/index.types.d.ts index 2c3baf4425..b85b5117cd 100644 --- a/src/domain/accounts/index.types.d.ts +++ b/src/domain/accounts/index.types.d.ts @@ -77,6 +77,7 @@ type Account = { readonly isEditor: boolean readonly quizQuestions: UserQuizQuestion[] // deprecated readonly quiz: Quiz[] + pushNotificationSettings: PushNotificationSettings // QUESTION: should this be here or should the notification service have its own storage of notification settings? kratosUserId: UserId displayCurrency: DisplayCurrency // temp diff --git a/src/domain/notifications/errors.ts b/src/domain/notifications/errors.ts index 7003e56896..3dbce67a3a 100644 --- a/src/domain/notifications/errors.ts +++ b/src/domain/notifications/errors.ts @@ -17,3 +17,5 @@ export class NotificationsServiceUnreachableServerError extends NotificationsSer export class UnknownNotificationsServiceError extends NotificationsError { level = ErrorLevel.Critical } + +export class InvalidPushNotificationSettingError extends NotificationsError {} diff --git a/src/domain/notifications/index.ts b/src/domain/notifications/index.ts index 867b2ed1f6..2b53767af1 100644 --- a/src/domain/notifications/index.ts +++ b/src/domain/notifications/index.ts @@ -1,3 +1,5 @@ +import { InvalidPushNotificationSettingError } from "./errors" + export * from "./errors" export const NotificationType = { @@ -8,3 +10,44 @@ export const NotificationType = { OnchainPayment: "onchain_payment", LnInvoicePaid: "paid-invoice", } as const + +export const checkedToPushNotificationSettings = ({ + enabled, + settings, +}: { + enabled: boolean + settings: { + type: string + enabled: boolean + disabledSubtypes: string[] + }[] +}): PushNotificationSettings | InvalidPushNotificationSettingError => { + const checkedSettings = settings.map(checkedToPushNotificationSetting) + + const notificationTypes = checkedSettings.map((s) => s.type) + const uniqueTypes = [...new Set(notificationTypes)] + if (notificationTypes.length !== uniqueTypes.length) { + return new InvalidPushNotificationSettingError("Duplicate notification types") + } + + return { + enabled, + settings: checkedSettings, + } +} + +export const checkedToPushNotificationSetting = ({ + type, + enabled, + disabledSubtypes, +}: { + type: string + enabled: boolean + disabledSubtypes: string[] +}): PushNotificationSetting => { + return { + type: type as PushNotificationType, + enabled, + disabledSubtypes: disabledSubtypes as PushNotificationSubType[], + } +} diff --git a/src/domain/notifications/index.types.d.ts b/src/domain/notifications/index.types.d.ts index af94f7552e..bcce957d58 100644 --- a/src/domain/notifications/index.types.d.ts +++ b/src/domain/notifications/index.types.d.ts @@ -73,3 +73,14 @@ interface INotificationsService { args: SendPushNotificationArgs, ): Promise } + +type PushNotificationSettings = { + enabled: boolean + settings: PushNotificationSetting[] +} + +type PushNotificationSetting = { + type: PushNotificationType + enabled: boolean + disabledSubtypes: PushNotificationSubType[] +} diff --git a/src/graphql/error-map.ts b/src/graphql/error-map.ts index 2585128188..71ff54ce11 100644 --- a/src/graphql/error-map.ts +++ b/src/graphql/error-map.ts @@ -256,6 +256,10 @@ export const mapError = (error: ApplicationError): CustomApolloError => { message = "Invalid quiz question id was passed." return new ValidationInternalError({ message, logger: baseLogger }) + case "InvalidPushNotificationSettingError": + message = "Invalid push notification setting was passed." + return new ValidationInternalError({ message, logger: baseLogger }) + case "RewardAlreadyPresentError": message = "Reward for quiz question was already claimed." return new ValidationInternalError({ message, logger: baseLogger }) diff --git a/src/graphql/public/root/mutation/account-update-push-notification-settings.ts b/src/graphql/public/root/mutation/account-update-push-notification-settings.ts index 8323f646df..08aea638a8 100644 --- a/src/graphql/public/root/mutation/account-update-push-notification-settings.ts +++ b/src/graphql/public/root/mutation/account-update-push-notification-settings.ts @@ -3,9 +3,11 @@ import { GT } from "@graphql/index" import PushNotificationType from "@graphql/shared/types/scalar/push-notification-type" import PushNotificationSubType from "@graphql/shared/types/scalar/push-notification-sub-type" import AccountUpdatePushNotificationSettingsPayload from "@graphql/public/types/payload/account-update-push-notification-settings" +import { Accounts } from "@app/index" +import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" -const PushNotifcationSettingsInput = GT.Input({ - name: "PushNotifcationSettingsInput", +const PushNotificationSettingsInput = GT.Input({ + name: "PushNotificationSettingsInput", fields: () => ({ type: { type: GT.NonNull(PushNotificationType) }, enabled: { type: GT.NonNull(GT.Boolean) }, @@ -16,14 +18,27 @@ const PushNotifcationSettingsInput = GT.Input({ const AccountUpdatePushNotificationSettingsInput = GT.Input({ name: "AccountUpdatePushNotificationSettingsInput", fields: () => ({ - notificationsEnabled: { type: GT.NonNull(GT.Boolean) }, - notificationSettings: { - type: GT.NonNull(GT.List(PushNotifcationSettingsInput)), + enabled: { type: GT.NonNull(GT.Boolean) }, + settings: { + type: GT.NonNull(GT.List(PushNotificationSettingsInput)), }, }), }) -const AccountUpdatePushNotificationSettingsMutation = GT.Field({ +const AccountUpdatePushNotificationSettingsMutation = GT.Field< + null, + GraphQLPublicContextAuth, + { + input: { + enabled: boolean + settings: { + type: string + enabled: boolean + disabledSubtypes: string[] + }[] + } + } +>({ extensions: { complexity: 120, }, @@ -32,8 +47,18 @@ const AccountUpdatePushNotificationSettingsMutation = GT.Field({ input: { type: GT.NonNull(AccountUpdatePushNotificationSettingsInput) }, }, resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { + const result = await Accounts.updatePushNotificationSettings({ + accountId: domainAccount.id, + notificationSettings: args.input, + }) + + if (result instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(result)] } + } + return { errors: [], + account: result, } }, }) diff --git a/src/graphql/public/schema.graphql b/src/graphql/public/schema.graphql index 1dfaf56d31..4feb2a8f84 100644 --- a/src/graphql/public/schema.graphql +++ b/src/graphql/public/schema.graphql @@ -6,6 +6,7 @@ interface Account { id: ID! level: AccountLevel! limits: AccountLimits! + pushNotificationSettings: PushNotificationSettings! realtimePrice: RealtimePrice! transactions( """Returns the items in the list that come after the specified cursor.""" @@ -80,8 +81,8 @@ type AccountUpdateDisplayCurrencyPayload { } input AccountUpdatePushNotificationSettingsInput { - notificationSettings: [PushNotifcationSettingsInput]! - notificationsEnabled: Boolean! + enabled: Boolean! + settings: [PushNotificationSettingsInput]! } type AccountUpdatePushNotificationSettingsPayload { @@ -208,6 +209,7 @@ type ConsumerAccount implements Account { id: ID! level: AccountLevel! limits: AccountLimits! + pushNotificationSettings: PushNotificationSettings! """List the quiz questions of the consumer account""" quiz: [Quiz!]! @@ -964,7 +966,18 @@ type PublicWallet { walletCurrency: WalletCurrency! } -input PushNotifcationSettingsInput { +type PushNotificationSetting { + disabledSubtypes: [PushNotificationSubType]! + enabled: Boolean! + type: PushNotificationType! +} + +type PushNotificationSettings { + enabled: Boolean! + settings: [PushNotificationSetting]! +} + +input PushNotificationSettingsInput { disabledSubtypes: [PushNotificationSubType]! enabled: Boolean! type: PushNotificationType! diff --git a/src/graphql/public/types/abstract/account.ts b/src/graphql/public/types/abstract/account.ts index 9e19df3349..232fa9ea61 100644 --- a/src/graphql/public/types/abstract/account.ts +++ b/src/graphql/public/types/abstract/account.ts @@ -11,6 +11,7 @@ import AccountLevel from "../../../shared/types/scalar/account-level" import Wallet from "../../../shared/types/abstract/wallet" import CallbackEndpoint from "../object/callback-endpoint" +import { PushNotificationSettings } from "../object/push-notification-settings" const IAccount = GT.Interface({ name: "Account", @@ -56,6 +57,9 @@ const IAccount = GT.Interface({ }, }, }, + pushNotificationSettings: { + type: GT.NonNull(PushNotificationSettings), + }, // FUTURE-PLAN: Support a `users: [User!]!` field here }), diff --git a/src/graphql/public/types/object/business-account.ts b/src/graphql/public/types/object/business-account.ts index 1b93d01d6f..1cdf714f07 100644 --- a/src/graphql/public/types/object/business-account.ts +++ b/src/graphql/public/types/object/business-account.ts @@ -28,6 +28,7 @@ import AccountLevel from "../../../shared/types/scalar/account-level" import { TransactionConnection } from "../../../shared/types/object/transaction" import RealtimePrice from "./realtime-price" +import { PushNotificationSettings } from "./push-notification-settings" const BusinessAccount = GT.Object({ name: "BusinessAccount", @@ -159,6 +160,10 @@ const BusinessAccount = GT.Object({ ) }, }, + pushNotificationSettings: { + type: GT.NonNull(PushNotificationSettings), + resolve: (source) => source.pushNotificationSettings, + }, }), }) diff --git a/src/graphql/public/types/object/consumer-account.ts b/src/graphql/public/types/object/consumer-account.ts index 17bc5ab557..3d35976365 100644 --- a/src/graphql/public/types/object/consumer-account.ts +++ b/src/graphql/public/types/object/consumer-account.ts @@ -32,6 +32,7 @@ import { TransactionConnection } from "../../../shared/types/object/transaction" import AccountLimits from "./account-limits" import Quiz from "./quiz" import CallbackEndpoint from "./callback-endpoint" +import { PushNotificationSettings } from "./push-notification-settings" const ConsumerAccount = GT.Object({ name: "ConsumerAccount", @@ -182,6 +183,11 @@ const ConsumerAccount = GT.Object({ ) }, }, + + pushNotificationSettings: { + type: GT.NonNull(PushNotificationSettings), + resolve: (source) => source.pushNotificationSettings, + }, }), }) diff --git a/src/graphql/public/types/object/push-notification-settings.ts b/src/graphql/public/types/object/push-notification-settings.ts new file mode 100644 index 0000000000..b224ab3142 --- /dev/null +++ b/src/graphql/public/types/object/push-notification-settings.ts @@ -0,0 +1,42 @@ +import { GT } from "@graphql/index" +import PushNotificationSubType from "@graphql/shared/types/scalar/push-notification-sub-type" +import PushNotificationType from "@graphql/shared/types/scalar/push-notification-type" + +export const PushNotificationSettings = GT.Object< + PushNotificationSettings, + GraphQLPublicContextAuth +>({ + name: "PushNotificationSettings", + fields: () => ({ + enabled: { + type: GT.NonNull(GT.Boolean), + resolve: (source) => source.enabled, + }, + settings: { + type: GT.NonNull(GT.List(PushNotificationSetting)), + resolve: (source) => source.settings, + }, + }), +}) + +export const PushNotificationSetting = GT.Object< + PushNotificationSetting, + GraphQLPublicContextAuth +>({ + name: "PushNotificationSetting", + fields: () => ({ + type: { + type: GT.NonNull(PushNotificationType), + + resolve: (source) => source.type, + }, + enabled: { + type: GT.NonNull(GT.Boolean), + resolve: (source) => source.enabled, + }, + disabledSubtypes: { + type: GT.NonNull(GT.List(PushNotificationSubType)), + resolve: (source) => source.disabledSubtypes, + }, + }), +}) diff --git a/src/services/mongoose/accounts.ts b/src/services/mongoose/accounts.ts index ccf44c7b09..7e14e6ffe8 100644 --- a/src/services/mongoose/accounts.ts +++ b/src/services/mongoose/accounts.ts @@ -118,6 +118,7 @@ export const AccountsRepository = (): IAccountsRepository => { withdrawFee, kratosUserId, displayCurrency, + pushNotificationSettings, role, }: Account): Promise => { @@ -142,6 +143,7 @@ export const AccountsRepository = (): IAccountsRepository => { withdrawFee, kratosUserId, displayCurrency, + pushNotificationSettings, role, }, @@ -225,6 +227,12 @@ const translateToAccount = (result: AccountRecord): Account => ({ ), withdrawFee: result.withdrawFee as Satoshis, isEditor: result.role === "editor", + pushNotificationSettings: + result.pushNotificationSettings || + ({ + enabled: true, + settings: [], + } as PushNotificationSettings), // TODO: remove quizQuestions: diff --git a/src/services/mongoose/schema.ts b/src/services/mongoose/schema.ts index 0ae905ad11..42157efc80 100644 --- a/src/services/mongoose/schema.ts +++ b/src/services/mongoose/schema.ts @@ -259,6 +259,34 @@ const AccountSchema = new Schema( }, ], }, + pushNotificationSettings: { + type: { + enabled: { + type: Boolean, + default: true, + }, + settings: { + type: [ + { + type: { + type: String, + required: true, + }, + enabled: { + type: Boolean, + required: true, + }, + disabledSubtypes: { + type: [String], + required: true, + default: [], + }, + }, + ], + default: [], + }, + }, + }, defaultWalletId: { type: String, diff --git a/src/services/mongoose/schema.types.d.ts b/src/services/mongoose/schema.types.d.ts index 21e23ad961..21be2e55ca 100644 --- a/src/services/mongoose/schema.types.d.ts +++ b/src/services/mongoose/schema.types.d.ts @@ -84,6 +84,7 @@ interface AccountRecord { onchain: OnChainObjectForUser[] defaultWalletId: WalletId displayCurrency?: string + pushNotificationSettings?: PushNotificationSettings // business: title?: string diff --git a/test/bats/gql/account-update-push-notification-settings.gql b/test/bats/gql/account-update-push-notification-settings.gql index 8aa9d826d3..b551a3c1a7 100644 --- a/test/bats/gql/account-update-push-notification-settings.gql +++ b/test/bats/gql/account-update-push-notification-settings.gql @@ -4,7 +4,12 @@ mutation accountUpdatePushNotificationSettings($input: AccountUpdatePushNotifica message } account { - defaultWalletId + pushNotificationSettings { + enabled + settings { + type + } + } } } } diff --git a/test/bats/push-notification-settings.bats b/test/bats/push-notification-settings.bats index 93d37d043c..ff889858a8 100644 --- a/test/bats/push-notification-settings.bats +++ b/test/bats/push-notification-settings.bats @@ -22,7 +22,6 @@ teardown_file() { @test "push-notification-settings: set and get" { token_name="$ALICE_TOKEN_NAME" - # notification_setting="{type: \"Circles\", enabled: false, disabledSubtypes: []}" notification_setting=$( jq -n \ --arg type "Circles" \ @@ -34,11 +33,11 @@ teardown_file() { variables=$( jq -n \ --argjson notification_setting "$notification_setting" \ - '{input: {notificationsEnabled: true, notificationSettings: [$notification_setting]}}' + '{input: {enabled: true, settings: [$notification_setting]}}' ) exec_graphql "$token_name" 'account-update-push-notification-settings' "$variables" - wallet_id="$(graphql_output '.data.accountUpdatePushNotificationSettings.account.defaultWalletId')" - [[ "$wallet_id" == "1232" ]] || exit 1 + notification_type="$(graphql_output '.data.accountUpdatePushNotificationSettings.account.pushNotificationSettings.settings[0].type')" + [[ "$notification_type" == "Circles" ]] || exit 1 } diff --git a/test/unit/domain/wallets/payment-input-validator.spec.ts b/test/unit/domain/wallets/payment-input-validator.spec.ts index 72b35c92d6..532e6815e8 100644 --- a/test/unit/domain/wallets/payment-input-validator.spec.ts +++ b/test/unit/domain/wallets/payment-input-validator.spec.ts @@ -22,6 +22,10 @@ describe("PaymentInputValidator", () => { latitude: 0, longitude: 0, }, + pushNotificationSettings: { + enabled: true, + settings: [], + }, contactEnabled: true, contacts: [], isEditor: false, From 0d72d3f51dbb5567d57d3023ad93e0d1f256b4d7 Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Thu, 14 Sep 2023 15:12:45 -0500 Subject: [PATCH 03/12] chore: change notification settings schema --- dev/apollo-federation/supergraph.graphql | 27 ++-------- .../update-push-notification-settings.ts | 19 +++---- src/domain/notifications/index.ts | 50 ++++++++----------- src/domain/notifications/index.types.d.ts | 10 +--- src/domain/primitives/index.types.d.ts | 1 - ...count-update-push-notification-settings.ts | 30 ++++------- src/graphql/public/schema.graphql | 22 ++------ .../object/push-notification-settings.ts | 33 ++---------- .../scalar/push-notification-sub-type.ts | 30 ----------- src/services/mongoose/accounts.ts | 4 +- src/services/mongoose/schema.ts | 23 ++------- ...ount-update-push-notification-settings.gql | 6 +-- test/bats/push-notification-settings.bats | 18 ++----- .../wallets/payment-input-validator.spec.ts | 4 +- 14 files changed, 69 insertions(+), 208 deletions(-) delete mode 100644 src/graphql/shared/types/scalar/push-notification-sub-type.ts diff --git a/dev/apollo-federation/supergraph.graphql b/dev/apollo-federation/supergraph.graphql index 73e86d6534..577f5d3936 100644 --- a/dev/apollo-federation/supergraph.graphql +++ b/dev/apollo-federation/supergraph.graphql @@ -124,8 +124,8 @@ type AccountUpdateDisplayCurrencyPayload input AccountUpdatePushNotificationSettingsInput @join__type(graph: PUBLIC) { - enabled: Boolean! - settings: [PushNotificationSettingsInput]! + disabledPushNotificationTypes: [PushNotificationType]! + pushNotificationsEnabled: Boolean! } type AccountUpdatePushNotificationSettingsPayload @@ -1231,32 +1231,13 @@ type PublicWallet walletCurrency: WalletCurrency! } -type PushNotificationSetting - @join__type(graph: PUBLIC) -{ - disabledSubtypes: [PushNotificationSubType]! - enabled: Boolean! - type: PushNotificationType! -} - type PushNotificationSettings @join__type(graph: PUBLIC) { - enabled: Boolean! - settings: [PushNotificationSetting]! -} - -input PushNotificationSettingsInput - @join__type(graph: PUBLIC) -{ - disabledSubtypes: [PushNotificationSubType]! - enabled: Boolean! - type: PushNotificationType! + disabledPushNotificationTypes: [PushNotificationType]! + pushNotificationsEnabled: Boolean! } -scalar PushNotificationSubType - @join__type(graph: PUBLIC) - scalar PushNotificationType @join__type(graph: PUBLIC) diff --git a/src/app/accounts/update-push-notification-settings.ts b/src/app/accounts/update-push-notification-settings.ts index d0fee86829..cc5e5d0dd0 100644 --- a/src/app/accounts/update-push-notification-settings.ts +++ b/src/app/accounts/update-push-notification-settings.ts @@ -4,20 +4,17 @@ import { AccountsRepository } from "@services/mongoose" export const updatePushNotificationSettings = async ({ accountId, - notificationSettings, + pushNotificationsEnabled, + disabledPushNotificationTypes, }: { accountId: AccountId - notificationSettings: { - enabled: boolean - settings: { - type: string - enabled: boolean - disabledSubtypes: string[] - }[] - } + pushNotificationsEnabled: boolean + disabledPushNotificationTypes: string[] }): Promise => { - const checkedPushNotificationSettings = - checkedToPushNotificationSettings(notificationSettings) + const checkedPushNotificationSettings = checkedToPushNotificationSettings({ + disabledPushNotificationTypes, + pushNotificationsEnabled, + }) if (checkedPushNotificationSettings instanceof Error) return checkedPushNotificationSettings diff --git a/src/domain/notifications/index.ts b/src/domain/notifications/index.ts index 2b53767af1..fc0f56ca5c 100644 --- a/src/domain/notifications/index.ts +++ b/src/domain/notifications/index.ts @@ -12,42 +12,36 @@ export const NotificationType = { } as const export const checkedToPushNotificationSettings = ({ - enabled, - settings, + disabledPushNotificationTypes, + pushNotificationsEnabled, }: { - enabled: boolean - settings: { - type: string - enabled: boolean - disabledSubtypes: string[] - }[] + disabledPushNotificationTypes: string[] + pushNotificationsEnabled: boolean }): PushNotificationSettings | InvalidPushNotificationSettingError => { - const checkedSettings = settings.map(checkedToPushNotificationSetting) + const checkedDisabledPushNotificationTypes: PushNotificationType[] = [] - const notificationTypes = checkedSettings.map((s) => s.type) - const uniqueTypes = [...new Set(notificationTypes)] - if (notificationTypes.length !== uniqueTypes.length) { - return new InvalidPushNotificationSettingError("Duplicate notification types") + for (const pushNotification of disabledPushNotificationTypes) { + const checkedPushNotification = checkedToPushNotificationType(pushNotification) + if (checkedPushNotification instanceof Error) { + return checkedPushNotification + } else { + checkedDisabledPushNotificationTypes.push(checkedPushNotification) + } } return { - enabled, - settings: checkedSettings, + pushNotificationsEnabled, + disabledPushNotificationTypes: checkedDisabledPushNotificationTypes, } } -export const checkedToPushNotificationSetting = ({ - type, - enabled, - disabledSubtypes, -}: { - type: string - enabled: boolean - disabledSubtypes: string[] -}): PushNotificationSetting => { - return { - type: type as PushNotificationType, - enabled, - disabledSubtypes: disabledSubtypes as PushNotificationSubType[], +const checkedToPushNotificationType = ( + type: string, +): PushNotificationType | ValidationError => { + // TODO: add validation + if (!type) { + return new InvalidPushNotificationSettingError("Invalid notification type") } + + return type as PushNotificationType } diff --git a/src/domain/notifications/index.types.d.ts b/src/domain/notifications/index.types.d.ts index bcce957d58..640c97af2a 100644 --- a/src/domain/notifications/index.types.d.ts +++ b/src/domain/notifications/index.types.d.ts @@ -75,12 +75,6 @@ interface INotificationsService { } type PushNotificationSettings = { - enabled: boolean - settings: PushNotificationSetting[] -} - -type PushNotificationSetting = { - type: PushNotificationType - enabled: boolean - disabledSubtypes: PushNotificationSubType[] + pushNotificationsEnabled: boolean + disabledPushNotificationTypes: PushNotificationType[] } diff --git a/src/domain/primitives/index.types.d.ts b/src/domain/primitives/index.types.d.ts index f6322b8575..7bfa578b58 100644 --- a/src/domain/primitives/index.types.d.ts +++ b/src/domain/primitives/index.types.d.ts @@ -12,7 +12,6 @@ type Days = number & { readonly brand: unique symbol } type JwtToken = string & { readonly brand: unique symbol } // short lived asymmetric token from oathkeeper type Memo = string & { readonly brand: unique symbol } type PushNotificationType = string & { readonly brand: unique symbol } -type PushNotificationSubType = string & { readonly brand: unique symbol } type XOR = | (T1 & { [k in Exclude]?: never }) diff --git a/src/graphql/public/root/mutation/account-update-push-notification-settings.ts b/src/graphql/public/root/mutation/account-update-push-notification-settings.ts index 08aea638a8..9e10c4fa23 100644 --- a/src/graphql/public/root/mutation/account-update-push-notification-settings.ts +++ b/src/graphql/public/root/mutation/account-update-push-notification-settings.ts @@ -1,26 +1,18 @@ import { GT } from "@graphql/index" import PushNotificationType from "@graphql/shared/types/scalar/push-notification-type" -import PushNotificationSubType from "@graphql/shared/types/scalar/push-notification-sub-type" import AccountUpdatePushNotificationSettingsPayload from "@graphql/public/types/payload/account-update-push-notification-settings" import { Accounts } from "@app/index" import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" -const PushNotificationSettingsInput = GT.Input({ - name: "PushNotificationSettingsInput", - fields: () => ({ - type: { type: GT.NonNull(PushNotificationType) }, - enabled: { type: GT.NonNull(GT.Boolean) }, - disabledSubtypes: { type: GT.NonNull(GT.List(PushNotificationSubType)) }, - }), -}) - const AccountUpdatePushNotificationSettingsInput = GT.Input({ name: "AccountUpdatePushNotificationSettingsInput", fields: () => ({ - enabled: { type: GT.NonNull(GT.Boolean) }, - settings: { - type: GT.NonNull(GT.List(PushNotificationSettingsInput)), + pushNotificationsEnabled: { + type: GT.NonNull(GT.Boolean), + }, + disabledPushNotificationTypes: { + type: GT.NonNull(GT.List(PushNotificationType)), }, }), }) @@ -30,12 +22,8 @@ const AccountUpdatePushNotificationSettingsMutation = GT.Field< GraphQLPublicContextAuth, { input: { - enabled: boolean - settings: { - type: string - enabled: boolean - disabledSubtypes: string[] - }[] + disabledPushNotificationTypes: string[] + pushNotificationsEnabled: boolean } } >({ @@ -47,9 +35,11 @@ const AccountUpdatePushNotificationSettingsMutation = GT.Field< input: { type: GT.NonNull(AccountUpdatePushNotificationSettingsInput) }, }, resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { + const { disabledPushNotificationTypes, pushNotificationsEnabled } = args.input const result = await Accounts.updatePushNotificationSettings({ accountId: domainAccount.id, - notificationSettings: args.input, + disabledPushNotificationTypes, + pushNotificationsEnabled, }) if (result instanceof Error) { diff --git a/src/graphql/public/schema.graphql b/src/graphql/public/schema.graphql index 4feb2a8f84..29ea30988d 100644 --- a/src/graphql/public/schema.graphql +++ b/src/graphql/public/schema.graphql @@ -81,8 +81,8 @@ type AccountUpdateDisplayCurrencyPayload { } input AccountUpdatePushNotificationSettingsInput { - enabled: Boolean! - settings: [PushNotificationSettingsInput]! + disabledPushNotificationTypes: [PushNotificationType]! + pushNotificationsEnabled: Boolean! } type AccountUpdatePushNotificationSettingsPayload { @@ -966,25 +966,11 @@ type PublicWallet { walletCurrency: WalletCurrency! } -type PushNotificationSetting { - disabledSubtypes: [PushNotificationSubType]! - enabled: Boolean! - type: PushNotificationType! -} - type PushNotificationSettings { - enabled: Boolean! - settings: [PushNotificationSetting]! + disabledPushNotificationTypes: [PushNotificationType]! + pushNotificationsEnabled: Boolean! } -input PushNotificationSettingsInput { - disabledSubtypes: [PushNotificationSubType]! - enabled: Boolean! - type: PushNotificationType! -} - -scalar PushNotificationSubType - scalar PushNotificationType type Query { diff --git a/src/graphql/public/types/object/push-notification-settings.ts b/src/graphql/public/types/object/push-notification-settings.ts index b224ab3142..c5777ad22c 100644 --- a/src/graphql/public/types/object/push-notification-settings.ts +++ b/src/graphql/public/types/object/push-notification-settings.ts @@ -1,5 +1,4 @@ import { GT } from "@graphql/index" -import PushNotificationSubType from "@graphql/shared/types/scalar/push-notification-sub-type" import PushNotificationType from "@graphql/shared/types/scalar/push-notification-type" export const PushNotificationSettings = GT.Object< @@ -8,35 +7,13 @@ export const PushNotificationSettings = GT.Object< >({ name: "PushNotificationSettings", fields: () => ({ - enabled: { + pushNotificationsEnabled: { type: GT.NonNull(GT.Boolean), - resolve: (source) => source.enabled, + resolve: (source) => source.pushNotificationsEnabled, }, - settings: { - type: GT.NonNull(GT.List(PushNotificationSetting)), - resolve: (source) => source.settings, - }, - }), -}) - -export const PushNotificationSetting = GT.Object< - PushNotificationSetting, - GraphQLPublicContextAuth ->({ - name: "PushNotificationSetting", - fields: () => ({ - type: { - type: GT.NonNull(PushNotificationType), - - resolve: (source) => source.type, - }, - enabled: { - type: GT.NonNull(GT.Boolean), - resolve: (source) => source.enabled, - }, - disabledSubtypes: { - type: GT.NonNull(GT.List(PushNotificationSubType)), - resolve: (source) => source.disabledSubtypes, + disabledPushNotificationTypes: { + type: GT.NonNull(GT.List(PushNotificationType)), + resolve: (source) => source.disabledPushNotificationTypes, }, }), }) diff --git a/src/graphql/shared/types/scalar/push-notification-sub-type.ts b/src/graphql/shared/types/scalar/push-notification-sub-type.ts deleted file mode 100644 index c695b5c36e..0000000000 --- a/src/graphql/shared/types/scalar/push-notification-sub-type.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { InputValidationError } from "@graphql/error" -import { GT } from "@graphql/index" - -const PushNotificationSubType = GT.Scalar({ - name: "PushNotificationSubType", - parseValue(value) { - if (typeof value !== "string") { - return new InputValidationError({ - message: "Invalid type for PushNotificationSubType", - }) - } - return validPushNotificationSubType(value) - }, - parseLiteral(ast) { - if (ast.kind === GT.Kind.STRING) { - return validPushNotificationSubType(ast.value) - } - return new InputValidationError({ - message: "Invalid type for PushNotificationSubType", - }) - }, -}) - -function validPushNotificationSubType( - value: string, -): PushNotificationSubType | InputValidationError { - return value as PushNotificationSubType -} - -export default PushNotificationSubType diff --git a/src/services/mongoose/accounts.ts b/src/services/mongoose/accounts.ts index 7e14e6ffe8..a1e01459e0 100644 --- a/src/services/mongoose/accounts.ts +++ b/src/services/mongoose/accounts.ts @@ -230,8 +230,8 @@ const translateToAccount = (result: AccountRecord): Account => ({ pushNotificationSettings: result.pushNotificationSettings || ({ - enabled: true, - settings: [], + pushNotificationsEnabled: true, + disabledPushNotificationTypes: [], } as PushNotificationSettings), // TODO: remove diff --git a/src/services/mongoose/schema.ts b/src/services/mongoose/schema.ts index 42157efc80..f4ec82868b 100644 --- a/src/services/mongoose/schema.ts +++ b/src/services/mongoose/schema.ts @@ -261,28 +261,13 @@ const AccountSchema = new Schema( }, pushNotificationSettings: { type: { - enabled: { + pushNotificationsEnabled: { type: Boolean, default: true, }, - settings: { - type: [ - { - type: { - type: String, - required: true, - }, - enabled: { - type: Boolean, - required: true, - }, - disabledSubtypes: { - type: [String], - required: true, - default: [], - }, - }, - ], + disabledPushNotificationTypes: { + type: [String], + required: true, default: [], }, }, diff --git a/test/bats/gql/account-update-push-notification-settings.gql b/test/bats/gql/account-update-push-notification-settings.gql index b551a3c1a7..195d37c4b1 100644 --- a/test/bats/gql/account-update-push-notification-settings.gql +++ b/test/bats/gql/account-update-push-notification-settings.gql @@ -5,10 +5,8 @@ mutation accountUpdatePushNotificationSettings($input: AccountUpdatePushNotifica } account { pushNotificationSettings { - enabled - settings { - type - } + pushNotificationsEnabled + disabledPushNotificationTypes } } } diff --git a/test/bats/push-notification-settings.bats b/test/bats/push-notification-settings.bats index ff889858a8..83c1566ec9 100644 --- a/test/bats/push-notification-settings.bats +++ b/test/bats/push-notification-settings.bats @@ -20,24 +20,14 @@ teardown_file() { @test "push-notification-settings: set and get" { - token_name="$ALICE_TOKEN_NAME" - - notification_setting=$( - jq -n \ - --arg type "Circles" \ - --argjson enabled false \ - --argjson disabledSubtypes "[]" \ - '{type: $type, enabled: $enabled, disabledSubtypes: $disabledSubtypes}' - ) + token_name="$ALICE_TOKEN_NAME" variables=$( jq -n \ - --argjson notification_setting "$notification_setting" \ - '{input: {enabled: true, settings: [$notification_setting]}}' - ) + '{input: { pushNotificationsEnabled: true, disabledPushNotificationTypes: [ "Circles" ] }}') exec_graphql "$token_name" 'account-update-push-notification-settings' "$variables" - notification_type="$(graphql_output '.data.accountUpdatePushNotificationSettings.account.pushNotificationSettings.settings[0].type')" - [[ "$notification_type" == "Circles" ]] || exit 1 + disabled_notification="$(graphql_output '.data.accountUpdatePushNotificationSettings.account.pushNotificationSettings.disabledPushNotificationTypes[0]')" + [[ "$disabled_notification" == "Circles" ]] || exit 1 } diff --git a/test/unit/domain/wallets/payment-input-validator.spec.ts b/test/unit/domain/wallets/payment-input-validator.spec.ts index 532e6815e8..a2aeae7ed0 100644 --- a/test/unit/domain/wallets/payment-input-validator.spec.ts +++ b/test/unit/domain/wallets/payment-input-validator.spec.ts @@ -23,8 +23,8 @@ describe("PaymentInputValidator", () => { longitude: 0, }, pushNotificationSettings: { - enabled: true, - settings: [], + pushNotificationsEnabled: true, + disabledPushNotificationTypes: [], }, contactEnabled: true, contacts: [], From 8b626468c9d8c1e991bbf5f0a48c2e85f0ebe409 Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Fri, 15 Sep 2023 13:11:17 -0500 Subject: [PATCH 04/12] chore: add notification filtering --- src/domain/notifications/index.ts | 35 +++++++++++++++ src/domain/notifications/index.types.d.ts | 3 ++ .../create-push-notification-content.ts | 12 ++++- src/services/notifications/index.ts | 32 +++++++++++++ .../notifications/push-notifications.ts | 45 ++++++++++++++++++- .../push-notifications.types.d.ts | 19 ++++++++ 6 files changed, 144 insertions(+), 2 deletions(-) diff --git a/src/domain/notifications/index.ts b/src/domain/notifications/index.ts index fc0f56ca5c..d0ea2c7083 100644 --- a/src/domain/notifications/index.ts +++ b/src/domain/notifications/index.ts @@ -35,6 +35,25 @@ export const checkedToPushNotificationSettings = ({ } } +export const GaloyPushNotifications = { + Payments: "Payments" as PushNotificationType, + Balance: "Balance" as PushNotificationType, +} as const + +export const mapNotificationTypeToPushNotificationType = ( + notificationType: NotificationType, +): PushNotificationType => { + switch (notificationType) { + case NotificationType.IntraLedgerReceipt: + case NotificationType.IntraLedgerPayment: + case NotificationType.OnchainReceipt: + case NotificationType.OnchainReceiptPending: + case NotificationType.LnInvoicePaid: + case NotificationType.OnchainPayment: + return GaloyPushNotifications.Payments + } +} + const checkedToPushNotificationType = ( type: string, ): PushNotificationType | ValidationError => { @@ -45,3 +64,19 @@ const checkedToPushNotificationType = ( return type as PushNotificationType } + +export const shouldSendPushNotification = ({ + pushNotificationSettings, + pushNotificationType, +}: { + pushNotificationSettings: PushNotificationSettings + pushNotificationType: PushNotificationType +}): boolean => { + if (pushNotificationSettings.pushNotificationsEnabled) { + return !pushNotificationSettings.disabledPushNotificationTypes.includes( + pushNotificationType, + ) + } + + return false +} diff --git a/src/domain/notifications/index.types.d.ts b/src/domain/notifications/index.types.d.ts index 640c97af2a..67fa474c9a 100644 --- a/src/domain/notifications/index.types.d.ts +++ b/src/domain/notifications/index.types.d.ts @@ -72,6 +72,9 @@ interface INotificationsService { adminPushNotificationSend( args: SendPushNotificationArgs, ): Promise + adminPushNotificationFilteredSend( + args: SendFilteredPushNotificationArgs, + ): Promise } type PushNotificationSettings = { diff --git a/src/services/notifications/create-push-notification-content.ts b/src/services/notifications/create-push-notification-content.ts index 99c8750158..2a05b68b00 100644 --- a/src/services/notifications/create-push-notification-content.ts +++ b/src/services/notifications/create-push-notification-content.ts @@ -3,6 +3,10 @@ import { getI18nInstance } from "@config" import { getCurrencyMajorExponent, MajorExponent } from "@domain/fiat" import { WalletCurrency } from "@domain/shared" import { getLanguageOrDefault } from "@domain/locale" +import { + GaloyPushNotifications, + mapNotificationTypeToPushNotificationType, +} from "@domain/notifications" const i18n = getI18nInstance() @@ -33,6 +37,7 @@ export const createPushNotificationContent = ({ }): { title: string body: string + pushNotificationType: PushNotificationType } => { const locale = getLanguageOrDefault(userLanguage) const baseCurrency = amount.currency @@ -87,5 +92,10 @@ export const createPushNotificationContent = ({ ) } - return { title, body } + const pushNotificationType = + type === "balance" + ? GaloyPushNotifications.Balance + : mapNotificationTypeToPushNotificationType(type) + + return { title, body, pushNotificationType } } diff --git a/src/services/notifications/index.ts b/src/services/notifications/index.ts index 25908018de..0a831b995e 100644 --- a/src/services/notifications/index.ts +++ b/src/services/notifications/index.ts @@ -340,6 +340,37 @@ export const NotificationsService = (): INotificationsService => { } } + const adminPushNotificationFilteredSend = async ({ + title, + body, + data, + deviceTokens, + pushNotificationSettings, + pushNotificationType, + }: SendFilteredPushNotificationArgs): Promise => { + const hasDeviceTokens = deviceTokens && deviceTokens.length > 0 + if (!hasDeviceTokens) return true + + try { + const result = await pushNotification.sendFilteredNotification({ + deviceTokens, + title, + body, + data, + pushNotificationSettings, + pushNotificationType, + }) + + if (result instanceof NotificationsServiceError) { + return result + } + + return true + } catch (err) { + return handleCommonNotificationErrors(err) + } + } + // trace everything except price update because it runs every 30 seconds return { priceUpdate, @@ -353,6 +384,7 @@ export const NotificationsService = (): INotificationsService => { onChainTxSent, sendBalance, adminPushNotificationSend, + adminPushNotificationFilteredSend, }, }), } diff --git a/src/services/notifications/push-notifications.ts b/src/services/notifications/push-notifications.ts index 436d1de5ae..e4a41a965f 100644 --- a/src/services/notifications/push-notifications.ts +++ b/src/services/notifications/push-notifications.ts @@ -3,8 +3,10 @@ import * as admin from "firebase-admin" import { DeviceTokensNotRegisteredNotificationsServiceError, InvalidDeviceNotificationsServiceError, + NotificationsServiceError, NotificationsServiceUnreachableServerError, UnknownNotificationsServiceError, + shouldSendPushNotification, } from "@domain/notifications" import { ErrorLevel, parseErrorMessageFromUnknown } from "@domain/shared" import { baseLogger } from "@services/logger" @@ -112,7 +114,43 @@ export const PushNotificationsService = (): IPushNotificationsService => { })() } - return { sendNotification } + const sendFilteredNotification = async (args: SendFilteredPushNotificationArgs) => { + const { + pushNotificationSettings, + pushNotificationType, + data, + ...sendNotificationArgs + } = args + + if ( + !shouldSendPushNotification({ + pushNotificationSettings, + pushNotificationType, + }) + ) { + return { + status: SendFilteredPushNotificationStatus.Filtered, + } + } + + const result = await sendNotification({ + ...sendNotificationArgs, + data: { + ...data, + PushNotificationType: pushNotificationType, + }, + }) + + if (result instanceof NotificationsServiceError) { + return result + } + + return { + status: SendFilteredPushNotificationStatus.Sent, + } + } + + return { sendNotification, sendFilteredNotification } } export const handleCommonNotificationErrors = (err: Error | string | unknown) => { @@ -134,3 +172,8 @@ export const KnownNotificationErrorMessages = { GoogleBadGatewayError: /Raw server response .* Error 502/, GoogleInternalServerError: /Raw server response .* Error 500/, } as const + +export const SendFilteredPushNotificationStatus = { + Sent: "Sent", + Filtered: "Filtered", +} diff --git a/src/services/notifications/push-notifications.types.d.ts b/src/services/notifications/push-notifications.types.d.ts index 84844bb07a..e3514a3716 100644 --- a/src/services/notifications/push-notifications.types.d.ts +++ b/src/services/notifications/push-notifications.types.d.ts @@ -5,6 +5,18 @@ type SendPushNotificationArgs = { data?: { [key: string]: string } } +type SendFilteredPushNotificationArgs = { + deviceTokens: DeviceToken[] + title: string + body: string + data?: { [key: string]: string } + pushNotificationSettings: PushNotificationSettings + pushNotificationType: PushNotificationType +} + +type SendFilteredPushNotificationStatus = + (typeof import("./push-notifications").SendFilteredPushNotificationStatus)[keyof typeof import("./push-notifications").SendFilteredPushNotificationStatus] + interface IPushNotificationsService { sendNotification({ deviceTokens, @@ -12,4 +24,11 @@ interface IPushNotificationsService { body, data, }: SendPushNotificationArgs): Promise + + sendFilteredNotification(args: SendFilteredPushNotificationArgs): Promise< + | { + status: SendFilteredPushNotificationStatus + } + | NotificationsServiceError + > } From 121dae76f36e62305460cd44bc582538d70d2336 Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Fri, 15 Sep 2023 14:02:20 -0500 Subject: [PATCH 05/12] chore: use filtered noitification send --- .../send-default-wallet-balance-to-users.ts | 1 + src/app/payments/send-intraledger.ts | 1 + src/app/payments/send-lightning.ts | 1 + .../add-pending-on-chain-transaction.ts | 1 + .../add-settled-on-chain-transaction.ts | 1 + src/app/wallets/send-on-chain.ts | 1 + src/app/wallets/settle-payout-txn.ts | 1 + src/app/wallets/update-pending-invoices.ts | 1 + src/domain/notifications/index.types.d.ts | 3 + src/services/notifications/index.ts | 59 +++++++++++-- .../add-pending-on-chain-transaction.spec.ts | 13 +-- .../add-settled-on-chain-transaction.spec.ts | 13 +-- .../app/wallets/send-intraledger.spec.ts | 13 +-- .../app/wallets/send-lightning.spec.ts | 13 +-- .../app/wallets/send-onchain.spec.ts | 13 +-- .../notifications/notification.spec.ts | 82 +++++++++++-------- 16 files changed, 152 insertions(+), 65 deletions(-) diff --git a/src/app/accounts/send-default-wallet-balance-to-users.ts b/src/app/accounts/send-default-wallet-balance-to-users.ts index 475289f150..3257ebc043 100644 --- a/src/app/accounts/send-default-wallet-balance-to-users.ts +++ b/src/app/accounts/send-default-wallet-balance-to-users.ts @@ -64,6 +64,7 @@ export const sendDefaultWalletBalanceToAccounts = async () => { deviceTokens: user.deviceTokens, displayBalanceAmount: displayAmount, recipientLanguage: user.language, + pushNotificationSettings: account.pushNotificationSettings, }) if (result instanceof DeviceTokensNotRegisteredNotificationsServiceError) { diff --git a/src/app/payments/send-intraledger.ts b/src/app/payments/send-intraledger.ts index fb11af5c4a..620598c0c4 100644 --- a/src/app/payments/send-intraledger.ts +++ b/src/app/payments/send-intraledger.ts @@ -365,6 +365,7 @@ const executePaymentViaIntraledger = async < recipientDeviceTokens: recipientUser.deviceTokens, recipientLanguage: recipientUser.language, paymentAmount: { amount, currency: recipientWallet.currency }, + recipientPushNotificationSettings: recipientAccount.pushNotificationSettings, displayPaymentAmount: recipientDisplayAmount, }) diff --git a/src/app/payments/send-lightning.ts b/src/app/payments/send-lightning.ts index 04ecb8a9b9..1e5755ce84 100644 --- a/src/app/payments/send-lightning.ts +++ b/src/app/payments/send-lightning.ts @@ -531,6 +531,7 @@ const executePaymentViaIntraledger = async < displayPaymentAmount: recipientDisplayAmount, paymentHash, recipientDeviceTokens: recipientUser.deviceTokens, + recipientPushNotificationSettings: recipientAccount.pushNotificationSettings, recipientLanguage: recipientUser.language, }) diff --git a/src/app/wallets/add-pending-on-chain-transaction.ts b/src/app/wallets/add-pending-on-chain-transaction.ts index fed58a42e1..5c54f94176 100644 --- a/src/app/wallets/add-pending-on-chain-transaction.ts +++ b/src/app/wallets/add-pending-on-chain-transaction.ts @@ -152,6 +152,7 @@ export const addPendingTransaction = async ({ displayPaymentAmount: settlementDisplayAmount, txHash: txId, recipientDeviceTokens: recipientUser.deviceTokens, + recipientPushNotificationSettings: account.pushNotificationSettings, recipientLanguage: recipientUser.language, }) diff --git a/src/app/wallets/add-settled-on-chain-transaction.ts b/src/app/wallets/add-settled-on-chain-transaction.ts index 4070db638f..3945455c01 100644 --- a/src/app/wallets/add-settled-on-chain-transaction.ts +++ b/src/app/wallets/add-settled-on-chain-transaction.ts @@ -179,6 +179,7 @@ const addSettledTransactionBeforeFinally = async ({ displayPaymentAmount: displayAmount, txHash, recipientDeviceTokens: user.deviceTokens, + recipientPushNotificationSettings: account.pushNotificationSettings, recipientLanguage: user.language, }) diff --git a/src/app/wallets/send-on-chain.ts b/src/app/wallets/send-on-chain.ts index d2505f5eb8..8f51376dc4 100644 --- a/src/app/wallets/send-on-chain.ts +++ b/src/app/wallets/send-on-chain.ts @@ -430,6 +430,7 @@ const executePaymentViaIntraledger = async < paymentAmount: { amount, currency: recipientWalletCurrency }, displayPaymentAmount: recipientDisplayAmount, recipientDeviceTokens: recipientUser.deviceTokens, + recipientPushNotificationSettings: recipientAccount.pushNotificationSettings, recipientLanguage: recipientUser.language, }) diff --git a/src/app/wallets/settle-payout-txn.ts b/src/app/wallets/settle-payout-txn.ts index 8140406662..cff43088f1 100644 --- a/src/app/wallets/settle-payout-txn.ts +++ b/src/app/wallets/settle-payout-txn.ts @@ -59,6 +59,7 @@ export const settlePayout = async ( displayPaymentAmount, txHash, senderDeviceTokens: user.deviceTokens, + senderPushNotificationSettings: account.pushNotificationSettings, senderLanguage: user.language, }) diff --git a/src/app/wallets/update-pending-invoices.ts b/src/app/wallets/update-pending-invoices.ts index 1984c5c601..b0aa80a254 100644 --- a/src/app/wallets/update-pending-invoices.ts +++ b/src/app/wallets/update-pending-invoices.ts @@ -309,6 +309,7 @@ const updatePendingInvoiceBeforeFinally = async ({ displayPaymentAmount, paymentHash, recipientDeviceTokens: recipientUser.deviceTokens, + recipientPushNotificationSettings: recipientAccount.pushNotificationSettings, recipientLanguage: recipientUser.language, }) diff --git a/src/domain/notifications/index.types.d.ts b/src/domain/notifications/index.types.d.ts index 67fa474c9a..83dd2885cc 100644 --- a/src/domain/notifications/index.types.d.ts +++ b/src/domain/notifications/index.types.d.ts @@ -13,6 +13,7 @@ type TransactionReceivedNotificationBaseArgs = TransactionNotificationBaseArgs & recipientAccountId: AccountId recipientWalletId: WalletId recipientDeviceTokens: DeviceToken[] + recipientPushNotificationSettings: PushNotificationSettings recipientLanguage: UserLanguageOrEmpty } @@ -20,6 +21,7 @@ type TransactionSentNotificationBaseArgs = TransactionNotificationBaseArgs & { senderAccountId: AccountId senderWalletId: WalletId senderDeviceTokens: DeviceToken[] + senderPushNotificationSettings: PushNotificationSettings senderLanguage: UserLanguageOrEmpty } @@ -41,6 +43,7 @@ type OnChainTxSentArgs = TransactionSentNotificationBaseArgs & OnChainTxBaseArgs type SendBalanceArgs = { balanceAmount: BalanceAmount deviceTokens: DeviceToken[] + pushNotificationSettings: PushNotificationSettings displayBalanceAmount?: DisplayAmount recipientLanguage: UserLanguageOrEmpty } diff --git a/src/services/notifications/index.ts b/src/services/notifications/index.ts index 0a831b995e..92306a0f50 100644 --- a/src/services/notifications/index.ts +++ b/src/services/notifications/index.ts @@ -24,6 +24,7 @@ export const NotificationsService = (): INotificationsService => { displayPaymentAmount, paymentHash, recipientDeviceTokens, + recipientPushNotificationSettings, recipientLanguage, }: LightningTxReceivedArgs): Promise => { try { @@ -54,18 +55,26 @@ export const NotificationsService = (): INotificationsService => { }) if (recipientDeviceTokens && recipientDeviceTokens.length > 0) { - const { title, body } = createPushNotificationContent({ + const { title, body, pushNotificationType } = createPushNotificationContent({ type: NotificationType.LnInvoicePaid, userLanguage: recipientLanguage, amount: paymentAmount, displayAmount: displayPaymentAmount, }) - return pushNotification.sendNotification({ + const result = await pushNotification.sendFilteredNotification({ deviceTokens: recipientDeviceTokens, title, body, + pushNotificationType, + pushNotificationSettings: recipientPushNotificationSettings, }) + + if (result instanceof NotificationsServiceError) { + return result + } + + return true } return true @@ -80,6 +89,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, recipientDeviceTokens, + recipientPushNotificationSettings, recipientLanguage, }: IntraLedgerTxReceivedArgs): Promise => { try { @@ -114,18 +124,26 @@ export const NotificationsService = (): INotificationsService => { }) if (recipientDeviceTokens && recipientDeviceTokens.length > 0) { - const { title, body } = createPushNotificationContent({ + const { title, body, pushNotificationType } = createPushNotificationContent({ type: NotificationType.IntraLedgerReceipt, userLanguage: recipientLanguage, amount: paymentAmount, displayAmount: displayPaymentAmount, }) - return pushNotification.sendNotification({ + const result = await pushNotification.sendFilteredNotification({ deviceTokens: recipientDeviceTokens, title, body, + pushNotificationType, + pushNotificationSettings: recipientPushNotificationSettings, }) + + if (result instanceof NotificationsServiceError) { + return result + } + + return true } return true @@ -141,6 +159,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, deviceTokens, + pushNotificationSettings, language, txHash, }: { @@ -150,6 +169,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount: PaymentAmount displayPaymentAmount?: DisplayAmount deviceTokens: DeviceToken[] + pushNotificationSettings: PushNotificationSettings language: UserLanguageOrEmpty txHash: OnChainTxHash }): Promise => { @@ -180,18 +200,26 @@ export const NotificationsService = (): INotificationsService => { }) if (deviceTokens.length > 0) { - const { title, body } = createPushNotificationContent({ + const { title, body, pushNotificationType } = createPushNotificationContent({ type, userLanguage: language, amount: paymentAmount, displayAmount: displayPaymentAmount, }) - return pushNotification.sendNotification({ + const result = await pushNotification.sendFilteredNotification({ deviceTokens, title, body, + pushNotificationType, + pushNotificationSettings, }) + + if (result instanceof NotificationsServiceError) { + return result + } + + return true } return true @@ -206,6 +234,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, recipientDeviceTokens, + recipientPushNotificationSettings, recipientLanguage, txHash, }: OnChainTxReceivedArgs) => @@ -216,6 +245,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, deviceTokens: recipientDeviceTokens, + pushNotificationSettings: recipientPushNotificationSettings, language: recipientLanguage, txHash, }) @@ -226,6 +256,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, recipientDeviceTokens, + recipientPushNotificationSettings, recipientLanguage, txHash, }: OnChainTxReceivedPendingArgs) => @@ -236,6 +267,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, deviceTokens: recipientDeviceTokens, + pushNotificationSettings: recipientPushNotificationSettings, language: recipientLanguage, txHash, }) @@ -246,6 +278,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, senderDeviceTokens, + senderPushNotificationSettings, senderLanguage, txHash, }: OnChainTxSentArgs) => @@ -256,6 +289,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, deviceTokens: senderDeviceTokens, + pushNotificationSettings: senderPushNotificationSettings, language: senderLanguage, txHash, }) @@ -295,6 +329,7 @@ export const NotificationsService = (): INotificationsService => { const sendBalance = async ({ balanceAmount, deviceTokens, + pushNotificationSettings, displayBalanceAmount, recipientLanguage, }: SendBalanceArgs): Promise => { @@ -302,18 +337,26 @@ export const NotificationsService = (): INotificationsService => { if (!hasDeviceTokens) return true try { - const { title, body } = createPushNotificationContent({ + const { title, body, pushNotificationType } = createPushNotificationContent({ type: "balance", userLanguage: recipientLanguage, amount: balanceAmount, displayAmount: displayBalanceAmount, }) - return pushNotification.sendNotification({ + const result = await pushNotification.sendFilteredNotification({ deviceTokens, title, body, + pushNotificationType, + pushNotificationSettings, }) + + if (result instanceof NotificationsServiceError) { + return result + } + + return true } catch (err) { return handleCommonNotificationErrors(err) } diff --git a/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts b/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts index 3a6c770cd9..bdef931412 100644 --- a/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts +++ b/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts @@ -22,12 +22,15 @@ afterEach(async () => { }) describe("addPendingTransaction", () => { - it("calls sendNotification on pending onchain receive", async () => { + it("calls sendFilteredNotification on pending onchain receive", async () => { // Setup mocks - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() const pushNotificationsServiceSpy = jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") - .mockImplementationOnce(() => ({ sendNotification })) + .mockImplementationOnce(() => ({ + sendFilteredNotification, + sendNotification: jest.fn(), + })) // Create user const { btcWalletDescriptor } = await createRandomUserAndWallets() @@ -47,8 +50,8 @@ describe("addPendingTransaction", () => { }) // Expect sent notification - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBeTruthy() + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBeTruthy() // Restore system state pushNotificationsServiceSpy.mockRestore() diff --git a/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts b/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts index 69e1268cbe..b8636a21ab 100644 --- a/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts +++ b/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts @@ -28,12 +28,15 @@ afterEach(async () => { }) describe("addSettledTransaction", () => { - it("calls sendNotification on successful onchain receive", async () => { + it("calls sendFilteredNotification on successful onchain receive", async () => { // Setup mocks - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() const pushNotificationsServiceSpy = jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") - .mockImplementationOnce(() => ({ sendNotification })) + .mockImplementationOnce(() => ({ + sendFilteredNotification, + sendNotification: jest.fn(), + })) // Create user const { btcWalletDescriptor } = await createRandomUserAndWallets() @@ -53,8 +56,8 @@ describe("addSettledTransaction", () => { }) // Expect sent notification - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBeTruthy() + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBeTruthy() // Restore system state pushNotificationsServiceSpy.mockRestore() diff --git a/test/integration/app/wallets/send-intraledger.spec.ts b/test/integration/app/wallets/send-intraledger.spec.ts index 6862eaa9a9..f6bea1bd0f 100644 --- a/test/integration/app/wallets/send-intraledger.spec.ts +++ b/test/integration/app/wallets/send-intraledger.spec.ts @@ -256,12 +256,15 @@ describe("intraLedgerPay", () => { expect(paymentResult).toBeInstanceOf(IntraledgerLimitsExceededError) }) - it("calls sendNotification on successful intraledger send", async () => { + it("calls sendFilteredNotification on successful intraledger send", async () => { // Setup mocks - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() const pushNotificationsServiceSpy = jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") - .mockImplementationOnce(() => ({ sendNotification })) + .mockImplementationOnce(() => ({ + sendFilteredNotification, + sendNotification: jest.fn(), + })) // Create users const { btcWalletDescriptor: newWalletDescriptor, usdWalletDescriptor } = @@ -290,8 +293,8 @@ describe("intraLedgerPay", () => { expect(paymentResult).toEqual(PaymentSendStatus.Success) // Expect sent notification - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBeTruthy() + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBeTruthy() // Restore system state pushNotificationsServiceSpy.mockReset() diff --git a/test/integration/app/wallets/send-lightning.spec.ts b/test/integration/app/wallets/send-lightning.spec.ts index ee61da9547..6be024fa1c 100644 --- a/test/integration/app/wallets/send-lightning.spec.ts +++ b/test/integration/app/wallets/send-lightning.spec.ts @@ -745,12 +745,15 @@ describe("initiated via lightning", () => { lndServiceSpy.mockReset() }) - it("calls sendNotification on successful intraledger send", async () => { + it("calls sendFilteredNotification on successful intraledger send", async () => { // Setup mocks - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() const pushNotificationsServiceSpy = jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") - .mockImplementationOnce(() => ({ sendNotification })) + .mockImplementationOnce(() => ({ + sendFilteredNotification, + sendNotification: jest.fn(), + })) const { LndService: LnServiceOrig } = jest.requireActual("@services/lnd") const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ @@ -799,8 +802,8 @@ describe("initiated via lightning", () => { expect(paymentResult).toEqual(PaymentSendStatus.Success) // Expect sent notification - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBeTruthy() + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBeTruthy() // Restore system state pushNotificationsServiceSpy.mockRestore() diff --git a/test/integration/app/wallets/send-onchain.spec.ts b/test/integration/app/wallets/send-onchain.spec.ts index 998bfc3ffa..18075f3faf 100644 --- a/test/integration/app/wallets/send-onchain.spec.ts +++ b/test/integration/app/wallets/send-onchain.spec.ts @@ -551,12 +551,15 @@ describe("onChainPay", () => { expect(res).toBeInstanceOf(InactiveAccountError) }) - it("calls sendNotification on successful intraledger receive", async () => { + it("calls sendFilteredNotification on successful intraledger receive", async () => { // Setup mocks - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() const pushNotificationsServiceSpy = jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") - .mockImplementationOnce(() => ({ sendNotification })) + .mockImplementationOnce(() => ({ + sendFilteredNotification, + sendNotification: jest.fn(), + })) // Create users const { btcWalletDescriptor: newWalletDescriptor, usdWalletDescriptor } = @@ -595,8 +598,8 @@ describe("onChainPay", () => { expect(paymentResult.status).toEqual(PaymentSendStatus.Success) // Expect sent notification - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBeTruthy() + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBeTruthy() // Restore system state pushNotificationsServiceSpy.mockRestore() diff --git a/test/legacy-integration/notifications/notification.spec.ts b/test/legacy-integration/notifications/notification.spec.ts index 149140fd6f..4d306c4d45 100644 --- a/test/legacy-integration/notifications/notification.spec.ts +++ b/test/legacy-integration/notifications/notification.spec.ts @@ -43,6 +43,11 @@ const crcDisplayPaymentAmount = { displayInMajor: "3500.50", } +const unfilteredPushNotificationSettings: PushNotificationSettings = { + pushNotificationsEnabled: true, + disabledPushNotificationTypes: [], +} + beforeAll(async () => { const usdDisplayPriceRatio = await getCurrentPriceAsDisplayPriceRatio({ currency: UsdDisplayCurrency, @@ -90,16 +95,19 @@ async function toArray(gen: AsyncIterable): Promise { } describe("notification", () => { - describe("sendNotification", () => { + describe("sendFilteredNotification", () => { // FIXME // 1/ we don't use this code in production any more // 2/ this is a very convoluted test that relies on other tests as an artefact. // It's hard to debug. it's probably something we'll want to refactor with more cleaner/independant integration tests. it.skip("sends daily balance to active users", async () => { - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") - .mockImplementation(() => ({ sendNotification })) + .mockImplementation(() => ({ + sendFilteredNotification, + sendNotification: jest.fn(), + })) await sendDefaultWalletBalanceToAccounts() const activeAccounts = getRecentlyActiveAccounts() @@ -108,7 +116,7 @@ describe("notification", () => { const activeAccountsArray = await toArray(activeAccounts) expect(activeAccountsArray.length).toBeGreaterThan(0) - expect(sendNotification.mock.calls.length).toBeGreaterThan(0) + expect(sendFilteredNotification.mock.calls.length).toBeGreaterThan(0) let usersWithDeviceTokens = 0 for (const { kratosUserId } of activeAccountsArray) { @@ -118,10 +126,10 @@ describe("notification", () => { if (user.deviceTokens.length > 0) usersWithDeviceTokens++ } - expect(sendNotification.mock.calls.length).toBe(usersWithDeviceTokens) + expect(sendFilteredNotification.mock.calls.length).toBe(usersWithDeviceTokens) - for (let i = 0; i < sendNotification.mock.calls.length; i++) { - const [call] = sendNotification.mock.calls[i] + for (let i = 0; i < sendFilteredNotification.mock.calls.length; i++) { + const [call] = sendFilteredNotification.mock.calls[i] const { defaultWalletId, kratosUserId } = activeAccountsArray[i] const user = await UsersRepository().findById(kratosUserId) @@ -196,11 +204,12 @@ describe("notification", () => { ] tests.forEach(({ name, paymentAmount, title, body }) => it(`${name}`, async () => { - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") .mockImplementationOnce(() => ({ - sendNotification, + sendFilteredNotification, + sendNotification: jest.fn(), })) await NotificationsService().lightningTxReceived({ @@ -211,12 +220,13 @@ describe("notification", () => { displayPaymentAmount: crcDisplayPaymentAmount, paymentHash, recipientDeviceTokens: deviceTokens, + recipientPushNotificationSettings: unfilteredPushNotificationSettings, recipientLanguage: language, }) - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBe(title) - expect(sendNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) + expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) }), ) }) @@ -239,11 +249,12 @@ describe("notification", () => { tests.forEach(({ name, paymentAmount, title, body }) => it(`${name}`, async () => { - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") .mockImplementationOnce(() => ({ - sendNotification, + sendFilteredNotification, + sendNotification: jest.fn(), })) await NotificationsService().intraLedgerTxReceived({ @@ -253,12 +264,13 @@ describe("notification", () => { recipientWalletId: walletId, displayPaymentAmount: crcDisplayPaymentAmount, recipientDeviceTokens: deviceTokens, + recipientPushNotificationSettings: unfilteredPushNotificationSettings, recipientLanguage: language, }) - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBe(title) - expect(sendNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) + expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) }), ) }) @@ -281,11 +293,12 @@ describe("notification", () => { tests.forEach(({ name, paymentAmount, title, body }) => it(`${name}`, async () => { - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") .mockImplementationOnce(() => ({ - sendNotification, + sendFilteredNotification, + sendNotification: jest.fn(), })) await NotificationsService().onChainTxReceived({ @@ -296,12 +309,13 @@ describe("notification", () => { displayPaymentAmount: crcDisplayPaymentAmount, txHash, recipientDeviceTokens: deviceTokens, + recipientPushNotificationSettings: unfilteredPushNotificationSettings, recipientLanguage: language, }) - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBe(title) - expect(sendNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) + expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) }), ) }) @@ -324,11 +338,12 @@ describe("notification", () => { tests.forEach(({ name, paymentAmount, title, body }) => it(`${name}`, async () => { - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") .mockImplementationOnce(() => ({ - sendNotification, + sendFilteredNotification, + sendNotification: jest.fn(), })) await NotificationsService().onChainTxReceivedPending({ @@ -338,12 +353,13 @@ describe("notification", () => { txHash, displayPaymentAmount: crcDisplayPaymentAmount, recipientDeviceTokens: deviceTokens, + recipientPushNotificationSettings: unfilteredPushNotificationSettings, recipientLanguage: language, }) - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBe(title) - expect(sendNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) + expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) }), ) }) @@ -366,11 +382,12 @@ describe("notification", () => { tests.forEach(({ name, paymentAmount, title, body }) => it(`${name}`, async () => { - const sendNotification = jest.fn() + const sendFilteredNotification = jest.fn() jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") .mockImplementationOnce(() => ({ - sendNotification, + sendFilteredNotification, + sendNotification: jest.fn(), })) await NotificationsService().onChainTxSent({ @@ -380,12 +397,13 @@ describe("notification", () => { txHash, displayPaymentAmount: crcDisplayPaymentAmount, senderDeviceTokens: deviceTokens, + senderPushNotificationSettings: unfilteredPushNotificationSettings, senderLanguage: language, }) - expect(sendNotification.mock.calls.length).toBe(1) - expect(sendNotification.mock.calls[0][0].title).toBe(title) - expect(sendNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls.length).toBe(1) + expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) + expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) }), ) }) From 9c849e0142016f772dfa17d76dafcaf1eb64a7c8 Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Fri, 15 Sep 2023 14:37:43 -0500 Subject: [PATCH 06/12] feat: add push notification type to admin api --- src/app/admin/send-admin-push-notification.ts | 13 ++++++++++--- src/domain/notifications/index.ts | 3 ++- .../root/mutation/admin-push-notification-send.ts | 14 ++++++++++---- src/graphql/admin/schema.graphql | 9 ++++++--- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/app/admin/send-admin-push-notification.ts b/src/app/admin/send-admin-push-notification.ts index 01ee7d511e..cbd4c98748 100644 --- a/src/app/admin/send-admin-push-notification.ts +++ b/src/app/admin/send-admin-push-notification.ts @@ -1,4 +1,5 @@ import { checkedToAccountUuid } from "@domain/accounts" +import { GaloyPushNotifications, checkedToPushNotificationType } from "@domain/notifications" import { AccountsRepository } from "@services/mongoose/accounts" import { UsersRepository } from "@services/mongoose/users" import { NotificationsService } from "@services/notifications" @@ -8,12 +9,18 @@ export const sendAdminPushNotification = async ({ title, body, data, + pushNotificationType, }: { accountId: string title: string body: string data?: { [key: string]: string } + pushNotificationType?: string }): Promise => { + const checkedPushNotificationType = pushNotificationType ? checkedToPushNotificationType(pushNotificationType) : GaloyPushNotifications.AdminPushNotification + + if (checkedPushNotificationType instanceof Error) return checkedPushNotificationType + const accountId = checkedToAccountUuid(accountIdRaw) if (accountId instanceof Error) return accountId @@ -26,14 +33,14 @@ export const sendAdminPushNotification = async ({ const user = await usersRepo.findById(kratosUserId) if (user instanceof Error) return user - const success = await NotificationsService().adminPushNotificationSend({ + const success = await NotificationsService().adminPushNotificationFilteredSend({ deviceTokens: user.deviceTokens, title, body, data, + pushNotificationType: checkedPushNotificationType, + pushNotificationSettings: account.pushNotificationSettings, }) - if (success instanceof Error) return success - return success } diff --git a/src/domain/notifications/index.ts b/src/domain/notifications/index.ts index d0ea2c7083..13ee0a4df0 100644 --- a/src/domain/notifications/index.ts +++ b/src/domain/notifications/index.ts @@ -38,6 +38,7 @@ export const checkedToPushNotificationSettings = ({ export const GaloyPushNotifications = { Payments: "Payments" as PushNotificationType, Balance: "Balance" as PushNotificationType, + AdminPushNotification: "AdminPushNotification" as PushNotificationType, } as const export const mapNotificationTypeToPushNotificationType = ( @@ -54,7 +55,7 @@ export const mapNotificationTypeToPushNotificationType = ( } } -const checkedToPushNotificationType = ( +export const checkedToPushNotificationType = ( type: string, ): PushNotificationType | ValidationError => { // TODO: add validation diff --git a/src/graphql/admin/root/mutation/admin-push-notification-send.ts b/src/graphql/admin/root/mutation/admin-push-notification-send.ts index 2dceb47dbc..45c4d92af0 100644 --- a/src/graphql/admin/root/mutation/admin-push-notification-send.ts +++ b/src/graphql/admin/root/mutation/admin-push-notification-send.ts @@ -3,22 +3,26 @@ import { GT } from "@graphql/index" import AdminPushNotificationSendPayload from "@graphql/admin/types/payload/admin-push-notification-send" import { Admin } from "@app" import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" +import PushNotificationType from "@graphql/shared/types/scalar/push-notification-type" const AdminPushNotificationSendInput = GT.Input({ name: "AdminPushNotificationSendInput", fields: () => ({ accountId: { - type: GT.String, + type: GT.NonNull(GT.String), }, title: { - type: GT.String, + type: GT.NonNull(GT.String), }, body: { - type: GT.String, + type: GT.NonNull(GT.String), }, data: { type: GT.Scalar(Object), }, + pushNotificationType: { + type: PushNotificationType + } }), }) @@ -31,6 +35,7 @@ const AdminPushNotificationSendMutation = GT.Field< title: string body: string data?: { [key: string]: string } + pushNotificationType?: string } } >({ @@ -42,13 +47,14 @@ const AdminPushNotificationSendMutation = GT.Field< input: { type: GT.NonNull(AdminPushNotificationSendInput) }, }, resolve: async (_, args) => { - const { accountId, body, title, data } = args.input + const { accountId, body, title, data, pushNotificationType } = args.input const success = await Admin.sendAdminPushNotification({ accountId, title, body, data, + pushNotificationType }) if (success instanceof Error) { diff --git a/src/graphql/admin/schema.graphql b/src/graphql/admin/schema.graphql index 0d50f3750f..689277df45 100644 --- a/src/graphql/admin/schema.graphql +++ b/src/graphql/admin/schema.graphql @@ -29,10 +29,11 @@ input AccountUpdateStatusInput { } input AdminPushNotificationSendInput { - accountId: String - body: String + accountId: String! + body: String! data: Object - title: String + pushNotificationType: PushNotificationType + title: String! } type AdminPushNotificationSendPayload { @@ -303,6 +304,8 @@ type PriceOfOneSettlementMinorUnitInDisplayMinorUnit implements PriceInterface { offset: Int! } +scalar PushNotificationType + type Query { accountDetailsByAccountId(accountId: ID!): AuditedAccount! accountDetailsByEmail(email: EmailAddress!): AuditedAccount! From 078b648215c28f218012951111c867c410243acc Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Fri, 15 Sep 2023 15:30:06 -0500 Subject: [PATCH 07/12] test: add notification filtering tests --- src/app/admin/send-admin-push-notification.ts | 9 +++- src/domain/notifications/index.ts | 16 ------ .../mutation/admin-push-notification-send.ts | 6 +-- .../push-notification-filtering.ts | 15 ++++++ .../notifications/push-notifications.ts | 5 +- .../notifications/notification.spec.ts | 16 ++++++ .../notifications/push-notification.spec.ts | 26 +++++++++ .../push-notification-filtering.spec.ts | 53 +++++++++++++++++++ 8 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 src/services/notifications/push-notification-filtering.ts create mode 100644 test/legacy-integration/notifications/push-notification.spec.ts create mode 100644 test/unit/services/notifications/push-notification-filtering.spec.ts diff --git a/src/app/admin/send-admin-push-notification.ts b/src/app/admin/send-admin-push-notification.ts index cbd4c98748..3ee36eddcd 100644 --- a/src/app/admin/send-admin-push-notification.ts +++ b/src/app/admin/send-admin-push-notification.ts @@ -1,5 +1,8 @@ import { checkedToAccountUuid } from "@domain/accounts" -import { GaloyPushNotifications, checkedToPushNotificationType } from "@domain/notifications" +import { + GaloyPushNotifications, + checkedToPushNotificationType, +} from "@domain/notifications" import { AccountsRepository } from "@services/mongoose/accounts" import { UsersRepository } from "@services/mongoose/users" import { NotificationsService } from "@services/notifications" @@ -17,7 +20,9 @@ export const sendAdminPushNotification = async ({ data?: { [key: string]: string } pushNotificationType?: string }): Promise => { - const checkedPushNotificationType = pushNotificationType ? checkedToPushNotificationType(pushNotificationType) : GaloyPushNotifications.AdminPushNotification + const checkedPushNotificationType = pushNotificationType + ? checkedToPushNotificationType(pushNotificationType) + : GaloyPushNotifications.AdminPushNotification if (checkedPushNotificationType instanceof Error) return checkedPushNotificationType diff --git a/src/domain/notifications/index.ts b/src/domain/notifications/index.ts index 13ee0a4df0..efefe18fea 100644 --- a/src/domain/notifications/index.ts +++ b/src/domain/notifications/index.ts @@ -65,19 +65,3 @@ export const checkedToPushNotificationType = ( return type as PushNotificationType } - -export const shouldSendPushNotification = ({ - pushNotificationSettings, - pushNotificationType, -}: { - pushNotificationSettings: PushNotificationSettings - pushNotificationType: PushNotificationType -}): boolean => { - if (pushNotificationSettings.pushNotificationsEnabled) { - return !pushNotificationSettings.disabledPushNotificationTypes.includes( - pushNotificationType, - ) - } - - return false -} diff --git a/src/graphql/admin/root/mutation/admin-push-notification-send.ts b/src/graphql/admin/root/mutation/admin-push-notification-send.ts index 45c4d92af0..a4d530dabc 100644 --- a/src/graphql/admin/root/mutation/admin-push-notification-send.ts +++ b/src/graphql/admin/root/mutation/admin-push-notification-send.ts @@ -21,8 +21,8 @@ const AdminPushNotificationSendInput = GT.Input({ type: GT.Scalar(Object), }, pushNotificationType: { - type: PushNotificationType - } + type: PushNotificationType, + }, }), }) @@ -54,7 +54,7 @@ const AdminPushNotificationSendMutation = GT.Field< title, body, data, - pushNotificationType + pushNotificationType, }) if (success instanceof Error) { diff --git a/src/services/notifications/push-notification-filtering.ts b/src/services/notifications/push-notification-filtering.ts new file mode 100644 index 0000000000..07057ef8bd --- /dev/null +++ b/src/services/notifications/push-notification-filtering.ts @@ -0,0 +1,15 @@ +export const shouldSendPushNotification = ({ + pushNotificationSettings, + pushNotificationType, +}: { + pushNotificationSettings: PushNotificationSettings + pushNotificationType: PushNotificationType +}): boolean => { + if (pushNotificationSettings.pushNotificationsEnabled) { + return !pushNotificationSettings.disabledPushNotificationTypes.includes( + pushNotificationType, + ) + } + + return false +} diff --git a/src/services/notifications/push-notifications.ts b/src/services/notifications/push-notifications.ts index e4a41a965f..71d3f0669d 100644 --- a/src/services/notifications/push-notifications.ts +++ b/src/services/notifications/push-notifications.ts @@ -6,7 +6,6 @@ import { NotificationsServiceError, NotificationsServiceUnreachableServerError, UnknownNotificationsServiceError, - shouldSendPushNotification, } from "@domain/notifications" import { ErrorLevel, parseErrorMessageFromUnknown } from "@domain/shared" import { baseLogger } from "@services/logger" @@ -19,6 +18,8 @@ import { Messaging } from "firebase-admin/lib/messaging/messaging" import { GOOGLE_APPLICATION_CREDENTIALS } from "@config" +import { shouldSendPushNotification } from "./push-notification-filtering" + const logger = baseLogger.child({ module: "notifications" }) type MessagingPayload = admin.messaging.MessagingPayload @@ -176,4 +177,4 @@ export const KnownNotificationErrorMessages = { export const SendFilteredPushNotificationStatus = { Sent: "Sent", Filtered: "Filtered", -} +} as const diff --git a/test/legacy-integration/notifications/notification.spec.ts b/test/legacy-integration/notifications/notification.spec.ts index 4d306c4d45..c03d239718 100644 --- a/test/legacy-integration/notifications/notification.spec.ts +++ b/test/legacy-integration/notifications/notification.spec.ts @@ -18,6 +18,7 @@ import { getCurrentPriceAsDisplayPriceRatio, } from "@app/prices" import { WalletCurrency } from "@domain/shared" +import { GaloyPushNotifications } from "@domain/notifications" let spy let displayPriceRatios: Record> @@ -227,6 +228,9 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + GaloyPushNotifications.Payments, + ) }), ) }) @@ -271,6 +275,9 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + GaloyPushNotifications.Payments, + ) }), ) }) @@ -316,6 +323,9 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + GaloyPushNotifications.Payments, + ) }), ) }) @@ -360,6 +370,9 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + GaloyPushNotifications.Payments, + ) }), ) }) @@ -404,6 +417,9 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) + expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + GaloyPushNotifications.Payments, + ) }), ) }) diff --git a/test/legacy-integration/notifications/push-notification.spec.ts b/test/legacy-integration/notifications/push-notification.spec.ts new file mode 100644 index 0000000000..36c51b8a9e --- /dev/null +++ b/test/legacy-integration/notifications/push-notification.spec.ts @@ -0,0 +1,26 @@ +import { + PushNotificationsService, + SendFilteredPushNotificationStatus, +} from "@services/notifications/push-notifications" + +describe("push notification", () => { + it("should filter a notification", async () => { + const pushNotificationType = "transaction" as PushNotificationType + const pushNotificationSettings = { + pushNotificationsEnabled: true, + disabledPushNotificationTypes: [pushNotificationType], + } + + const result = await PushNotificationsService().sendFilteredNotification({ + body: "body", + title: "title", + deviceTokens: ["deviceToken" as DeviceToken], + pushNotificationType, + pushNotificationSettings, + }) + + expect(result).toBe({ + status: SendFilteredPushNotificationStatus.Filtered, + }) + }) +}) diff --git a/test/unit/services/notifications/push-notification-filtering.spec.ts b/test/unit/services/notifications/push-notification-filtering.spec.ts new file mode 100644 index 0000000000..d0d31468da --- /dev/null +++ b/test/unit/services/notifications/push-notification-filtering.spec.ts @@ -0,0 +1,53 @@ +import { shouldSendPushNotification } from "@services/notifications/push-notification-filtering" + +describe("Notifications - push notification filtering", () => { + describe("shouldSendPushNotification", () => { + it("returns false when push notifications are disabled", () => { + const pushNotificationSettings = { + pushNotificationsEnabled: false, + disabledPushNotificationTypes: [], + } + + const pushNotificationType = "transaction" as PushNotificationType + + expect( + shouldSendPushNotification({ + pushNotificationSettings, + pushNotificationType, + }), + ).toBe(false) + }) + }) + + it("returns true when a notification is not disabled", () => { + const pushNotificationSettings = { + pushNotificationsEnabled: true, + disabledPushNotificationTypes: [], + } + + const pushNotificationType = "transaction" as PushNotificationType + + expect( + shouldSendPushNotification({ + pushNotificationSettings, + pushNotificationType, + }), + ).toBe(true) + }) + + it("returns false when a notification is disabled", () => { + const pushNotificationSettings = { + pushNotificationsEnabled: true, + disabledPushNotificationTypes: ["transaction" as PushNotificationType], + } + + const pushNotificationType = "transaction" as PushNotificationType + + expect( + shouldSendPushNotification({ + pushNotificationSettings, + pushNotificationType, + }), + ).toBe(false) + }) +}) From 6de76cba429e8e7369095c0ac9e65be5b71ef35d Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Fri, 15 Sep 2023 15:54:02 -0500 Subject: [PATCH 08/12] chore: address pr feedback --- src/graphql/public/types/object/push-notification-settings.ts | 2 +- test/bats/admin.bats | 2 +- test/legacy-integration/notifications/push-notification.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/graphql/public/types/object/push-notification-settings.ts b/src/graphql/public/types/object/push-notification-settings.ts index c5777ad22c..2603756a8f 100644 --- a/src/graphql/public/types/object/push-notification-settings.ts +++ b/src/graphql/public/types/object/push-notification-settings.ts @@ -12,7 +12,7 @@ export const PushNotificationSettings = GT.Object< resolve: (source) => source.pushNotificationsEnabled, }, disabledPushNotificationTypes: { - type: GT.NonNull(GT.List(PushNotificationType)), + type: GT.NonNull(GT.NonNullList(PushNotificationType)), resolve: (source) => source.disabledPushNotificationTypes, }, }), diff --git a/test/bats/admin.bats b/test/bats/admin.bats index b141ef1d99..8b6792ce4f 100644 --- a/test/bats/admin.bats +++ b/test/bats/admin.bats @@ -121,4 +121,4 @@ TESTER_PHONE="+19876543210" # TODO: add check by email # TODO: business update map info -} \ No newline at end of file +} diff --git a/test/legacy-integration/notifications/push-notification.spec.ts b/test/legacy-integration/notifications/push-notification.spec.ts index 36c51b8a9e..dd8ba46f43 100644 --- a/test/legacy-integration/notifications/push-notification.spec.ts +++ b/test/legacy-integration/notifications/push-notification.spec.ts @@ -19,7 +19,7 @@ describe("push notification", () => { pushNotificationSettings, }) - expect(result).toBe({ + expect(result).toEqual({ status: SendFilteredPushNotificationStatus.Filtered, }) }) From 54d097e9e088ca639b98ed9a1872e5df9b4cba1c Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Mon, 18 Sep 2023 15:15:21 -0500 Subject: [PATCH 09/12] chore: refactor interface --- dev/apollo-federation/supergraph.graphql | 76 ++++++--- ...sable-notification-category-for-channel.ts | 31 ++++ .../accounts/disable-notification-channel.ts | 23 +++ ...nable-notification-category-for-channel.ts | 31 ++++ .../accounts/enable-notification-channel.ts | 23 +++ src/app/accounts/index.ts | 5 +- .../send-default-wallet-balance-to-users.ts | 2 +- .../update-push-notification-settings.ts | 28 ---- src/app/admin/send-admin-push-notification.ts | 20 +-- src/app/payments/send-intraledger.ts | 2 +- src/app/payments/send-lightning.ts | 2 +- .../add-pending-on-chain-transaction.ts | 2 +- .../add-settled-on-chain-transaction.ts | 2 +- src/app/wallets/send-on-chain.ts | 2 +- src/app/wallets/settle-payout-txn.ts | 2 +- src/app/wallets/update-pending-invoices.ts | 2 +- src/domain/accounts/index.types.d.ts | 2 +- src/domain/notifications/index.ts | 141 ++++++++++++----- src/domain/notifications/index.types.d.ts | 17 +- src/domain/primitives/index.types.d.ts | 2 +- .../mutation/admin-push-notification-send.ts | 12 +- src/graphql/admin/schema.graphql | 6 +- src/graphql/public/mutations.ts | 14 +- ...sable-notification-category-for-channel.ts | 60 +++++++ .../account-disable-notification-channel.ts | 54 +++++++ ...nable-notification-category-for-channel.ts | 60 +++++++ .../account-enable-notification-channel.ts | 54 +++++++ ...count-update-push-notification-settings.ts | 56 ------- src/graphql/public/schema.graphql | 56 +++++-- src/graphql/public/types/abstract/account.ts | 6 +- .../public/types/object/business-account.ts | 8 +- .../public/types/object/consumer-account.ts | 8 +- .../object/notification-channel-settings.ts | 19 +++ .../types/object/notification-settings.ts | 16 ++ .../object/push-notification-settings.ts | 19 --- ...> account-update-notification-settings.ts} | 6 +- .../types/scalar/notification-category.ts | 28 ++++ .../types/scalar/notification-channel.ts | 11 ++ .../types/scalar/push-notification-type.ts | 28 ---- src/services/mongoose/accounts.ts | 16 +- src/services/mongoose/schema.ts | 21 +-- src/services/mongoose/schema.types.d.ts | 2 +- .../create-push-notification-content.ts | 12 +- src/services/notifications/index.ts | 68 ++++---- .../push-notification-filtering.ts | 15 -- .../notifications/push-notifications.ts | 21 ++- .../push-notifications.types.d.ts | 4 +- .../notifications/notification.spec.ts | 30 ++-- .../notifications/push-notification.spec.ts | 14 +- test/unit/domain/notifications/index.spec.ts | 149 ++++++++++++++++++ .../wallets/payment-input-validator.spec.ts | 8 +- .../push-notification-filtering.spec.ts | 53 ------- 52 files changed, 926 insertions(+), 423 deletions(-) create mode 100644 src/app/accounts/disable-notification-category-for-channel.ts create mode 100644 src/app/accounts/disable-notification-channel.ts create mode 100644 src/app/accounts/enable-notification-category-for-channel.ts create mode 100644 src/app/accounts/enable-notification-channel.ts delete mode 100644 src/app/accounts/update-push-notification-settings.ts create mode 100644 src/graphql/public/root/mutation/account-disable-notification-category-for-channel.ts create mode 100644 src/graphql/public/root/mutation/account-disable-notification-channel.ts create mode 100644 src/graphql/public/root/mutation/account-enable-notification-category-for-channel.ts create mode 100644 src/graphql/public/root/mutation/account-enable-notification-channel.ts delete mode 100644 src/graphql/public/root/mutation/account-update-push-notification-settings.ts create mode 100644 src/graphql/public/types/object/notification-channel-settings.ts create mode 100644 src/graphql/public/types/object/notification-settings.ts delete mode 100644 src/graphql/public/types/object/push-notification-settings.ts rename src/graphql/public/types/payload/{account-update-push-notification-settings.ts => account-update-notification-settings.ts} (61%) create mode 100644 src/graphql/shared/types/scalar/notification-category.ts create mode 100644 src/graphql/shared/types/scalar/notification-channel.ts delete mode 100644 src/graphql/shared/types/scalar/push-notification-type.ts delete mode 100644 src/services/notifications/push-notification-filtering.ts create mode 100644 test/unit/domain/notifications/index.spec.ts delete mode 100644 test/unit/services/notifications/push-notification-filtering.spec.ts diff --git a/dev/apollo-federation/supergraph.graphql b/dev/apollo-federation/supergraph.graphql index 577f5d3936..24459835a0 100644 --- a/dev/apollo-federation/supergraph.graphql +++ b/dev/apollo-federation/supergraph.graphql @@ -31,7 +31,7 @@ interface Account id: ID! level: AccountLevel! limits: AccountLimits! - pushNotificationSettings: PushNotificationSettings! + notificationSettings: NotificationSettings! realtimePrice: RealtimePrice! transactions( """Returns the items in the list that come after the specified cursor.""" @@ -57,6 +57,32 @@ type AccountDeletePayload success: Boolean! } +input AccountDisableNotificationCategoryForChannelInput + @join__type(graph: PUBLIC) +{ + category: NotificationCategory! + channel: NotificationChannel! +} + +input AccountDisableNotificationChannelInput + @join__type(graph: PUBLIC) +{ + channel: NotificationChannel! +} + +input AccountEnableNotificationCategoryForChannelInput + @join__type(graph: PUBLIC) +{ + category: NotificationCategory! + channel: NotificationChannel! +} + +input AccountEnableNotificationChannelInput + @join__type(graph: PUBLIC) +{ + channel: NotificationChannel! +} + enum AccountLevel @join__type(graph: PUBLIC) { @@ -121,14 +147,7 @@ type AccountUpdateDisplayCurrencyPayload errors: [Error!]! } -input AccountUpdatePushNotificationSettingsInput - @join__type(graph: PUBLIC) -{ - disabledPushNotificationTypes: [PushNotificationType]! - pushNotificationsEnabled: Boolean! -} - -type AccountUpdatePushNotificationSettingsPayload +type AccountUpdateNotificationSettingsPayload @join__type(graph: PUBLIC) { account: ConsumerAccount @@ -282,7 +301,7 @@ type ConsumerAccount implements Account id: ID! level: AccountLevel! limits: AccountLimits! - pushNotificationSettings: PushNotificationSettings! + notificationSettings: NotificationSettings! """List the quiz questions of the consumer account""" quiz: [Quiz!]! @@ -832,9 +851,12 @@ type Mutation @join__type(graph: PUBLIC) { accountDelete: AccountDeletePayload! + accountDisableNotificationCategoryForChannel(input: AccountDisableNotificationCategoryForChannelInput!): AccountUpdateNotificationSettingsPayload! + accountDisableNotificationChannel(input: AccountDisableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! + accountEnableNotificationCategoryForChannel(input: AccountEnableNotificationCategoryForChannelInput!): AccountUpdateNotificationSettingsPayload! + accountEnableNotificationChannel(input: AccountEnableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! accountUpdateDefaultWalletId(input: AccountUpdateDefaultWalletIdInput!): AccountUpdateDefaultWalletIdPayload! accountUpdateDisplayCurrency(input: AccountUpdateDisplayCurrencyInput!): AccountUpdateDisplayCurrencyPayload! - accountUpdatePushNotificationSettings(input: AccountUpdatePushNotificationSettingsInput!): AccountUpdatePushNotificationSettingsPayload! callbackEndpointAdd(input: CallbackEndpointAddInput!): CallbackEndpointAddPayload! callbackEndpointDelete(input: CallbackEndpointDeleteInput!): SuccessPayload! captchaCreateChallenge: CaptchaCreateChallengePayload! @@ -966,6 +988,28 @@ enum Network testnet @join__enumValue(graph: PUBLIC) } +scalar NotificationCategory + @join__type(graph: PUBLIC) + +enum NotificationChannel + @join__type(graph: PUBLIC) +{ + PUSH @join__enumValue(graph: PUBLIC) +} + +type NotificationChannelSettings + @join__type(graph: PUBLIC) +{ + disabledCategories: [NotificationCategory!]! + enabled: Boolean! +} + +type NotificationSettings + @join__type(graph: PUBLIC) +{ + push: NotificationChannelSettings! +} + """An address for an on-chain bitcoin destination""" scalar OnChainAddress @join__type(graph: PUBLIC) @@ -1231,16 +1275,6 @@ type PublicWallet walletCurrency: WalletCurrency! } -type PushNotificationSettings - @join__type(graph: PUBLIC) -{ - disabledPushNotificationTypes: [PushNotificationType]! - pushNotificationsEnabled: Boolean! -} - -scalar PushNotificationType - @join__type(graph: PUBLIC) - type Query @join__type(graph: PUBLIC) { diff --git a/src/app/accounts/disable-notification-category-for-channel.ts b/src/app/accounts/disable-notification-category-for-channel.ts new file mode 100644 index 0000000000..ddbd2a3386 --- /dev/null +++ b/src/app/accounts/disable-notification-category-for-channel.ts @@ -0,0 +1,31 @@ +import { + checkedToNotificationCategory, + disableNotificationCategoryForChannel as disableNotificationCategory, +} from "@domain/notifications" +import { AccountsRepository } from "@services/mongoose" + +export const disableNotificationCategoryForChannel = async ({ + accountId, + notificationChannel, + notificationCategory, +}: { + accountId: AccountId + notificationChannel: NotificationChannel + notificationCategory: string +}): Promise => { + const checkedNotificationCategory = checkedToNotificationCategory(notificationCategory) + if (checkedNotificationCategory instanceof Error) return checkedNotificationCategory + + const account = await AccountsRepository().findById(accountId) + if (account instanceof Error) return account + + const newNotificationSettings = disableNotificationCategory({ + notificationSettings: account.notificationSettings, + notificationChannel, + notificationCategory: checkedNotificationCategory, + }) + + account.notificationSettings = newNotificationSettings + + return AccountsRepository().update(account) +} diff --git a/src/app/accounts/disable-notification-channel.ts b/src/app/accounts/disable-notification-channel.ts new file mode 100644 index 0000000000..ea7dfa9883 --- /dev/null +++ b/src/app/accounts/disable-notification-channel.ts @@ -0,0 +1,23 @@ +import { setNotificationChannelIsEnabled } from "@domain/notifications" +import { AccountsRepository } from "@services/mongoose" + +export const disableNotificationChannel = async ({ + accountId, + notificationChannel, +}: { + accountId: AccountId + notificationChannel: NotificationChannel +}): Promise => { + const account = await AccountsRepository().findById(accountId) + if (account instanceof Error) return account + + const newNotificationSettings = setNotificationChannelIsEnabled({ + notificationSettings: account.notificationSettings, + notificationChannel, + enabled: false, + }) + + account.notificationSettings = newNotificationSettings + + return AccountsRepository().update(account) +} diff --git a/src/app/accounts/enable-notification-category-for-channel.ts b/src/app/accounts/enable-notification-category-for-channel.ts new file mode 100644 index 0000000000..90aac3a1e9 --- /dev/null +++ b/src/app/accounts/enable-notification-category-for-channel.ts @@ -0,0 +1,31 @@ +import { + checkedToNotificationCategory, + enableNotificationCategoryForChannel as enableNotificationChannel, +} from "@domain/notifications" +import { AccountsRepository } from "@services/mongoose" + +export const enableNotificationCategoryForChannel = async ({ + accountId, + notificationChannel, + notificationCategory, +}: { + accountId: AccountId + notificationChannel: NotificationChannel + notificationCategory: string +}): Promise => { + const checkedNotificationCategory = checkedToNotificationCategory(notificationCategory) + if (checkedNotificationCategory instanceof Error) return checkedNotificationCategory + + const account = await AccountsRepository().findById(accountId) + if (account instanceof Error) return account + + const newNotificationSettings = enableNotificationChannel({ + notificationSettings: account.notificationSettings, + notificationChannel, + notificationCategory: checkedNotificationCategory, + }) + + account.notificationSettings = newNotificationSettings + + return AccountsRepository().update(account) +} diff --git a/src/app/accounts/enable-notification-channel.ts b/src/app/accounts/enable-notification-channel.ts new file mode 100644 index 0000000000..761fd421e2 --- /dev/null +++ b/src/app/accounts/enable-notification-channel.ts @@ -0,0 +1,23 @@ +import { setNotificationChannelIsEnabled } from "@domain/notifications" +import { AccountsRepository } from "@services/mongoose" + +export const enableNotificationChannel = async ({ + accountId, + notificationChannel, +}: { + accountId: AccountId + notificationChannel: NotificationChannel +}): Promise => { + const account = await AccountsRepository().findById(accountId) + if (account instanceof Error) return account + + const newNotificationSettings = setNotificationChannelIsEnabled({ + notificationSettings: account.notificationSettings, + notificationChannel, + enabled: true, + }) + + account.notificationSettings = newNotificationSettings + + return AccountsRepository().update(account) +} diff --git a/src/app/accounts/index.ts b/src/app/accounts/index.ts index 60192e4401..acbf3d7f18 100644 --- a/src/app/accounts/index.ts +++ b/src/app/accounts/index.ts @@ -21,7 +21,10 @@ export * from "./update-display-currency" export * from "./username-available" export * from "./delete-business-map-info" export * from "./upgrade-device-account" -export * from "./update-push-notification-settings" +export * from "./disable-notification-category-for-channel" +export * from "./enable-notification-category-for-channel" +export * from "./enable-notification-channel" +export * from "./disable-notification-channel" const accounts = AccountsRepository() diff --git a/src/app/accounts/send-default-wallet-balance-to-users.ts b/src/app/accounts/send-default-wallet-balance-to-users.ts index 3257ebc043..9a74f279ac 100644 --- a/src/app/accounts/send-default-wallet-balance-to-users.ts +++ b/src/app/accounts/send-default-wallet-balance-to-users.ts @@ -64,7 +64,7 @@ export const sendDefaultWalletBalanceToAccounts = async () => { deviceTokens: user.deviceTokens, displayBalanceAmount: displayAmount, recipientLanguage: user.language, - pushNotificationSettings: account.pushNotificationSettings, + notificationSettings: account.notificationSettings, }) if (result instanceof DeviceTokensNotRegisteredNotificationsServiceError) { diff --git a/src/app/accounts/update-push-notification-settings.ts b/src/app/accounts/update-push-notification-settings.ts deleted file mode 100644 index cc5e5d0dd0..0000000000 --- a/src/app/accounts/update-push-notification-settings.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { checkedToPushNotificationSettings } from "@domain/notifications" - -import { AccountsRepository } from "@services/mongoose" - -export const updatePushNotificationSettings = async ({ - accountId, - pushNotificationsEnabled, - disabledPushNotificationTypes, -}: { - accountId: AccountId - pushNotificationsEnabled: boolean - disabledPushNotificationTypes: string[] -}): Promise => { - const checkedPushNotificationSettings = checkedToPushNotificationSettings({ - disabledPushNotificationTypes, - pushNotificationsEnabled, - }) - - if (checkedPushNotificationSettings instanceof Error) - return checkedPushNotificationSettings - - const account = await AccountsRepository().findById(accountId) - if (account instanceof Error) return account - - account.pushNotificationSettings = checkedPushNotificationSettings - - return AccountsRepository().update(account) -} diff --git a/src/app/admin/send-admin-push-notification.ts b/src/app/admin/send-admin-push-notification.ts index 3ee36eddcd..5694214423 100644 --- a/src/app/admin/send-admin-push-notification.ts +++ b/src/app/admin/send-admin-push-notification.ts @@ -1,7 +1,7 @@ import { checkedToAccountUuid } from "@domain/accounts" import { - GaloyPushNotifications, - checkedToPushNotificationType, + GaloyNotificationCategories, + checkedToNotificationCategory, } from "@domain/notifications" import { AccountsRepository } from "@services/mongoose/accounts" import { UsersRepository } from "@services/mongoose/users" @@ -12,19 +12,19 @@ export const sendAdminPushNotification = async ({ title, body, data, - pushNotificationType, + notificationCategory, }: { accountId: string title: string body: string data?: { [key: string]: string } - pushNotificationType?: string + notificationCategory?: string }): Promise => { - const checkedPushNotificationType = pushNotificationType - ? checkedToPushNotificationType(pushNotificationType) - : GaloyPushNotifications.AdminPushNotification + const checkedNotificationCategory = notificationCategory + ? checkedToNotificationCategory(notificationCategory) + : GaloyNotificationCategories.AdminPushNotification - if (checkedPushNotificationType instanceof Error) return checkedPushNotificationType + if (checkedNotificationCategory instanceof Error) return checkedNotificationCategory const accountId = checkedToAccountUuid(accountIdRaw) if (accountId instanceof Error) return accountId @@ -43,8 +43,8 @@ export const sendAdminPushNotification = async ({ title, body, data, - pushNotificationType: checkedPushNotificationType, - pushNotificationSettings: account.pushNotificationSettings, + notificationCategory: checkedNotificationCategory, + notificationSettings: account.notificationSettings, }) return success diff --git a/src/app/payments/send-intraledger.ts b/src/app/payments/send-intraledger.ts index 620598c0c4..c78ff87009 100644 --- a/src/app/payments/send-intraledger.ts +++ b/src/app/payments/send-intraledger.ts @@ -365,7 +365,7 @@ const executePaymentViaIntraledger = async < recipientDeviceTokens: recipientUser.deviceTokens, recipientLanguage: recipientUser.language, paymentAmount: { amount, currency: recipientWallet.currency }, - recipientPushNotificationSettings: recipientAccount.pushNotificationSettings, + recipientNotificationSettings: recipientAccount.notificationSettings, displayPaymentAmount: recipientDisplayAmount, }) diff --git a/src/app/payments/send-lightning.ts b/src/app/payments/send-lightning.ts index 1e5755ce84..9369d04f2f 100644 --- a/src/app/payments/send-lightning.ts +++ b/src/app/payments/send-lightning.ts @@ -531,7 +531,7 @@ const executePaymentViaIntraledger = async < displayPaymentAmount: recipientDisplayAmount, paymentHash, recipientDeviceTokens: recipientUser.deviceTokens, - recipientPushNotificationSettings: recipientAccount.pushNotificationSettings, + recipientNotificationSettings: recipientAccount.notificationSettings, recipientLanguage: recipientUser.language, }) diff --git a/src/app/wallets/add-pending-on-chain-transaction.ts b/src/app/wallets/add-pending-on-chain-transaction.ts index 5c54f94176..53f5ed07cf 100644 --- a/src/app/wallets/add-pending-on-chain-transaction.ts +++ b/src/app/wallets/add-pending-on-chain-transaction.ts @@ -152,7 +152,7 @@ export const addPendingTransaction = async ({ displayPaymentAmount: settlementDisplayAmount, txHash: txId, recipientDeviceTokens: recipientUser.deviceTokens, - recipientPushNotificationSettings: account.pushNotificationSettings, + recipientNotificationSettings: account.notificationSettings, recipientLanguage: recipientUser.language, }) diff --git a/src/app/wallets/add-settled-on-chain-transaction.ts b/src/app/wallets/add-settled-on-chain-transaction.ts index 3945455c01..3c049f67a9 100644 --- a/src/app/wallets/add-settled-on-chain-transaction.ts +++ b/src/app/wallets/add-settled-on-chain-transaction.ts @@ -179,7 +179,7 @@ const addSettledTransactionBeforeFinally = async ({ displayPaymentAmount: displayAmount, txHash, recipientDeviceTokens: user.deviceTokens, - recipientPushNotificationSettings: account.pushNotificationSettings, + recipientNotificationSettings: account.notificationSettings, recipientLanguage: user.language, }) diff --git a/src/app/wallets/send-on-chain.ts b/src/app/wallets/send-on-chain.ts index 8f51376dc4..9ef46b62d1 100644 --- a/src/app/wallets/send-on-chain.ts +++ b/src/app/wallets/send-on-chain.ts @@ -430,7 +430,7 @@ const executePaymentViaIntraledger = async < paymentAmount: { amount, currency: recipientWalletCurrency }, displayPaymentAmount: recipientDisplayAmount, recipientDeviceTokens: recipientUser.deviceTokens, - recipientPushNotificationSettings: recipientAccount.pushNotificationSettings, + recipientNotificationSettings: recipientAccount.notificationSettings, recipientLanguage: recipientUser.language, }) diff --git a/src/app/wallets/settle-payout-txn.ts b/src/app/wallets/settle-payout-txn.ts index cff43088f1..2d5c7a6d65 100644 --- a/src/app/wallets/settle-payout-txn.ts +++ b/src/app/wallets/settle-payout-txn.ts @@ -59,7 +59,7 @@ export const settlePayout = async ( displayPaymentAmount, txHash, senderDeviceTokens: user.deviceTokens, - senderPushNotificationSettings: account.pushNotificationSettings, + senderNotificationSettings: account.notificationSettings, senderLanguage: user.language, }) diff --git a/src/app/wallets/update-pending-invoices.ts b/src/app/wallets/update-pending-invoices.ts index b0aa80a254..5645bdc081 100644 --- a/src/app/wallets/update-pending-invoices.ts +++ b/src/app/wallets/update-pending-invoices.ts @@ -309,7 +309,7 @@ const updatePendingInvoiceBeforeFinally = async ({ displayPaymentAmount, paymentHash, recipientDeviceTokens: recipientUser.deviceTokens, - recipientPushNotificationSettings: recipientAccount.pushNotificationSettings, + recipientNotificationSettings: recipientAccount.notificationSettings, recipientLanguage: recipientUser.language, }) diff --git a/src/domain/accounts/index.types.d.ts b/src/domain/accounts/index.types.d.ts index b85b5117cd..c7ea17b3c3 100644 --- a/src/domain/accounts/index.types.d.ts +++ b/src/domain/accounts/index.types.d.ts @@ -77,7 +77,7 @@ type Account = { readonly isEditor: boolean readonly quizQuestions: UserQuizQuestion[] // deprecated readonly quiz: Quiz[] - pushNotificationSettings: PushNotificationSettings // QUESTION: should this be here or should the notification service have its own storage of notification settings? + notificationSettings: NotificationSettings kratosUserId: UserId displayCurrency: DisplayCurrency // temp diff --git a/src/domain/notifications/index.ts b/src/domain/notifications/index.ts index efefe18fea..1e165c4612 100644 --- a/src/domain/notifications/index.ts +++ b/src/domain/notifications/index.ts @@ -1,4 +1,4 @@ -import { InvalidPushNotificationSettingError } from "./errors" +import { InvalidPushNotificationSettingError as InvalidNotificationSettingsError } from "./errors" export * from "./errors" @@ -11,57 +11,116 @@ export const NotificationType = { LnInvoicePaid: "paid-invoice", } as const -export const checkedToPushNotificationSettings = ({ - disabledPushNotificationTypes, - pushNotificationsEnabled, +export const NotificationChannel = { + Push: "push", +} as const + +export const GaloyNotificationCategories = { + Payments: "Payments" as NotificationCategory, + Balance: "Balance" as NotificationCategory, + AdminPushNotification: "AdminPushNotification" as NotificationCategory, +} as const + +export const checkedToNotificationCategory = ( + type: string, +): NotificationCategory | ValidationError => { + // TODO: add validation + if (!type) { + return new InvalidNotificationSettingsError("Invalid notification category") + } + + return type as NotificationCategory +} + +export const setNotificationChannelIsEnabled = ({ + notificationSettings, + notificationChannel, + enabled, }: { - disabledPushNotificationTypes: string[] - pushNotificationsEnabled: boolean -}): PushNotificationSettings | InvalidPushNotificationSettingError => { - const checkedDisabledPushNotificationTypes: PushNotificationType[] = [] - - for (const pushNotification of disabledPushNotificationTypes) { - const checkedPushNotification = checkedToPushNotificationType(pushNotification) - if (checkedPushNotification instanceof Error) { - return checkedPushNotification - } else { - checkedDisabledPushNotificationTypes.push(checkedPushNotification) - } + notificationSettings: NotificationSettings + notificationChannel: NotificationChannel + enabled: boolean +}): NotificationSettings => { + const notificationChannelSettings = notificationSettings[notificationChannel] + const enabledChanged = notificationChannelSettings.enabled !== enabled + + const newNotificationSettings = { + enabled, + disabledCategories: enabledChanged + ? [] + : notificationChannelSettings.disabledCategories, } return { - pushNotificationsEnabled, - disabledPushNotificationTypes: checkedDisabledPushNotificationTypes, + ...notificationSettings, + [notificationChannel]: newNotificationSettings, } } -export const GaloyPushNotifications = { - Payments: "Payments" as PushNotificationType, - Balance: "Balance" as PushNotificationType, - AdminPushNotification: "AdminPushNotification" as PushNotificationType, -} as const +export const enableNotificationCategoryForChannel = ({ + notificationSettings, + notificationChannel, + notificationCategory, +}: { + notificationSettings: NotificationSettings + notificationChannel: NotificationChannel + notificationCategory: NotificationCategory +}): NotificationSettings => { + const notificationChannelSettings = notificationSettings[notificationChannel] + const disabledCategories = notificationChannelSettings.disabledCategories + + const newNotificationSettings = { + enabled: notificationChannelSettings.enabled, + disabledCategories: disabledCategories.filter( + (category) => category !== notificationCategory, + ), + } -export const mapNotificationTypeToPushNotificationType = ( - notificationType: NotificationType, -): PushNotificationType => { - switch (notificationType) { - case NotificationType.IntraLedgerReceipt: - case NotificationType.IntraLedgerPayment: - case NotificationType.OnchainReceipt: - case NotificationType.OnchainReceiptPending: - case NotificationType.LnInvoicePaid: - case NotificationType.OnchainPayment: - return GaloyPushNotifications.Payments + return { + ...notificationSettings, + [notificationChannel]: newNotificationSettings, } } -export const checkedToPushNotificationType = ( - type: string, -): PushNotificationType | ValidationError => { - // TODO: add validation - if (!type) { - return new InvalidPushNotificationSettingError("Invalid notification type") +export const disableNotificationCategoryForChannel = ({ + notificationSettings, + notificationChannel, + notificationCategory, +}: { + notificationSettings: NotificationSettings + notificationChannel: NotificationChannel + notificationCategory: NotificationCategory +}): NotificationSettings => { + const notificationChannelSettings = notificationSettings[notificationChannel] + const disabledCategories = notificationChannelSettings.disabledCategories + disabledCategories.push(notificationCategory) + const uniqueDisabledCategories = [...new Set(disabledCategories)] + + const newNotificationSettings = { + enabled: notificationChannelSettings.enabled, + disabledCategories: uniqueDisabledCategories, + } + + return { + ...notificationSettings, + [notificationChannel]: newNotificationSettings, + } +} + +export const shouldSendNotification = ({ + notificationChannel, + notificationSettings, + notificationCategory, +}: { + notificationChannel: NotificationChannel + notificationSettings: NotificationSettings + notificationCategory: NotificationCategory +}): boolean => { + const channelNotificationSettings = notificationSettings[notificationChannel] + + if (channelNotificationSettings.enabled) { + return !channelNotificationSettings.disabledCategories.includes(notificationCategory) } - return type as PushNotificationType + return false } diff --git a/src/domain/notifications/index.types.d.ts b/src/domain/notifications/index.types.d.ts index 83dd2885cc..d66a246354 100644 --- a/src/domain/notifications/index.types.d.ts +++ b/src/domain/notifications/index.types.d.ts @@ -13,7 +13,7 @@ type TransactionReceivedNotificationBaseArgs = TransactionNotificationBaseArgs & recipientAccountId: AccountId recipientWalletId: WalletId recipientDeviceTokens: DeviceToken[] - recipientPushNotificationSettings: PushNotificationSettings + recipientNotificationSettings: NotificationSettings recipientLanguage: UserLanguageOrEmpty } @@ -21,7 +21,7 @@ type TransactionSentNotificationBaseArgs = TransactionNotificationBaseArgs & { senderAccountId: AccountId senderWalletId: WalletId senderDeviceTokens: DeviceToken[] - senderPushNotificationSettings: PushNotificationSettings + senderNotificationSettings: NotificationSettings senderLanguage: UserLanguageOrEmpty } @@ -43,7 +43,7 @@ type OnChainTxSentArgs = TransactionSentNotificationBaseArgs & OnChainTxBaseArgs type SendBalanceArgs = { balanceAmount: BalanceAmount deviceTokens: DeviceToken[] - pushNotificationSettings: PushNotificationSettings + notificationSettings: NotificationSettings displayBalanceAmount?: DisplayAmount recipientLanguage: UserLanguageOrEmpty } @@ -80,7 +80,12 @@ interface INotificationsService { ): Promise } -type PushNotificationSettings = { - pushNotificationsEnabled: boolean - disabledPushNotificationTypes: PushNotificationType[] +type NotificationChannel = + (typeof import("./index").NotificationChannel)[keyof typeof import("./index").NotificationChannel] + +type NotificationSettings = Record + +type NotificationChannelSettings = { + enabled: boolean + disabledCategories: NotificationCategory[] } diff --git a/src/domain/primitives/index.types.d.ts b/src/domain/primitives/index.types.d.ts index 7bfa578b58..b6d59b19dc 100644 --- a/src/domain/primitives/index.types.d.ts +++ b/src/domain/primitives/index.types.d.ts @@ -11,7 +11,7 @@ type MilliSeconds = number & { readonly brand: unique symbol } type Days = number & { readonly brand: unique symbol } type JwtToken = string & { readonly brand: unique symbol } // short lived asymmetric token from oathkeeper type Memo = string & { readonly brand: unique symbol } -type PushNotificationType = string & { readonly brand: unique symbol } +type NotificationCategory = string & { readonly brand: unique symbol } type XOR = | (T1 & { [k in Exclude]?: never }) diff --git a/src/graphql/admin/root/mutation/admin-push-notification-send.ts b/src/graphql/admin/root/mutation/admin-push-notification-send.ts index a4d530dabc..41123f5bdc 100644 --- a/src/graphql/admin/root/mutation/admin-push-notification-send.ts +++ b/src/graphql/admin/root/mutation/admin-push-notification-send.ts @@ -3,7 +3,7 @@ import { GT } from "@graphql/index" import AdminPushNotificationSendPayload from "@graphql/admin/types/payload/admin-push-notification-send" import { Admin } from "@app" import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" -import PushNotificationType from "@graphql/shared/types/scalar/push-notification-type" +import NotificationCategory from "@graphql/shared/types/scalar/notification-category" const AdminPushNotificationSendInput = GT.Input({ name: "AdminPushNotificationSendInput", @@ -20,8 +20,8 @@ const AdminPushNotificationSendInput = GT.Input({ data: { type: GT.Scalar(Object), }, - pushNotificationType: { - type: PushNotificationType, + notificationCategory: { + type: NotificationCategory, }, }), }) @@ -35,7 +35,7 @@ const AdminPushNotificationSendMutation = GT.Field< title: string body: string data?: { [key: string]: string } - pushNotificationType?: string + notificationCategory?: string } } >({ @@ -47,14 +47,14 @@ const AdminPushNotificationSendMutation = GT.Field< input: { type: GT.NonNull(AdminPushNotificationSendInput) }, }, resolve: async (_, args) => { - const { accountId, body, title, data, pushNotificationType } = args.input + const { accountId, body, title, data, notificationCategory } = args.input const success = await Admin.sendAdminPushNotification({ accountId, title, body, data, - pushNotificationType, + notificationCategory, }) if (success instanceof Error) { diff --git a/src/graphql/admin/schema.graphql b/src/graphql/admin/schema.graphql index 689277df45..0998abf30a 100644 --- a/src/graphql/admin/schema.graphql +++ b/src/graphql/admin/schema.graphql @@ -32,7 +32,7 @@ input AdminPushNotificationSendInput { accountId: String! body: String! data: Object - pushNotificationType: PushNotificationType + notificationCategory: NotificationCategory title: String! } @@ -253,6 +253,8 @@ type Mutation { userUpdatePhone(input: UserUpdatePhoneInput!): AccountDetailPayload! } +scalar NotificationCategory + scalar Object """An address for an on-chain bitcoin destination""" @@ -304,8 +306,6 @@ type PriceOfOneSettlementMinorUnitInDisplayMinorUnit implements PriceInterface { offset: Int! } -scalar PushNotificationType - type Query { accountDetailsByAccountId(accountId: ID!): AuditedAccount! accountDetailsByEmail(email: EmailAddress!): AuditedAccount! diff --git a/src/graphql/public/mutations.ts b/src/graphql/public/mutations.ts index 9188aea593..35722fe505 100644 --- a/src/graphql/public/mutations.ts +++ b/src/graphql/public/mutations.ts @@ -49,7 +49,10 @@ import UserTotpDeleteMutation from "@graphql/public/root/mutation/user-totp-dele import CallbackEndpointAdd from "./root/mutation/callback-endpoint-add" import CallbackEndpointDelete from "./root/mutation/callback-endpoint-delete" -import AccountUpdatePushNotificationSettingsMutation from "./root/mutation/account-update-push-notification-settings" +import AccountEnableNotificationCategoryForChannelMutation from "./root/mutation/account-enable-notification-category-for-channel" +import AccountDisableNotificationCategoryForChannelMutation from "./root/mutation/account-disable-notification-category-for-channel" +import AccountEnableNotificationChannelMutation from "./root/mutation/account-enable-notification-channel" +import AccountDisableNotificationChannelMutation from "./root/mutation/account-disable-notification-channel" // TODO: // const fields: { [key: string]: GraphQLFieldConfig } export const mutationFields = { @@ -87,8 +90,13 @@ export const mutationFields = { userContactUpdateAlias: UserContactUpdateAliasMutation, accountUpdateDefaultWalletId: AccountUpdateDefaultWalletIdMutation, accountUpdateDisplayCurrency: AccountUpdateDisplayCurrencyMutation, - accountUpdatePushNotificationSettings: - AccountUpdatePushNotificationSettingsMutation, + accountEnableNotificationCategoryForChannel: + AccountEnableNotificationCategoryForChannelMutation, + accountDisableNotificationCategoryForChannel: + AccountDisableNotificationCategoryForChannelMutation, + accountEnableNotificationChannel: AccountEnableNotificationChannelMutation, + accountDisableNotificationChannel: AccountDisableNotificationChannelMutation, + accountDelete: AccountDeleteMutation, feedbackSubmit: FeedbackSubmitMutation, diff --git a/src/graphql/public/root/mutation/account-disable-notification-category-for-channel.ts b/src/graphql/public/root/mutation/account-disable-notification-category-for-channel.ts new file mode 100644 index 0000000000..4c0d82e3fa --- /dev/null +++ b/src/graphql/public/root/mutation/account-disable-notification-category-for-channel.ts @@ -0,0 +1,60 @@ +import { GT } from "@graphql/index" + +import AccountUpdateNotificationSettingsPayload from "@graphql/public/types/payload/account-update-notification-settings" +import { Accounts } from "@app/index" +import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" +import NotificationChannel from "@graphql/shared/types/scalar/notification-channel" +import NotificationCategory from "@graphql/shared/types/scalar/notification-category" + +const AccountDisableNotificationCategoryForChannelInput = GT.Input({ + name: "AccountDisableNotificationCategoryForChannelInput", + fields: () => ({ + channel: { + type: GT.NonNull(NotificationChannel), + }, + category: { + type: GT.NonNull(NotificationCategory), + }, + }), +}) + +const AccountDisableNotificationCategoryForChannelMutation = GT.Field< + null, + GraphQLPublicContextAuth, + { + input: { + channel: NotificationChannel | Error + category: NotificationCategory + } + } +>({ + extensions: { + complexity: 120, + }, + type: GT.NonNull(AccountUpdateNotificationSettingsPayload), + args: { + input: { type: GT.NonNull(AccountDisableNotificationCategoryForChannelInput) }, + }, + resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { + const { channel, category } = args.input + + if (channel instanceof Error) return { errors: [{ message: channel.message }] } + + const result = await Accounts.disableNotificationCategoryForChannel({ + accountId: domainAccount.id, + notificationChannel: channel, + notificationCategory: category, + }) + + if (result instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(result)] } + } + + return { + errors: [], + account: result, + } + }, +}) + +export default AccountDisableNotificationCategoryForChannelMutation diff --git a/src/graphql/public/root/mutation/account-disable-notification-channel.ts b/src/graphql/public/root/mutation/account-disable-notification-channel.ts new file mode 100644 index 0000000000..52fcbfc49e --- /dev/null +++ b/src/graphql/public/root/mutation/account-disable-notification-channel.ts @@ -0,0 +1,54 @@ +import { GT } from "@graphql/index" + +import AccountUpdateNotificationSettingsPayload from "@graphql/public/types/payload/account-update-notification-settings" +import { Accounts } from "@app/index" +import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" +import NotificationChannel from "@graphql/shared/types/scalar/notification-channel" + +const AccountDisableNotificationChannelInput = GT.Input({ + name: "AccountDisableNotificationChannelInput", + fields: () => ({ + channel: { + type: GT.NonNull(NotificationChannel), + }, + }), +}) + +const AccountDisableNotificationChannelMutation = GT.Field< + null, + GraphQLPublicContextAuth, + { + input: { + channel: NotificationChannel | Error + } + } +>({ + extensions: { + complexity: 120, + }, + type: GT.NonNull(AccountUpdateNotificationSettingsPayload), + args: { + input: { type: GT.NonNull(AccountDisableNotificationChannelInput) }, + }, + resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { + const { channel } = args.input + + if (channel instanceof Error) return { errors: [{ message: channel.message }] } + + const result = await Accounts.disableNotificationChannel({ + accountId: domainAccount.id, + notificationChannel: channel, + }) + + if (result instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(result)] } + } + + return { + errors: [], + account: result, + } + }, +}) + +export default AccountDisableNotificationChannelMutation diff --git a/src/graphql/public/root/mutation/account-enable-notification-category-for-channel.ts b/src/graphql/public/root/mutation/account-enable-notification-category-for-channel.ts new file mode 100644 index 0000000000..89c72b799a --- /dev/null +++ b/src/graphql/public/root/mutation/account-enable-notification-category-for-channel.ts @@ -0,0 +1,60 @@ +import { GT } from "@graphql/index" + +import AccountUpdateNotificationSettingsPayload from "@graphql/public/types/payload/account-update-notification-settings" +import { Accounts } from "@app/index" +import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" +import NotificationChannel from "@graphql/shared/types/scalar/notification-channel" +import NotificationCategory from "@graphql/shared/types/scalar/notification-category" + +const AccountEnableNotificationCategoryForChannelInput = GT.Input({ + name: "AccountEnableNotificationCategoryForChannelInput", + fields: () => ({ + channel: { + type: GT.NonNull(NotificationChannel), + }, + category: { + type: GT.NonNull(NotificationCategory), + }, + }), +}) + +const AccountEnableNotificationCategoryForChannelMutation = GT.Field< + null, + GraphQLPublicContextAuth, + { + input: { + channel: NotificationChannel | Error + category: NotificationCategory + } + } +>({ + extensions: { + complexity: 120, + }, + type: GT.NonNull(AccountUpdateNotificationSettingsPayload), + args: { + input: { type: GT.NonNull(AccountEnableNotificationCategoryForChannelInput) }, + }, + resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { + const { channel, category } = args.input + + if (channel instanceof Error) return { errors: [{ message: channel.message }] } + + const result = await Accounts.enableNotificationCategoryForChannel({ + accountId: domainAccount.id, + notificationChannel: channel, + notificationCategory: category, + }) + + if (result instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(result)] } + } + + return { + errors: [], + account: result, + } + }, +}) + +export default AccountEnableNotificationCategoryForChannelMutation diff --git a/src/graphql/public/root/mutation/account-enable-notification-channel.ts b/src/graphql/public/root/mutation/account-enable-notification-channel.ts new file mode 100644 index 0000000000..3102a2656d --- /dev/null +++ b/src/graphql/public/root/mutation/account-enable-notification-channel.ts @@ -0,0 +1,54 @@ +import { GT } from "@graphql/index" + +import AccountUpdateNotificationSettingsPayload from "@graphql/public/types/payload/account-update-notification-settings" +import { Accounts } from "@app/index" +import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" +import NotificationChannel from "@graphql/shared/types/scalar/notification-channel" + +const AccountEnableNotificationChannelInput = GT.Input({ + name: "AccountEnableNotificationChannelInput", + fields: () => ({ + channel: { + type: GT.NonNull(NotificationChannel), + }, + }), +}) + +const AccountEnableNotificationChannelMutation = GT.Field< + null, + GraphQLPublicContextAuth, + { + input: { + channel: NotificationChannel | Error + } + } +>({ + extensions: { + complexity: 120, + }, + type: GT.NonNull(AccountUpdateNotificationSettingsPayload), + args: { + input: { type: GT.NonNull(AccountEnableNotificationChannelInput) }, + }, + resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { + const { channel } = args.input + + if (channel instanceof Error) return { errors: [{ message: channel.message }] } + + const result = await Accounts.enableNotificationChannel({ + accountId: domainAccount.id, + notificationChannel: channel, + }) + + if (result instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(result)] } + } + + return { + errors: [], + account: result, + } + }, +}) + +export default AccountEnableNotificationChannelMutation diff --git a/src/graphql/public/root/mutation/account-update-push-notification-settings.ts b/src/graphql/public/root/mutation/account-update-push-notification-settings.ts deleted file mode 100644 index 9e10c4fa23..0000000000 --- a/src/graphql/public/root/mutation/account-update-push-notification-settings.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { GT } from "@graphql/index" - -import PushNotificationType from "@graphql/shared/types/scalar/push-notification-type" -import AccountUpdatePushNotificationSettingsPayload from "@graphql/public/types/payload/account-update-push-notification-settings" -import { Accounts } from "@app/index" -import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" - -const AccountUpdatePushNotificationSettingsInput = GT.Input({ - name: "AccountUpdatePushNotificationSettingsInput", - fields: () => ({ - pushNotificationsEnabled: { - type: GT.NonNull(GT.Boolean), - }, - disabledPushNotificationTypes: { - type: GT.NonNull(GT.List(PushNotificationType)), - }, - }), -}) - -const AccountUpdatePushNotificationSettingsMutation = GT.Field< - null, - GraphQLPublicContextAuth, - { - input: { - disabledPushNotificationTypes: string[] - pushNotificationsEnabled: boolean - } - } ->({ - extensions: { - complexity: 120, - }, - type: GT.NonNull(AccountUpdatePushNotificationSettingsPayload), - args: { - input: { type: GT.NonNull(AccountUpdatePushNotificationSettingsInput) }, - }, - resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { - const { disabledPushNotificationTypes, pushNotificationsEnabled } = args.input - const result = await Accounts.updatePushNotificationSettings({ - accountId: domainAccount.id, - disabledPushNotificationTypes, - pushNotificationsEnabled, - }) - - if (result instanceof Error) { - return { errors: [mapAndParseErrorForGqlResponse(result)] } - } - - return { - errors: [], - account: result, - } - }, -}) - -export default AccountUpdatePushNotificationSettingsMutation diff --git a/src/graphql/public/schema.graphql b/src/graphql/public/schema.graphql index 29ea30988d..17d9d3b8ab 100644 --- a/src/graphql/public/schema.graphql +++ b/src/graphql/public/schema.graphql @@ -6,7 +6,7 @@ interface Account { id: ID! level: AccountLevel! limits: AccountLimits! - pushNotificationSettings: PushNotificationSettings! + notificationSettings: NotificationSettings! realtimePrice: RealtimePrice! transactions( """Returns the items in the list that come after the specified cursor.""" @@ -30,6 +30,24 @@ type AccountDeletePayload { success: Boolean! } +input AccountDisableNotificationCategoryForChannelInput { + category: NotificationCategory! + channel: NotificationChannel! +} + +input AccountDisableNotificationChannelInput { + channel: NotificationChannel! +} + +input AccountEnableNotificationCategoryForChannelInput { + category: NotificationCategory! + channel: NotificationChannel! +} + +input AccountEnableNotificationChannelInput { + channel: NotificationChannel! +} + enum AccountLevel { ONE TWO @@ -80,12 +98,7 @@ type AccountUpdateDisplayCurrencyPayload { errors: [Error!]! } -input AccountUpdatePushNotificationSettingsInput { - disabledPushNotificationTypes: [PushNotificationType]! - pushNotificationsEnabled: Boolean! -} - -type AccountUpdatePushNotificationSettingsPayload { +type AccountUpdateNotificationSettingsPayload { account: ConsumerAccount errors: [Error!]! } @@ -209,7 +222,7 @@ type ConsumerAccount implements Account { id: ID! level: AccountLevel! limits: AccountLimits! - pushNotificationSettings: PushNotificationSettings! + notificationSettings: NotificationSettings! """List the quiz questions of the consumer account""" quiz: [Quiz!]! @@ -632,9 +645,12 @@ type MobileVersions { type Mutation { accountDelete: AccountDeletePayload! + accountDisableNotificationCategoryForChannel(input: AccountDisableNotificationCategoryForChannelInput!): AccountUpdateNotificationSettingsPayload! + accountDisableNotificationChannel(input: AccountDisableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! + accountEnableNotificationCategoryForChannel(input: AccountEnableNotificationCategoryForChannelInput!): AccountUpdateNotificationSettingsPayload! + accountEnableNotificationChannel(input: AccountEnableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! accountUpdateDefaultWalletId(input: AccountUpdateDefaultWalletIdInput!): AccountUpdateDefaultWalletIdPayload! accountUpdateDisplayCurrency(input: AccountUpdateDisplayCurrencyInput!): AccountUpdateDisplayCurrencyPayload! - accountUpdatePushNotificationSettings(input: AccountUpdatePushNotificationSettingsInput!): AccountUpdatePushNotificationSettingsPayload! callbackEndpointAdd(input: CallbackEndpointAddInput!): CallbackEndpointAddPayload! callbackEndpointDelete(input: CallbackEndpointDeleteInput!): SuccessPayload! captchaCreateChallenge: CaptchaCreateChallengePayload! @@ -762,6 +778,21 @@ enum Network { testnet } +scalar NotificationCategory + +enum NotificationChannel { + PUSH +} + +type NotificationChannelSettings { + disabledCategories: [NotificationCategory!]! + enabled: Boolean! +} + +type NotificationSettings { + push: NotificationChannelSettings! +} + """An address for an on-chain bitcoin destination""" scalar OnChainAddress @@ -966,13 +997,6 @@ type PublicWallet { walletCurrency: WalletCurrency! } -type PushNotificationSettings { - disabledPushNotificationTypes: [PushNotificationType]! - pushNotificationsEnabled: Boolean! -} - -scalar PushNotificationType - type Query { accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet! btcPrice(currency: DisplayCurrency! = "USD"): Price @deprecated(reason: "Deprecated in favor of realtimePrice") diff --git a/src/graphql/public/types/abstract/account.ts b/src/graphql/public/types/abstract/account.ts index 232fa9ea61..8b96aadd8f 100644 --- a/src/graphql/public/types/abstract/account.ts +++ b/src/graphql/public/types/abstract/account.ts @@ -11,7 +11,7 @@ import AccountLevel from "../../../shared/types/scalar/account-level" import Wallet from "../../../shared/types/abstract/wallet" import CallbackEndpoint from "../object/callback-endpoint" -import { PushNotificationSettings } from "../object/push-notification-settings" +import { NotificationSettings } from "../object/notification-settings" const IAccount = GT.Interface({ name: "Account", @@ -57,8 +57,8 @@ const IAccount = GT.Interface({ }, }, }, - pushNotificationSettings: { - type: GT.NonNull(PushNotificationSettings), + notificationSettings: { + type: GT.NonNull(NotificationSettings), }, // FUTURE-PLAN: Support a `users: [User!]!` field here diff --git a/src/graphql/public/types/object/business-account.ts b/src/graphql/public/types/object/business-account.ts index 1cdf714f07..934c4c52e7 100644 --- a/src/graphql/public/types/object/business-account.ts +++ b/src/graphql/public/types/object/business-account.ts @@ -28,7 +28,7 @@ import AccountLevel from "../../../shared/types/scalar/account-level" import { TransactionConnection } from "../../../shared/types/object/transaction" import RealtimePrice from "./realtime-price" -import { PushNotificationSettings } from "./push-notification-settings" +import { NotificationSettings } from "./notification-settings" const BusinessAccount = GT.Object({ name: "BusinessAccount", @@ -160,9 +160,9 @@ const BusinessAccount = GT.Object({ ) }, }, - pushNotificationSettings: { - type: GT.NonNull(PushNotificationSettings), - resolve: (source) => source.pushNotificationSettings, + notificationSettings: { + type: GT.NonNull(NotificationSettings), + resolve: (source) => source.notificationSettings, }, }), }) diff --git a/src/graphql/public/types/object/consumer-account.ts b/src/graphql/public/types/object/consumer-account.ts index 3d35976365..64acb10e1c 100644 --- a/src/graphql/public/types/object/consumer-account.ts +++ b/src/graphql/public/types/object/consumer-account.ts @@ -32,7 +32,7 @@ import { TransactionConnection } from "../../../shared/types/object/transaction" import AccountLimits from "./account-limits" import Quiz from "./quiz" import CallbackEndpoint from "./callback-endpoint" -import { PushNotificationSettings } from "./push-notification-settings" +import { NotificationSettings } from "./notification-settings" const ConsumerAccount = GT.Object({ name: "ConsumerAccount", @@ -184,9 +184,9 @@ const ConsumerAccount = GT.Object({ }, }, - pushNotificationSettings: { - type: GT.NonNull(PushNotificationSettings), - resolve: (source) => source.pushNotificationSettings, + notificationSettings: { + type: GT.NonNull(NotificationSettings), + resolve: (source) => source.notificationSettings, }, }), }) diff --git a/src/graphql/public/types/object/notification-channel-settings.ts b/src/graphql/public/types/object/notification-channel-settings.ts new file mode 100644 index 0000000000..09d965b5a1 --- /dev/null +++ b/src/graphql/public/types/object/notification-channel-settings.ts @@ -0,0 +1,19 @@ +import { GT } from "@graphql/index" +import NotificationCategory from "@graphql/shared/types/scalar/notification-category" + +export const NotificationChannelSettings = GT.Object< + NotificationChannelSettings, + GraphQLPublicContextAuth +>({ + name: "NotificationChannelSettings", + fields: () => ({ + enabled: { + type: GT.NonNull(GT.Boolean), + resolve: (source) => source.enabled, + }, + disabledCategories: { + type: GT.NonNullList(NotificationCategory), + resolve: (source) => source.disabledCategories, + }, + }), +}) diff --git a/src/graphql/public/types/object/notification-settings.ts b/src/graphql/public/types/object/notification-settings.ts new file mode 100644 index 0000000000..231238ba5b --- /dev/null +++ b/src/graphql/public/types/object/notification-settings.ts @@ -0,0 +1,16 @@ +import { GT } from "@graphql/index" + +import { NotificationChannelSettings } from "./notification-channel-settings" + +export const NotificationSettings = GT.Object< + NotificationSettings, + GraphQLPublicContextAuth +>({ + name: "NotificationSettings", + fields: () => ({ + push: { + type: GT.NonNull(NotificationChannelSettings), + resolve: (source) => source.push, + }, + }), +}) diff --git a/src/graphql/public/types/object/push-notification-settings.ts b/src/graphql/public/types/object/push-notification-settings.ts deleted file mode 100644 index 2603756a8f..0000000000 --- a/src/graphql/public/types/object/push-notification-settings.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GT } from "@graphql/index" -import PushNotificationType from "@graphql/shared/types/scalar/push-notification-type" - -export const PushNotificationSettings = GT.Object< - PushNotificationSettings, - GraphQLPublicContextAuth ->({ - name: "PushNotificationSettings", - fields: () => ({ - pushNotificationsEnabled: { - type: GT.NonNull(GT.Boolean), - resolve: (source) => source.pushNotificationsEnabled, - }, - disabledPushNotificationTypes: { - type: GT.NonNull(GT.NonNullList(PushNotificationType)), - resolve: (source) => source.disabledPushNotificationTypes, - }, - }), -}) diff --git a/src/graphql/public/types/payload/account-update-push-notification-settings.ts b/src/graphql/public/types/payload/account-update-notification-settings.ts similarity index 61% rename from src/graphql/public/types/payload/account-update-push-notification-settings.ts rename to src/graphql/public/types/payload/account-update-notification-settings.ts index 61bf2a84df..0ef91e3124 100644 --- a/src/graphql/public/types/payload/account-update-push-notification-settings.ts +++ b/src/graphql/public/types/payload/account-update-notification-settings.ts @@ -3,8 +3,8 @@ import { GT } from "@graphql/index" import IError from "../../../shared/types/abstract/error" import ConsumerAccount from "../object/consumer-account" -const AccountUpdatePushNotificationSettingsPayload = GT.Object({ - name: "AccountUpdatePushNotificationSettingsPayload", +const AccountUpdateNotificationSettingsPayload = GT.Object({ + name: "AccountUpdateNotificationSettingsPayload", fields: () => ({ errors: { type: GT.NonNullList(IError), @@ -15,4 +15,4 @@ const AccountUpdatePushNotificationSettingsPayload = GT.Object({ }), }) -export default AccountUpdatePushNotificationSettingsPayload +export default AccountUpdateNotificationSettingsPayload diff --git a/src/graphql/shared/types/scalar/notification-category.ts b/src/graphql/shared/types/scalar/notification-category.ts new file mode 100644 index 0000000000..27207eeccd --- /dev/null +++ b/src/graphql/shared/types/scalar/notification-category.ts @@ -0,0 +1,28 @@ +import { InputValidationError } from "@graphql/error" +import { GT } from "@graphql/index" + +const NotificationCategory = GT.Scalar({ + name: "NotificationCategory", + parseValue(value) { + if (typeof value !== "string") { + return new InputValidationError({ + message: "Invalid type for NotificationCategory", + }) + } + return validNotificationCategory(value) + }, + parseLiteral(ast) { + if (ast.kind === GT.Kind.STRING) { + return validNotificationCategory(ast.value) + } + return new InputValidationError({ message: "Invalid type for NotificationCategory" }) + }, +}) + +function validNotificationCategory( + value: string, +): NotificationCategory | InputValidationError { + return value as NotificationCategory +} + +export default NotificationCategory diff --git a/src/graphql/shared/types/scalar/notification-channel.ts b/src/graphql/shared/types/scalar/notification-channel.ts new file mode 100644 index 0000000000..8481ea5205 --- /dev/null +++ b/src/graphql/shared/types/scalar/notification-channel.ts @@ -0,0 +1,11 @@ +import { NotificationChannel as NotificationChannelDomain } from "@domain/notifications" +import { GT } from "@graphql/index" + +const NotificationChannel = GT.Enum({ + name: "NotificationChannel", + values: { + PUSH: { value: NotificationChannelDomain.Push }, + }, +}) + +export default NotificationChannel diff --git a/src/graphql/shared/types/scalar/push-notification-type.ts b/src/graphql/shared/types/scalar/push-notification-type.ts deleted file mode 100644 index 4731c2f84a..0000000000 --- a/src/graphql/shared/types/scalar/push-notification-type.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { InputValidationError } from "@graphql/error" -import { GT } from "@graphql/index" - -const PushNotificationType = GT.Scalar({ - name: "PushNotificationType", - parseValue(value) { - if (typeof value !== "string") { - return new InputValidationError({ - message: "Invalid type for PushNotificationType", - }) - } - return validPushNotificationType(value) - }, - parseLiteral(ast) { - if (ast.kind === GT.Kind.STRING) { - return validPushNotificationType(ast.value) - } - return new InputValidationError({ message: "Invalid type for PushNotificationType" }) - }, -}) - -function validPushNotificationType( - value: string, -): PushNotificationType | InputValidationError { - return value as PushNotificationType -} - -export default PushNotificationType diff --git a/src/services/mongoose/accounts.ts b/src/services/mongoose/accounts.ts index a1e01459e0..1d8cec8fa1 100644 --- a/src/services/mongoose/accounts.ts +++ b/src/services/mongoose/accounts.ts @@ -118,7 +118,7 @@ export const AccountsRepository = (): IAccountsRepository => { withdrawFee, kratosUserId, displayCurrency, - pushNotificationSettings, + notificationSettings, role, }: Account): Promise => { @@ -143,7 +143,7 @@ export const AccountsRepository = (): IAccountsRepository => { withdrawFee, kratosUserId, displayCurrency, - pushNotificationSettings, + notificationSettings, role, }, @@ -227,12 +227,14 @@ const translateToAccount = (result: AccountRecord): Account => ({ ), withdrawFee: result.withdrawFee as Satoshis, isEditor: result.role === "editor", - pushNotificationSettings: - result.pushNotificationSettings || + notificationSettings: + result.notificationSettings || ({ - pushNotificationsEnabled: true, - disabledPushNotificationTypes: [], - } as PushNotificationSettings), + push: { + enabled: true, + disabledCategories: [], + }, + } as NotificationSettings), // TODO: remove quizQuestions: diff --git a/src/services/mongoose/schema.ts b/src/services/mongoose/schema.ts index f4ec82868b..7b6bc919f5 100644 --- a/src/services/mongoose/schema.ts +++ b/src/services/mongoose/schema.ts @@ -259,16 +259,19 @@ const AccountSchema = new Schema( }, ], }, - pushNotificationSettings: { + notificationSettings: { type: { - pushNotificationsEnabled: { - type: Boolean, - default: true, - }, - disabledPushNotificationTypes: { - type: [String], - required: true, - default: [], + push: { + type: { + enabled: { + type: Boolean, + default: true, + }, + disabledCategories: { + type: [String], + default: [], + }, + }, }, }, }, diff --git a/src/services/mongoose/schema.types.d.ts b/src/services/mongoose/schema.types.d.ts index 21be2e55ca..d6b87d4662 100644 --- a/src/services/mongoose/schema.types.d.ts +++ b/src/services/mongoose/schema.types.d.ts @@ -84,7 +84,7 @@ interface AccountRecord { onchain: OnChainObjectForUser[] defaultWalletId: WalletId displayCurrency?: string - pushNotificationSettings?: PushNotificationSettings + notificationSettings?: NotificationSettings // business: title?: string diff --git a/src/services/notifications/create-push-notification-content.ts b/src/services/notifications/create-push-notification-content.ts index 2a05b68b00..99c8750158 100644 --- a/src/services/notifications/create-push-notification-content.ts +++ b/src/services/notifications/create-push-notification-content.ts @@ -3,10 +3,6 @@ import { getI18nInstance } from "@config" import { getCurrencyMajorExponent, MajorExponent } from "@domain/fiat" import { WalletCurrency } from "@domain/shared" import { getLanguageOrDefault } from "@domain/locale" -import { - GaloyPushNotifications, - mapNotificationTypeToPushNotificationType, -} from "@domain/notifications" const i18n = getI18nInstance() @@ -37,7 +33,6 @@ export const createPushNotificationContent = ({ }): { title: string body: string - pushNotificationType: PushNotificationType } => { const locale = getLanguageOrDefault(userLanguage) const baseCurrency = amount.currency @@ -92,10 +87,5 @@ export const createPushNotificationContent = ({ ) } - const pushNotificationType = - type === "balance" - ? GaloyPushNotifications.Balance - : mapNotificationTypeToPushNotificationType(type) - - return { title, body, pushNotificationType } + return { title, body } } diff --git a/src/services/notifications/index.ts b/src/services/notifications/index.ts index 92306a0f50..f6fd773879 100644 --- a/src/services/notifications/index.ts +++ b/src/services/notifications/index.ts @@ -2,7 +2,11 @@ import { toSats } from "@domain/bitcoin" import { WalletCurrency } from "@domain/shared" import { toCents, UsdDisplayCurrency } from "@domain/fiat" import { customPubSubTrigger, PubSubDefaultTriggers } from "@domain/pubsub" -import { NotificationsServiceError, NotificationType } from "@domain/notifications" +import { + GaloyNotificationCategories, + NotificationsServiceError, + NotificationType, +} from "@domain/notifications" import { PubSubService } from "@services/pubsub" import { wrapAsyncFunctionsToRunInSpan } from "@services/tracing" @@ -24,7 +28,7 @@ export const NotificationsService = (): INotificationsService => { displayPaymentAmount, paymentHash, recipientDeviceTokens, - recipientPushNotificationSettings, + recipientNotificationSettings, recipientLanguage, }: LightningTxReceivedArgs): Promise => { try { @@ -55,7 +59,9 @@ export const NotificationsService = (): INotificationsService => { }) if (recipientDeviceTokens && recipientDeviceTokens.length > 0) { - const { title, body, pushNotificationType } = createPushNotificationContent({ + const notificationCategory = GaloyNotificationCategories.Payments + + const { title, body } = createPushNotificationContent({ type: NotificationType.LnInvoicePaid, userLanguage: recipientLanguage, amount: paymentAmount, @@ -66,8 +72,8 @@ export const NotificationsService = (): INotificationsService => { deviceTokens: recipientDeviceTokens, title, body, - pushNotificationType, - pushNotificationSettings: recipientPushNotificationSettings, + notificationCategory, + notificationSettings: recipientNotificationSettings, }) if (result instanceof NotificationsServiceError) { @@ -89,7 +95,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, recipientDeviceTokens, - recipientPushNotificationSettings, + recipientNotificationSettings, recipientLanguage, }: IntraLedgerTxReceivedArgs): Promise => { try { @@ -124,7 +130,9 @@ export const NotificationsService = (): INotificationsService => { }) if (recipientDeviceTokens && recipientDeviceTokens.length > 0) { - const { title, body, pushNotificationType } = createPushNotificationContent({ + const notificationCategory = GaloyNotificationCategories.Payments + + const { title, body } = createPushNotificationContent({ type: NotificationType.IntraLedgerReceipt, userLanguage: recipientLanguage, amount: paymentAmount, @@ -135,8 +143,8 @@ export const NotificationsService = (): INotificationsService => { deviceTokens: recipientDeviceTokens, title, body, - pushNotificationType, - pushNotificationSettings: recipientPushNotificationSettings, + notificationCategory, + notificationSettings: recipientNotificationSettings, }) if (result instanceof NotificationsServiceError) { @@ -159,7 +167,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, deviceTokens, - pushNotificationSettings, + notificationSettings, language, txHash, }: { @@ -169,7 +177,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount: PaymentAmount displayPaymentAmount?: DisplayAmount deviceTokens: DeviceToken[] - pushNotificationSettings: PushNotificationSettings + notificationSettings: NotificationSettings language: UserLanguageOrEmpty txHash: OnChainTxHash }): Promise => { @@ -200,7 +208,9 @@ export const NotificationsService = (): INotificationsService => { }) if (deviceTokens.length > 0) { - const { title, body, pushNotificationType } = createPushNotificationContent({ + const notificationCategory = GaloyNotificationCategories.Payments + + const { title, body } = createPushNotificationContent({ type, userLanguage: language, amount: paymentAmount, @@ -211,8 +221,8 @@ export const NotificationsService = (): INotificationsService => { deviceTokens, title, body, - pushNotificationType, - pushNotificationSettings, + notificationCategory, + notificationSettings, }) if (result instanceof NotificationsServiceError) { @@ -234,7 +244,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, recipientDeviceTokens, - recipientPushNotificationSettings, + recipientNotificationSettings, recipientLanguage, txHash, }: OnChainTxReceivedArgs) => @@ -245,7 +255,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, deviceTokens: recipientDeviceTokens, - pushNotificationSettings: recipientPushNotificationSettings, + notificationSettings: recipientNotificationSettings, language: recipientLanguage, txHash, }) @@ -256,7 +266,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, recipientDeviceTokens, - recipientPushNotificationSettings, + recipientNotificationSettings, recipientLanguage, txHash, }: OnChainTxReceivedPendingArgs) => @@ -267,7 +277,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, deviceTokens: recipientDeviceTokens, - pushNotificationSettings: recipientPushNotificationSettings, + notificationSettings: recipientNotificationSettings, language: recipientLanguage, txHash, }) @@ -278,7 +288,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, senderDeviceTokens, - senderPushNotificationSettings, + senderNotificationSettings, senderLanguage, txHash, }: OnChainTxSentArgs) => @@ -289,7 +299,7 @@ export const NotificationsService = (): INotificationsService => { paymentAmount, displayPaymentAmount, deviceTokens: senderDeviceTokens, - pushNotificationSettings: senderPushNotificationSettings, + notificationSettings: senderNotificationSettings, language: senderLanguage, txHash, }) @@ -329,7 +339,7 @@ export const NotificationsService = (): INotificationsService => { const sendBalance = async ({ balanceAmount, deviceTokens, - pushNotificationSettings, + notificationSettings, displayBalanceAmount, recipientLanguage, }: SendBalanceArgs): Promise => { @@ -337,7 +347,9 @@ export const NotificationsService = (): INotificationsService => { if (!hasDeviceTokens) return true try { - const { title, body, pushNotificationType } = createPushNotificationContent({ + const notificationCategory = GaloyNotificationCategories.Payments + + const { title, body } = createPushNotificationContent({ type: "balance", userLanguage: recipientLanguage, amount: balanceAmount, @@ -348,8 +360,8 @@ export const NotificationsService = (): INotificationsService => { deviceTokens, title, body, - pushNotificationType, - pushNotificationSettings, + notificationCategory, + notificationSettings, }) if (result instanceof NotificationsServiceError) { @@ -388,8 +400,8 @@ export const NotificationsService = (): INotificationsService => { body, data, deviceTokens, - pushNotificationSettings, - pushNotificationType, + notificationSettings, + notificationCategory, }: SendFilteredPushNotificationArgs): Promise => { const hasDeviceTokens = deviceTokens && deviceTokens.length > 0 if (!hasDeviceTokens) return true @@ -400,8 +412,8 @@ export const NotificationsService = (): INotificationsService => { title, body, data, - pushNotificationSettings, - pushNotificationType, + notificationSettings, + notificationCategory, }) if (result instanceof NotificationsServiceError) { diff --git a/src/services/notifications/push-notification-filtering.ts b/src/services/notifications/push-notification-filtering.ts deleted file mode 100644 index 07057ef8bd..0000000000 --- a/src/services/notifications/push-notification-filtering.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const shouldSendPushNotification = ({ - pushNotificationSettings, - pushNotificationType, -}: { - pushNotificationSettings: PushNotificationSettings - pushNotificationType: PushNotificationType -}): boolean => { - if (pushNotificationSettings.pushNotificationsEnabled) { - return !pushNotificationSettings.disabledPushNotificationTypes.includes( - pushNotificationType, - ) - } - - return false -} diff --git a/src/services/notifications/push-notifications.ts b/src/services/notifications/push-notifications.ts index 71d3f0669d..2af84e2c76 100644 --- a/src/services/notifications/push-notifications.ts +++ b/src/services/notifications/push-notifications.ts @@ -3,9 +3,11 @@ import * as admin from "firebase-admin" import { DeviceTokensNotRegisteredNotificationsServiceError, InvalidDeviceNotificationsServiceError, + NotificationChannel, NotificationsServiceError, NotificationsServiceUnreachableServerError, UnknownNotificationsServiceError, + shouldSendNotification, } from "@domain/notifications" import { ErrorLevel, parseErrorMessageFromUnknown } from "@domain/shared" import { baseLogger } from "@services/logger" @@ -18,8 +20,6 @@ import { Messaging } from "firebase-admin/lib/messaging/messaging" import { GOOGLE_APPLICATION_CREDENTIALS } from "@config" -import { shouldSendPushNotification } from "./push-notification-filtering" - const logger = baseLogger.child({ module: "notifications" }) type MessagingPayload = admin.messaging.MessagingPayload @@ -116,17 +116,14 @@ export const PushNotificationsService = (): IPushNotificationsService => { } const sendFilteredNotification = async (args: SendFilteredPushNotificationArgs) => { - const { - pushNotificationSettings, - pushNotificationType, - data, - ...sendNotificationArgs - } = args + const { notificationSettings, notificationCategory, data, ...sendNotificationArgs } = + args if ( - !shouldSendPushNotification({ - pushNotificationSettings, - pushNotificationType, + !shouldSendNotification({ + notificationCategory, + notificationSettings, + notificationChannel: NotificationChannel.Push, }) ) { return { @@ -138,7 +135,7 @@ export const PushNotificationsService = (): IPushNotificationsService => { ...sendNotificationArgs, data: { ...data, - PushNotificationType: pushNotificationType, + NotificationCategory: notificationCategory, }, }) diff --git a/src/services/notifications/push-notifications.types.d.ts b/src/services/notifications/push-notifications.types.d.ts index e3514a3716..c8a8dd928f 100644 --- a/src/services/notifications/push-notifications.types.d.ts +++ b/src/services/notifications/push-notifications.types.d.ts @@ -10,8 +10,8 @@ type SendFilteredPushNotificationArgs = { title: string body: string data?: { [key: string]: string } - pushNotificationSettings: PushNotificationSettings - pushNotificationType: PushNotificationType + notificationSettings: NotificationSettings + notificationCategory: NotificationCategory } type SendFilteredPushNotificationStatus = diff --git a/test/legacy-integration/notifications/notification.spec.ts b/test/legacy-integration/notifications/notification.spec.ts index c03d239718..58164c39a3 100644 --- a/test/legacy-integration/notifications/notification.spec.ts +++ b/test/legacy-integration/notifications/notification.spec.ts @@ -18,7 +18,7 @@ import { getCurrentPriceAsDisplayPriceRatio, } from "@app/prices" import { WalletCurrency } from "@domain/shared" -import { GaloyPushNotifications } from "@domain/notifications" +import { GaloyNotificationCategories } from "@domain/notifications" let spy let displayPriceRatios: Record> @@ -44,9 +44,11 @@ const crcDisplayPaymentAmount = { displayInMajor: "3500.50", } -const unfilteredPushNotificationSettings: PushNotificationSettings = { - pushNotificationsEnabled: true, - disabledPushNotificationTypes: [], +const unfilteredNotificationSettings: NotificationSettings = { + push: { + enabled: true, + disabledCategories: [], + }, } beforeAll(async () => { @@ -221,7 +223,7 @@ describe("notification", () => { displayPaymentAmount: crcDisplayPaymentAmount, paymentHash, recipientDeviceTokens: deviceTokens, - recipientPushNotificationSettings: unfilteredPushNotificationSettings, + recipientNotificationSettings: unfilteredNotificationSettings, recipientLanguage: language, }) @@ -229,7 +231,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( - GaloyPushNotifications.Payments, + GaloyNotificationCategories.Payments, ) }), ) @@ -268,7 +270,7 @@ describe("notification", () => { recipientWalletId: walletId, displayPaymentAmount: crcDisplayPaymentAmount, recipientDeviceTokens: deviceTokens, - recipientPushNotificationSettings: unfilteredPushNotificationSettings, + recipientNotificationSettings: unfilteredNotificationSettings, recipientLanguage: language, }) @@ -276,7 +278,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( - GaloyPushNotifications.Payments, + GaloyNotificationCategories.Payments, ) }), ) @@ -316,7 +318,7 @@ describe("notification", () => { displayPaymentAmount: crcDisplayPaymentAmount, txHash, recipientDeviceTokens: deviceTokens, - recipientPushNotificationSettings: unfilteredPushNotificationSettings, + recipientNotificationSettings: unfilteredNotificationSettings, recipientLanguage: language, }) @@ -324,7 +326,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( - GaloyPushNotifications.Payments, + GaloyNotificationCategories.Payments, ) }), ) @@ -363,7 +365,7 @@ describe("notification", () => { txHash, displayPaymentAmount: crcDisplayPaymentAmount, recipientDeviceTokens: deviceTokens, - recipientPushNotificationSettings: unfilteredPushNotificationSettings, + recipientNotificationSettings: unfilteredNotificationSettings, recipientLanguage: language, }) @@ -371,7 +373,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( - GaloyPushNotifications.Payments, + GaloyNotificationCategories.Payments, ) }), ) @@ -410,7 +412,7 @@ describe("notification", () => { txHash, displayPaymentAmount: crcDisplayPaymentAmount, senderDeviceTokens: deviceTokens, - senderPushNotificationSettings: unfilteredPushNotificationSettings, + senderNotificationSettings: unfilteredNotificationSettings, senderLanguage: language, }) @@ -418,7 +420,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( - GaloyPushNotifications.Payments, + GaloyNotificationCategories.Payments, ) }), ) diff --git a/test/legacy-integration/notifications/push-notification.spec.ts b/test/legacy-integration/notifications/push-notification.spec.ts index dd8ba46f43..bcc71a2e1a 100644 --- a/test/legacy-integration/notifications/push-notification.spec.ts +++ b/test/legacy-integration/notifications/push-notification.spec.ts @@ -5,18 +5,20 @@ import { describe("push notification", () => { it("should filter a notification", async () => { - const pushNotificationType = "transaction" as PushNotificationType - const pushNotificationSettings = { - pushNotificationsEnabled: true, - disabledPushNotificationTypes: [pushNotificationType], + const notificationCategory = "transaction" as NotificationCategory + const notificationSettings = { + push: { + enabled: true, + disabledCategories: [notificationCategory], + }, } const result = await PushNotificationsService().sendFilteredNotification({ body: "body", title: "title", deviceTokens: ["deviceToken" as DeviceToken], - pushNotificationType, - pushNotificationSettings, + notificationCategory, + notificationSettings, }) expect(result).toEqual({ diff --git a/test/unit/domain/notifications/index.spec.ts b/test/unit/domain/notifications/index.spec.ts new file mode 100644 index 0000000000..a3b8087f5f --- /dev/null +++ b/test/unit/domain/notifications/index.spec.ts @@ -0,0 +1,149 @@ +import { + NotificationChannel, + disableNotificationCategoryForChannel, + setNotificationChannelIsEnabled, + shouldSendNotification, +} from "@domain/notifications" + +describe("Notifications - push notification filtering", () => { + describe("shouldSendPushNotification", () => { + it("returns false when push notifications are disabled", () => { + const notificationSettings: NotificationSettings = { + push: { + enabled: false, + disabledCategories: [], + }, + } + + const notificationCategory = "transaction" as NotificationCategory + + expect( + shouldSendNotification({ + notificationSettings, + notificationCategory, + notificationChannel: NotificationChannel.Push, + }), + ).toBe(false) + }) + + it("returns true when a notification is not disabled", () => { + const notificationSettings: NotificationSettings = { + push: { + enabled: true, + disabledCategories: [], + }, + } + + const notificationCategory = "transaction" as NotificationCategory + + expect( + shouldSendNotification({ + notificationSettings, + notificationCategory, + notificationChannel: NotificationChannel.Push, + }), + ).toBe(true) + }) + + it("returns false when a notification is disabled", () => { + const notificationCategory = "transaction" as NotificationCategory + + const notificationSettings: NotificationSettings = { + push: { + enabled: true, + disabledCategories: [notificationCategory], + }, + } + + expect( + shouldSendNotification({ + notificationSettings, + notificationCategory, + notificationChannel: NotificationChannel.Push, + }), + ).toBe(false) + }) + }) + + describe("setNotificationChannelIsEnabled", () => { + it("clears disabled categories when enabling a channel", () => { + const notificationSettings: NotificationSettings = { + push: { + enabled: false, + disabledCategories: ["transaction" as NotificationCategory], + }, + } + + const notificationChannel = NotificationChannel.Push + + const enabled = true + + const result = setNotificationChannelIsEnabled({ + notificationSettings, + notificationChannel, + enabled, + }) + + expect(result).toEqual({ + push: { + enabled, + disabledCategories: [], + }, + }) + }) + }) + + describe("disableNotificationCategoryForChannel", () => { + it("adds a category to the disabled categories", () => { + const notificationSettings: NotificationSettings = { + push: { + enabled: true, + disabledCategories: [], + }, + } + + const notificationChannel = NotificationChannel.Push + + const notificationCategory = "transaction" as NotificationCategory + + const result = disableNotificationCategoryForChannel({ + notificationSettings, + notificationChannel, + notificationCategory, + }) + + expect(result).toEqual({ + push: { + enabled: true, + disabledCategories: [notificationCategory], + }, + }) + }) + }) + + it("does not add a category to the disabled categories if it is already there", () => { + const notificationCategory = "transaction" as NotificationCategory + + const notificationSettings: NotificationSettings = { + push: { + enabled: true, + disabledCategories: [notificationCategory], + }, + } + + const notificationChannel = NotificationChannel.Push + + const result = disableNotificationCategoryForChannel({ + notificationSettings, + notificationChannel, + notificationCategory, + }) + + expect(result).toEqual({ + push: { + enabled: true, + disabledCategories: [notificationCategory], + }, + }) + }) +}) diff --git a/test/unit/domain/wallets/payment-input-validator.spec.ts b/test/unit/domain/wallets/payment-input-validator.spec.ts index a2aeae7ed0..522043df41 100644 --- a/test/unit/domain/wallets/payment-input-validator.spec.ts +++ b/test/unit/domain/wallets/payment-input-validator.spec.ts @@ -22,9 +22,11 @@ describe("PaymentInputValidator", () => { latitude: 0, longitude: 0, }, - pushNotificationSettings: { - pushNotificationsEnabled: true, - disabledPushNotificationTypes: [], + notificationSettings: { + push: { + enabled: true, + disabledCategories: [], + }, }, contactEnabled: true, contacts: [], diff --git a/test/unit/services/notifications/push-notification-filtering.spec.ts b/test/unit/services/notifications/push-notification-filtering.spec.ts deleted file mode 100644 index d0d31468da..0000000000 --- a/test/unit/services/notifications/push-notification-filtering.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { shouldSendPushNotification } from "@services/notifications/push-notification-filtering" - -describe("Notifications - push notification filtering", () => { - describe("shouldSendPushNotification", () => { - it("returns false when push notifications are disabled", () => { - const pushNotificationSettings = { - pushNotificationsEnabled: false, - disabledPushNotificationTypes: [], - } - - const pushNotificationType = "transaction" as PushNotificationType - - expect( - shouldSendPushNotification({ - pushNotificationSettings, - pushNotificationType, - }), - ).toBe(false) - }) - }) - - it("returns true when a notification is not disabled", () => { - const pushNotificationSettings = { - pushNotificationsEnabled: true, - disabledPushNotificationTypes: [], - } - - const pushNotificationType = "transaction" as PushNotificationType - - expect( - shouldSendPushNotification({ - pushNotificationSettings, - pushNotificationType, - }), - ).toBe(true) - }) - - it("returns false when a notification is disabled", () => { - const pushNotificationSettings = { - pushNotificationsEnabled: true, - disabledPushNotificationTypes: ["transaction" as PushNotificationType], - } - - const pushNotificationType = "transaction" as PushNotificationType - - expect( - shouldSendPushNotification({ - pushNotificationSettings, - pushNotificationType, - }), - ).toBe(false) - }) -}) From deb5cb9f8e6e962b59679f290f718dc2507936d1 Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Tue, 19 Sep 2023 11:28:19 -0500 Subject: [PATCH 10/12] chore: address pr feedback --- dev/apollo-federation/supergraph.graphql | 12 +-- ...el.ts => disable-notification-category.ts} | 8 +- ...nel.ts => enable-notification-category.ts} | 8 +- src/app/accounts/index.ts | 4 +- src/domain/notifications/index.ts | 76 ++++++++++++------- src/graphql/public/mutations.ts | 10 +-- ...sable-notification-category-for-channel.ts | 16 ++-- ...nable-notification-category-for-channel.ts | 16 ++-- src/graphql/public/schema.graphql | 12 +-- .../notifications/notification.spec.ts | 10 +-- test/unit/domain/notifications/index.spec.ts | 6 +- 11 files changed, 98 insertions(+), 80 deletions(-) rename src/app/accounts/{enable-notification-category-for-channel.ts => disable-notification-category.ts} (77%) rename src/app/accounts/{disable-notification-category-for-channel.ts => enable-notification-category.ts} (77%) diff --git a/dev/apollo-federation/supergraph.graphql b/dev/apollo-federation/supergraph.graphql index 24459835a0..ed23e006ea 100644 --- a/dev/apollo-federation/supergraph.graphql +++ b/dev/apollo-federation/supergraph.graphql @@ -57,11 +57,11 @@ type AccountDeletePayload success: Boolean! } -input AccountDisableNotificationCategoryForChannelInput +input AccountDisableNotificationCategoryInput @join__type(graph: PUBLIC) { category: NotificationCategory! - channel: NotificationChannel! + channel: NotificationChannel } input AccountDisableNotificationChannelInput @@ -70,11 +70,11 @@ input AccountDisableNotificationChannelInput channel: NotificationChannel! } -input AccountEnableNotificationCategoryForChannelInput +input AccountEnableNotificationCategoryInput @join__type(graph: PUBLIC) { category: NotificationCategory! - channel: NotificationChannel! + channel: NotificationChannel } input AccountEnableNotificationChannelInput @@ -851,9 +851,9 @@ type Mutation @join__type(graph: PUBLIC) { accountDelete: AccountDeletePayload! - accountDisableNotificationCategoryForChannel(input: AccountDisableNotificationCategoryForChannelInput!): AccountUpdateNotificationSettingsPayload! + accountDisableNotificationCategory(input: AccountDisableNotificationCategoryInput!): AccountUpdateNotificationSettingsPayload! accountDisableNotificationChannel(input: AccountDisableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! - accountEnableNotificationCategoryForChannel(input: AccountEnableNotificationCategoryForChannelInput!): AccountUpdateNotificationSettingsPayload! + accountEnableNotificationCategory(input: AccountEnableNotificationCategoryInput!): AccountUpdateNotificationSettingsPayload! accountEnableNotificationChannel(input: AccountEnableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! accountUpdateDefaultWalletId(input: AccountUpdateDefaultWalletIdInput!): AccountUpdateDefaultWalletIdPayload! accountUpdateDisplayCurrency(input: AccountUpdateDisplayCurrencyInput!): AccountUpdateDisplayCurrencyPayload! diff --git a/src/app/accounts/enable-notification-category-for-channel.ts b/src/app/accounts/disable-notification-category.ts similarity index 77% rename from src/app/accounts/enable-notification-category-for-channel.ts rename to src/app/accounts/disable-notification-category.ts index 90aac3a1e9..8bc1bc4014 100644 --- a/src/app/accounts/enable-notification-category-for-channel.ts +++ b/src/app/accounts/disable-notification-category.ts @@ -1,16 +1,16 @@ import { checkedToNotificationCategory, - enableNotificationCategoryForChannel as enableNotificationChannel, + disableNotificationCategory as domainDisableNotificationCategory, } from "@domain/notifications" import { AccountsRepository } from "@services/mongoose" -export const enableNotificationCategoryForChannel = async ({ +export const disableNotificationCategory = async ({ accountId, notificationChannel, notificationCategory, }: { accountId: AccountId - notificationChannel: NotificationChannel + notificationChannel?: NotificationChannel notificationCategory: string }): Promise => { const checkedNotificationCategory = checkedToNotificationCategory(notificationCategory) @@ -19,7 +19,7 @@ export const enableNotificationCategoryForChannel = async ({ const account = await AccountsRepository().findById(accountId) if (account instanceof Error) return account - const newNotificationSettings = enableNotificationChannel({ + const newNotificationSettings = domainDisableNotificationCategory({ notificationSettings: account.notificationSettings, notificationChannel, notificationCategory: checkedNotificationCategory, diff --git a/src/app/accounts/disable-notification-category-for-channel.ts b/src/app/accounts/enable-notification-category.ts similarity index 77% rename from src/app/accounts/disable-notification-category-for-channel.ts rename to src/app/accounts/enable-notification-category.ts index ddbd2a3386..6248e6a499 100644 --- a/src/app/accounts/disable-notification-category-for-channel.ts +++ b/src/app/accounts/enable-notification-category.ts @@ -1,16 +1,16 @@ import { checkedToNotificationCategory, - disableNotificationCategoryForChannel as disableNotificationCategory, + enableNotificationCategory as domainEnableNotificationCategory, } from "@domain/notifications" import { AccountsRepository } from "@services/mongoose" -export const disableNotificationCategoryForChannel = async ({ +export const enableNotificationCategory = async ({ accountId, notificationChannel, notificationCategory, }: { accountId: AccountId - notificationChannel: NotificationChannel + notificationChannel?: NotificationChannel notificationCategory: string }): Promise => { const checkedNotificationCategory = checkedToNotificationCategory(notificationCategory) @@ -19,7 +19,7 @@ export const disableNotificationCategoryForChannel = async ({ const account = await AccountsRepository().findById(accountId) if (account instanceof Error) return account - const newNotificationSettings = disableNotificationCategory({ + const newNotificationSettings = domainEnableNotificationCategory({ notificationSettings: account.notificationSettings, notificationChannel, notificationCategory: checkedNotificationCategory, diff --git a/src/app/accounts/index.ts b/src/app/accounts/index.ts index acbf3d7f18..1daba7632d 100644 --- a/src/app/accounts/index.ts +++ b/src/app/accounts/index.ts @@ -21,8 +21,8 @@ export * from "./update-display-currency" export * from "./username-available" export * from "./delete-business-map-info" export * from "./upgrade-device-account" -export * from "./disable-notification-category-for-channel" -export * from "./enable-notification-category-for-channel" +export * from "./disable-notification-category" +export * from "./enable-notification-category" export * from "./enable-notification-channel" export * from "./disable-notification-channel" diff --git a/src/domain/notifications/index.ts b/src/domain/notifications/index.ts index 1e165c4612..71bdf88a42 100644 --- a/src/domain/notifications/index.ts +++ b/src/domain/notifications/index.ts @@ -57,54 +57,74 @@ export const setNotificationChannelIsEnabled = ({ } } -export const enableNotificationCategoryForChannel = ({ +export const enableNotificationCategory = ({ notificationSettings, notificationChannel, notificationCategory, }: { notificationSettings: NotificationSettings - notificationChannel: NotificationChannel + notificationChannel?: NotificationChannel notificationCategory: NotificationCategory }): NotificationSettings => { - const notificationChannelSettings = notificationSettings[notificationChannel] - const disabledCategories = notificationChannelSettings.disabledCategories - - const newNotificationSettings = { - enabled: notificationChannelSettings.enabled, - disabledCategories: disabledCategories.filter( - (category) => category !== notificationCategory, - ), + const notificationChannelsToUpdate: NotificationChannel[] = notificationChannel + ? [notificationChannel] + : Object.values(NotificationChannel) + + let newNotificationSettings = notificationSettings + + for (const notificationChannel of notificationChannelsToUpdate) { + const notificationChannelSettings = notificationSettings[notificationChannel] + const disabledCategories = notificationChannelSettings.disabledCategories + + const newNotificationChannelSettings = { + enabled: notificationChannelSettings.enabled, + disabledCategories: disabledCategories.filter( + (category) => category !== notificationCategory, + ), + } + + newNotificationSettings = { + ...notificationSettings, + [notificationChannel]: newNotificationChannelSettings, + } } - return { - ...notificationSettings, - [notificationChannel]: newNotificationSettings, - } + return newNotificationSettings } -export const disableNotificationCategoryForChannel = ({ +export const disableNotificationCategory = ({ notificationSettings, notificationChannel, notificationCategory, }: { notificationSettings: NotificationSettings - notificationChannel: NotificationChannel + notificationChannel?: NotificationChannel notificationCategory: NotificationCategory }): NotificationSettings => { - const notificationChannelSettings = notificationSettings[notificationChannel] - const disabledCategories = notificationChannelSettings.disabledCategories - disabledCategories.push(notificationCategory) - const uniqueDisabledCategories = [...new Set(disabledCategories)] - - const newNotificationSettings = { - enabled: notificationChannelSettings.enabled, - disabledCategories: uniqueDisabledCategories, + const notificationChannelsToUpdate: NotificationChannel[] = notificationChannel + ? [notificationChannel] + : Object.values(NotificationChannel) + + let newNotificationSettings = notificationSettings + + for (const notificationChannel of notificationChannelsToUpdate) { + const notificationChannelSettings = notificationSettings[notificationChannel] + const disabledCategories = notificationChannelSettings.disabledCategories + disabledCategories.push(notificationCategory) + const uniqueDisabledCategories = [...new Set(disabledCategories)] + + const newNotificationChannelSettings = { + enabled: notificationChannelSettings.enabled, + disabledCategories: uniqueDisabledCategories, + } + + newNotificationSettings = { + ...notificationSettings, + [notificationChannel]: newNotificationChannelSettings, + } } - return { - ...notificationSettings, - [notificationChannel]: newNotificationSettings, - } + return newNotificationSettings } export const shouldSendNotification = ({ diff --git a/src/graphql/public/mutations.ts b/src/graphql/public/mutations.ts index 35722fe505..892aabc502 100644 --- a/src/graphql/public/mutations.ts +++ b/src/graphql/public/mutations.ts @@ -49,8 +49,8 @@ import UserTotpDeleteMutation from "@graphql/public/root/mutation/user-totp-dele import CallbackEndpointAdd from "./root/mutation/callback-endpoint-add" import CallbackEndpointDelete from "./root/mutation/callback-endpoint-delete" -import AccountEnableNotificationCategoryForChannelMutation from "./root/mutation/account-enable-notification-category-for-channel" -import AccountDisableNotificationCategoryForChannelMutation from "./root/mutation/account-disable-notification-category-for-channel" +import AccountEnableNotificationCategoryMutation from "./root/mutation/account-enable-notification-category-for-channel" +import AccountDisableNotificationCategoryMutation from "./root/mutation/account-disable-notification-category-for-channel" import AccountEnableNotificationChannelMutation from "./root/mutation/account-enable-notification-channel" import AccountDisableNotificationChannelMutation from "./root/mutation/account-disable-notification-channel" @@ -90,10 +90,8 @@ export const mutationFields = { userContactUpdateAlias: UserContactUpdateAliasMutation, accountUpdateDefaultWalletId: AccountUpdateDefaultWalletIdMutation, accountUpdateDisplayCurrency: AccountUpdateDisplayCurrencyMutation, - accountEnableNotificationCategoryForChannel: - AccountEnableNotificationCategoryForChannelMutation, - accountDisableNotificationCategoryForChannel: - AccountDisableNotificationCategoryForChannelMutation, + accountEnableNotificationCategory: AccountEnableNotificationCategoryMutation, + accountDisableNotificationCategory: AccountDisableNotificationCategoryMutation, accountEnableNotificationChannel: AccountEnableNotificationChannelMutation, accountDisableNotificationChannel: AccountDisableNotificationChannelMutation, diff --git a/src/graphql/public/root/mutation/account-disable-notification-category-for-channel.ts b/src/graphql/public/root/mutation/account-disable-notification-category-for-channel.ts index 4c0d82e3fa..0a551e35bd 100644 --- a/src/graphql/public/root/mutation/account-disable-notification-category-for-channel.ts +++ b/src/graphql/public/root/mutation/account-disable-notification-category-for-channel.ts @@ -6,11 +6,11 @@ import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" import NotificationChannel from "@graphql/shared/types/scalar/notification-channel" import NotificationCategory from "@graphql/shared/types/scalar/notification-category" -const AccountDisableNotificationCategoryForChannelInput = GT.Input({ - name: "AccountDisableNotificationCategoryForChannelInput", +const AccountDisableNotificationCategoryInput = GT.Input({ + name: "AccountDisableNotificationCategoryInput", fields: () => ({ channel: { - type: GT.NonNull(NotificationChannel), + type: NotificationChannel, }, category: { type: GT.NonNull(NotificationCategory), @@ -18,12 +18,12 @@ const AccountDisableNotificationCategoryForChannelInput = GT.Input({ }), }) -const AccountDisableNotificationCategoryForChannelMutation = GT.Field< +const AccountDisableNotificationCategoryMutation = GT.Field< null, GraphQLPublicContextAuth, { input: { - channel: NotificationChannel | Error + channel?: NotificationChannel | Error category: NotificationCategory } } @@ -33,14 +33,14 @@ const AccountDisableNotificationCategoryForChannelMutation = GT.Field< }, type: GT.NonNull(AccountUpdateNotificationSettingsPayload), args: { - input: { type: GT.NonNull(AccountDisableNotificationCategoryForChannelInput) }, + input: { type: GT.NonNull(AccountDisableNotificationCategoryInput) }, }, resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { const { channel, category } = args.input if (channel instanceof Error) return { errors: [{ message: channel.message }] } - const result = await Accounts.disableNotificationCategoryForChannel({ + const result = await Accounts.disableNotificationCategory({ accountId: domainAccount.id, notificationChannel: channel, notificationCategory: category, @@ -57,4 +57,4 @@ const AccountDisableNotificationCategoryForChannelMutation = GT.Field< }, }) -export default AccountDisableNotificationCategoryForChannelMutation +export default AccountDisableNotificationCategoryMutation diff --git a/src/graphql/public/root/mutation/account-enable-notification-category-for-channel.ts b/src/graphql/public/root/mutation/account-enable-notification-category-for-channel.ts index 89c72b799a..f0b309e3a4 100644 --- a/src/graphql/public/root/mutation/account-enable-notification-category-for-channel.ts +++ b/src/graphql/public/root/mutation/account-enable-notification-category-for-channel.ts @@ -6,11 +6,11 @@ import { mapAndParseErrorForGqlResponse } from "@graphql/error-map" import NotificationChannel from "@graphql/shared/types/scalar/notification-channel" import NotificationCategory from "@graphql/shared/types/scalar/notification-category" -const AccountEnableNotificationCategoryForChannelInput = GT.Input({ - name: "AccountEnableNotificationCategoryForChannelInput", +const AccountEnableNotificationCategoryInput = GT.Input({ + name: "AccountEnableNotificationCategoryInput", fields: () => ({ channel: { - type: GT.NonNull(NotificationChannel), + type: NotificationChannel, }, category: { type: GT.NonNull(NotificationCategory), @@ -18,12 +18,12 @@ const AccountEnableNotificationCategoryForChannelInput = GT.Input({ }), }) -const AccountEnableNotificationCategoryForChannelMutation = GT.Field< +const AccountEnableNotificationCategoryMutation = GT.Field< null, GraphQLPublicContextAuth, { input: { - channel: NotificationChannel | Error + channel?: NotificationChannel | Error category: NotificationCategory } } @@ -33,14 +33,14 @@ const AccountEnableNotificationCategoryForChannelMutation = GT.Field< }, type: GT.NonNull(AccountUpdateNotificationSettingsPayload), args: { - input: { type: GT.NonNull(AccountEnableNotificationCategoryForChannelInput) }, + input: { type: GT.NonNull(AccountEnableNotificationCategoryInput) }, }, resolve: async (_, args, { domainAccount }: { domainAccount: Account }) => { const { channel, category } = args.input if (channel instanceof Error) return { errors: [{ message: channel.message }] } - const result = await Accounts.enableNotificationCategoryForChannel({ + const result = await Accounts.enableNotificationCategory({ accountId: domainAccount.id, notificationChannel: channel, notificationCategory: category, @@ -57,4 +57,4 @@ const AccountEnableNotificationCategoryForChannelMutation = GT.Field< }, }) -export default AccountEnableNotificationCategoryForChannelMutation +export default AccountEnableNotificationCategoryMutation diff --git a/src/graphql/public/schema.graphql b/src/graphql/public/schema.graphql index 17d9d3b8ab..681fca2e67 100644 --- a/src/graphql/public/schema.graphql +++ b/src/graphql/public/schema.graphql @@ -30,18 +30,18 @@ type AccountDeletePayload { success: Boolean! } -input AccountDisableNotificationCategoryForChannelInput { +input AccountDisableNotificationCategoryInput { category: NotificationCategory! - channel: NotificationChannel! + channel: NotificationChannel } input AccountDisableNotificationChannelInput { channel: NotificationChannel! } -input AccountEnableNotificationCategoryForChannelInput { +input AccountEnableNotificationCategoryInput { category: NotificationCategory! - channel: NotificationChannel! + channel: NotificationChannel } input AccountEnableNotificationChannelInput { @@ -645,9 +645,9 @@ type MobileVersions { type Mutation { accountDelete: AccountDeletePayload! - accountDisableNotificationCategoryForChannel(input: AccountDisableNotificationCategoryForChannelInput!): AccountUpdateNotificationSettingsPayload! + accountDisableNotificationCategory(input: AccountDisableNotificationCategoryInput!): AccountUpdateNotificationSettingsPayload! accountDisableNotificationChannel(input: AccountDisableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! - accountEnableNotificationCategoryForChannel(input: AccountEnableNotificationCategoryForChannelInput!): AccountUpdateNotificationSettingsPayload! + accountEnableNotificationCategory(input: AccountEnableNotificationCategoryInput!): AccountUpdateNotificationSettingsPayload! accountEnableNotificationChannel(input: AccountEnableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! accountUpdateDefaultWalletId(input: AccountUpdateDefaultWalletIdInput!): AccountUpdateDefaultWalletIdPayload! accountUpdateDisplayCurrency(input: AccountUpdateDisplayCurrencyInput!): AccountUpdateDisplayCurrencyPayload! diff --git a/test/legacy-integration/notifications/notification.spec.ts b/test/legacy-integration/notifications/notification.spec.ts index 58164c39a3..d5df3ccaea 100644 --- a/test/legacy-integration/notifications/notification.spec.ts +++ b/test/legacy-integration/notifications/notification.spec.ts @@ -230,7 +230,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) - expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + expect(sendFilteredNotification.mock.calls[0][0].notificationCategory).toBe( GaloyNotificationCategories.Payments, ) }), @@ -277,7 +277,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) - expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + expect(sendFilteredNotification.mock.calls[0][0].notificationCategory).toBe( GaloyNotificationCategories.Payments, ) }), @@ -325,7 +325,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) - expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + expect(sendFilteredNotification.mock.calls[0][0].notificationCategory).toBe( GaloyNotificationCategories.Payments, ) }), @@ -372,7 +372,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) - expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + expect(sendFilteredNotification.mock.calls[0][0].notificationCategory).toBe( GaloyNotificationCategories.Payments, ) }), @@ -419,7 +419,7 @@ describe("notification", () => { expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBe(title) expect(sendFilteredNotification.mock.calls[0][0].body).toBe(body) - expect(sendFilteredNotification.mock.calls[0][0].pushNotificationType).toBe( + expect(sendFilteredNotification.mock.calls[0][0].notificationCategory).toBe( GaloyNotificationCategories.Payments, ) }), diff --git a/test/unit/domain/notifications/index.spec.ts b/test/unit/domain/notifications/index.spec.ts index a3b8087f5f..00e5db7b5f 100644 --- a/test/unit/domain/notifications/index.spec.ts +++ b/test/unit/domain/notifications/index.spec.ts @@ -1,6 +1,6 @@ import { NotificationChannel, - disableNotificationCategoryForChannel, + disableNotificationCategory, setNotificationChannelIsEnabled, shouldSendNotification, } from "@domain/notifications" @@ -106,7 +106,7 @@ describe("Notifications - push notification filtering", () => { const notificationCategory = "transaction" as NotificationCategory - const result = disableNotificationCategoryForChannel({ + const result = disableNotificationCategory({ notificationSettings, notificationChannel, notificationCategory, @@ -133,7 +133,7 @@ describe("Notifications - push notification filtering", () => { const notificationChannel = NotificationChannel.Push - const result = disableNotificationCategoryForChannel({ + const result = disableNotificationCategory({ notificationSettings, notificationChannel, notificationCategory, From 337cffe22785ee32e813f378b7ba7ebeb809326c Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Tue, 19 Sep 2023 14:34:46 -0500 Subject: [PATCH 11/12] test: get e2e tests working --- src/services/mongoose/accounts.ts | 17 +++--- .../account-disable-notification-category.gql | 15 ++++++ .../account-disable-notification-channel.gql | 15 ++++++ .../account-enable-notification-category.gql | 15 ++++++ .../account-enable-notification-channel.gql | 15 ++++++ ...ount-update-push-notification-settings.gql | 13 ----- test/bats/notification-settings.bats | 54 +++++++++++++++++++ test/bats/push-notification-settings.bats | 33 ------------ 8 files changed, 122 insertions(+), 55 deletions(-) create mode 100644 test/bats/gql/account-disable-notification-category.gql create mode 100644 test/bats/gql/account-disable-notification-channel.gql create mode 100644 test/bats/gql/account-enable-notification-category.gql create mode 100644 test/bats/gql/account-enable-notification-channel.gql delete mode 100644 test/bats/gql/account-update-push-notification-settings.gql create mode 100644 test/bats/notification-settings.bats delete mode 100644 test/bats/push-notification-settings.bats diff --git a/src/services/mongoose/accounts.ts b/src/services/mongoose/accounts.ts index 1d8cec8fa1..f66958022c 100644 --- a/src/services/mongoose/accounts.ts +++ b/src/services/mongoose/accounts.ts @@ -227,15 +227,14 @@ const translateToAccount = (result: AccountRecord): Account => ({ ), withdrawFee: result.withdrawFee as Satoshis, isEditor: result.role === "editor", - notificationSettings: - result.notificationSettings || - ({ - push: { - enabled: true, - disabledCategories: [], - }, - } as NotificationSettings), - + notificationSettings: { + push: { + enabled: result.notificationSettings + ? result.notificationSettings.push.enabled + : true, + disabledCategories: result.notificationSettings?.push?.disabledCategories || [], + }, + }, // TODO: remove quizQuestions: result.earn?.map( diff --git a/test/bats/gql/account-disable-notification-category.gql b/test/bats/gql/account-disable-notification-category.gql new file mode 100644 index 0000000000..771a6c7e9f --- /dev/null +++ b/test/bats/gql/account-disable-notification-category.gql @@ -0,0 +1,15 @@ +mutation accountDisableNotificationCategory($input: AccountDisableNotificationCategoryInput!) { + accountDisableNotificationCategory(input: $input) { + errors { + message + } + account { + notificationSettings { + push { + enabled + disabledCategories + } + } + } + } +} diff --git a/test/bats/gql/account-disable-notification-channel.gql b/test/bats/gql/account-disable-notification-channel.gql new file mode 100644 index 0000000000..34e9a5795b --- /dev/null +++ b/test/bats/gql/account-disable-notification-channel.gql @@ -0,0 +1,15 @@ +mutation accountDisableNotificationChannel($input: AccountDisableNotificationChannelInput!) { + accountDisableNotificationChannel(input: $input) { + errors { + message + } + account { + notificationSettings { + push { + enabled + disabledCategories + } + } + } + } +} diff --git a/test/bats/gql/account-enable-notification-category.gql b/test/bats/gql/account-enable-notification-category.gql new file mode 100644 index 0000000000..5b5b752fe9 --- /dev/null +++ b/test/bats/gql/account-enable-notification-category.gql @@ -0,0 +1,15 @@ +mutation accountEnableNotificationCategory($input: AccountEnableNotificationCategoryInput!) { + accountEnableNotificationCategory(input: $input) { + errors { + message + } + account { + notificationSettings { + push { + enabled + disabledCategories + } + } + } + } +} diff --git a/test/bats/gql/account-enable-notification-channel.gql b/test/bats/gql/account-enable-notification-channel.gql new file mode 100644 index 0000000000..a3593270dc --- /dev/null +++ b/test/bats/gql/account-enable-notification-channel.gql @@ -0,0 +1,15 @@ +mutation accountEnableNotificationChannel($input: AccountEnableNotificationChannelInput!) { + accountEnableNotificationChannel(input: $input) { + errors { + message + } + account { + notificationSettings { + push { + enabled + disabledCategories + } + } + } + } +} diff --git a/test/bats/gql/account-update-push-notification-settings.gql b/test/bats/gql/account-update-push-notification-settings.gql deleted file mode 100644 index 195d37c4b1..0000000000 --- a/test/bats/gql/account-update-push-notification-settings.gql +++ /dev/null @@ -1,13 +0,0 @@ -mutation accountUpdatePushNotificationSettings($input: AccountUpdatePushNotificationSettingsInput!) { - accountUpdatePushNotificationSettings(input: $input) { - errors { - message - } - account { - pushNotificationSettings { - pushNotificationsEnabled - disabledPushNotificationTypes - } - } - } -} diff --git a/test/bats/notification-settings.bats b/test/bats/notification-settings.bats new file mode 100644 index 0000000000..3496ff27e8 --- /dev/null +++ b/test/bats/notification-settings.bats @@ -0,0 +1,54 @@ +#!/usr/bin/env bats + +load "helpers/setup-and-teardown" + +setup_file() { + clear_cache + reset_redis + + bitcoind_init + start_trigger + start_server + + initialize_user_from_onchain "$ALICE_TOKEN_NAME" "$ALICE_PHONE" "$CODE" +} + +teardown_file() { + stop_trigger + stop_server +} + + +@test "notification-settings: disable/enable notification channel" { + token_name="$ALICE_TOKEN_NAME" + + variables=$( + jq -n \ + '{input: { channel: "PUSH" }}') + + exec_graphql "$token_name" 'account-disable-notification-channel' "$variables" + channel_enabled="$(graphql_output '.data.accountDisableNotificationChannel.account.notificationSettings.push.enabled')" + [[ "$channel_enabled" == "false" ]] || exit 1 + + exec_graphql "$token_name" 'account-enable-notification-channel' "$variables" + + channel_enabled="$(graphql_output '.data.accountEnableNotificationChannel.account.notificationSettings.push.enabled')" + [[ "$channel_enabled" == "true" ]] || exit 1 +} + +@test "notification-settings: disable/enable notification category" { + token_name="$ALICE_TOKEN_NAME" + + variables=$( + jq -n \ + '{input: { channel: "PUSH", category: "Circles" }}') + + exec_graphql "$token_name" 'account-disable-notification-category' "$variables" + disabled_category="$(graphql_output '.data.accountDisableNotificationCategory.account.notificationSettings.push.disabledCategories[0]')" + [[ "$disabled_category" == "Circles" ]] || exit 1 + + exec_graphql "$token_name" 'account-enable-notification-category' "$variables" + + disabled_length="$(graphql_output '.data.accountEnableNotificationCategory.account.notificationSettings.push.disabledCategories | length')" + [[ "$disabled_length" == "0" ]] || exit 1 +} diff --git a/test/bats/push-notification-settings.bats b/test/bats/push-notification-settings.bats deleted file mode 100644 index 83c1566ec9..0000000000 --- a/test/bats/push-notification-settings.bats +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bats - -load "helpers/setup-and-teardown" - -setup_file() { - clear_cache - reset_redis - - bitcoind_init - start_trigger - start_server - - initialize_user_from_onchain "$ALICE_TOKEN_NAME" "$ALICE_PHONE" "$CODE" -} - -teardown_file() { - stop_trigger - stop_server -} - - -@test "push-notification-settings: set and get" { - token_name="$ALICE_TOKEN_NAME" - - variables=$( - jq -n \ - '{input: { pushNotificationsEnabled: true, disabledPushNotificationTypes: [ "Circles" ] }}') - - exec_graphql "$token_name" 'account-update-push-notification-settings' "$variables" - - disabled_notification="$(graphql_output '.data.accountUpdatePushNotificationSettings.account.pushNotificationSettings.disabledPushNotificationTypes[0]')" - [[ "$disabled_notification" == "Circles" ]] || exit 1 -} From 3ed58ffebcf045a12af3f9251ab82d234c1fa1da Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Tue, 19 Sep 2023 21:10:06 -0500 Subject: [PATCH 12/12] chore: address pr feedback --- .../accounts/disable-notification-channel.ts | 5 +- .../accounts/enable-notification-channel.ts | 5 +- src/domain/notifications/index.ts | 36 ++++++++- test/unit/domain/notifications/index.spec.ts | 75 ++++++++++++------- 4 files changed, 85 insertions(+), 36 deletions(-) diff --git a/src/app/accounts/disable-notification-channel.ts b/src/app/accounts/disable-notification-channel.ts index ea7dfa9883..1e5eed47eb 100644 --- a/src/app/accounts/disable-notification-channel.ts +++ b/src/app/accounts/disable-notification-channel.ts @@ -1,4 +1,4 @@ -import { setNotificationChannelIsEnabled } from "@domain/notifications" +import { disableNotificationChannel as domainDisableNotificationChannel } from "@domain/notifications" import { AccountsRepository } from "@services/mongoose" export const disableNotificationChannel = async ({ @@ -11,10 +11,9 @@ export const disableNotificationChannel = async ({ const account = await AccountsRepository().findById(accountId) if (account instanceof Error) return account - const newNotificationSettings = setNotificationChannelIsEnabled({ + const newNotificationSettings = domainDisableNotificationChannel({ notificationSettings: account.notificationSettings, notificationChannel, - enabled: false, }) account.notificationSettings = newNotificationSettings diff --git a/src/app/accounts/enable-notification-channel.ts b/src/app/accounts/enable-notification-channel.ts index 761fd421e2..4843786bf7 100644 --- a/src/app/accounts/enable-notification-channel.ts +++ b/src/app/accounts/enable-notification-channel.ts @@ -1,4 +1,4 @@ -import { setNotificationChannelIsEnabled } from "@domain/notifications" +import { enableNotificationChannel as domainEnableNotificationChannel } from "@domain/notifications" import { AccountsRepository } from "@services/mongoose" export const enableNotificationChannel = async ({ @@ -11,10 +11,9 @@ export const enableNotificationChannel = async ({ const account = await AccountsRepository().findById(accountId) if (account instanceof Error) return account - const newNotificationSettings = setNotificationChannelIsEnabled({ + const newNotificationSettings = domainEnableNotificationChannel({ notificationSettings: account.notificationSettings, notificationChannel, - enabled: true, }) account.notificationSettings = newNotificationSettings diff --git a/src/domain/notifications/index.ts b/src/domain/notifications/index.ts index 71bdf88a42..5b199cb572 100644 --- a/src/domain/notifications/index.ts +++ b/src/domain/notifications/index.ts @@ -22,17 +22,45 @@ export const GaloyNotificationCategories = { } as const export const checkedToNotificationCategory = ( - type: string, + notificationCategory: string, ): NotificationCategory | ValidationError => { // TODO: add validation - if (!type) { + if (!notificationCategory) { return new InvalidNotificationSettingsError("Invalid notification category") } - return type as NotificationCategory + return notificationCategory as NotificationCategory } -export const setNotificationChannelIsEnabled = ({ +export const enableNotificationChannel = ({ + notificationSettings, + notificationChannel, +}: { + notificationSettings: NotificationSettings + notificationChannel: NotificationChannel +}): NotificationSettings => { + return setNotificationChannelIsEnabled({ + notificationSettings, + notificationChannel, + enabled: true, + }) +} + +export const disableNotificationChannel = ({ + notificationSettings, + notificationChannel, +}: { + notificationSettings: NotificationSettings + notificationChannel: NotificationChannel +}): NotificationSettings => { + return setNotificationChannelIsEnabled({ + notificationSettings, + notificationChannel, + enabled: false, + }) +} + +const setNotificationChannelIsEnabled = ({ notificationSettings, notificationChannel, enabled, diff --git a/test/unit/domain/notifications/index.spec.ts b/test/unit/domain/notifications/index.spec.ts index 00e5db7b5f..76d9d3b23f 100644 --- a/test/unit/domain/notifications/index.spec.ts +++ b/test/unit/domain/notifications/index.spec.ts @@ -1,7 +1,8 @@ import { NotificationChannel, disableNotificationCategory, - setNotificationChannelIsEnabled, + enableNotificationChannel, + disableNotificationChannel, shouldSendNotification, } from "@domain/notifications" @@ -65,7 +66,7 @@ describe("Notifications - push notification filtering", () => { }) }) - describe("setNotificationChannelIsEnabled", () => { + describe("enableNotificationChannel", () => { it("clears disabled categories when enabling a channel", () => { const notificationSettings: NotificationSettings = { push: { @@ -76,17 +77,39 @@ describe("Notifications - push notification filtering", () => { const notificationChannel = NotificationChannel.Push - const enabled = true + const result = enableNotificationChannel({ + notificationSettings, + notificationChannel, + }) + + expect(result).toEqual({ + push: { + enabled: true, + disabledCategories: [], + }, + }) + }) + }) - const result = setNotificationChannelIsEnabled({ + describe("disableNotificationChannel", () => { + it("clears disabled categories when disabling a channel", () => { + const notificationSettings: NotificationSettings = { + push: { + enabled: true, + disabledCategories: ["transaction" as NotificationCategory], + }, + } + + const notificationChannel = NotificationChannel.Push + + const result = disableNotificationChannel({ notificationSettings, notificationChannel, - enabled, }) expect(result).toEqual({ push: { - enabled, + enabled: false, disabledCategories: [], }, }) @@ -119,31 +142,31 @@ describe("Notifications - push notification filtering", () => { }, }) }) - }) - it("does not add a category to the disabled categories if it is already there", () => { - const notificationCategory = "transaction" as NotificationCategory + it("does not add a category to the disabled categories if it is already there", () => { + const notificationCategory = "transaction" as NotificationCategory - const notificationSettings: NotificationSettings = { - push: { - enabled: true, - disabledCategories: [notificationCategory], - }, - } + const notificationSettings: NotificationSettings = { + push: { + enabled: true, + disabledCategories: [notificationCategory], + }, + } - const notificationChannel = NotificationChannel.Push + const notificationChannel = NotificationChannel.Push - const result = disableNotificationCategory({ - notificationSettings, - notificationChannel, - notificationCategory, - }) + const result = disableNotificationCategory({ + notificationSettings, + notificationChannel, + notificationCategory, + }) - expect(result).toEqual({ - push: { - enabled: true, - disabledCategories: [notificationCategory], - }, + expect(result).toEqual({ + push: { + enabled: true, + disabledCategories: [notificationCategory], + }, + }) }) }) })