import type { NotifiEmitterEvents, Types, } from '@notifi-network/notifi-graphql'; import type { NotifiService } from '@notifi-network/notifi-graphql'; import { type NotifiFrontendConfiguration } from '../configuration'; import { NotifiError } from '../errors'; import { type CardConfigItemV1, type FusionEventTopic, type TenantConfig, type TenantConfigV2, type TopicMetadata, } from '../models'; import type { NotifiStorage } from '../storage'; import { notNullOrEmpty, parseTenantConfig, resolveStringRef } from '../utils'; import { areIdsEqual } from '../utils/areIdsEqual'; import { type AlterTargetGroupParams, alterTargetGroupImpl, } from './alterTargetGroup'; import { AuthManager, LoginParams } from './auth'; import { ensureDiscord, ensureEmail, ensureSlack, ensureSms, ensureTelegram, ensureWeb3, renewTelegram, } from './deprecated'; /**@deprecated use CardConfigItemV2 */ export type CardConfigType = CardConfigItemV1; type BeginLoginProps = Omit; type CompleteLoginProps = Omit< Types.CompleteLogInByTransactionInput, 'dappAddress' | 'randomUuid' >; type FindSubscriptionCardParams = Omit; export class NotifiFrontendClient { private _authManager: AuthManager; constructor( private _configuration: NotifiFrontendConfiguration, private _service: NotifiService, private _storage: NotifiStorage, ) { this._authManager = new AuthManager( this._service, this._storage, this._configuration, ); } get auth(): AuthManager { return this._authManager; } async fetchFusionData(): Promise { return this._service.fetchFusionData({}); } async getTargetGroups(): Promise< ReadonlyArray > { const query = await this._service.getTargetGroups({}); const results = query.targetGroup?.filter(notNullOrEmpty) ?? []; return results; } async getAlerts(): Promise> { const query = await this._service.getAlerts({}); return query.alert?.filter(notNullOrEmpty) ?? []; } async ensureFusionAlerts( input: Types.CreateFusionAlertsInput, ): Promise { const inputAlertsNames = new Set(input.alerts.map((alert) => alert.name)); const query = await this._service.getAlerts({}); const existingAlerts = new Set(query.alert); const duplicateAlerts = [...existingAlerts].filter((alert) => inputAlertsNames.has(alert?.name), ); const duplicateAlertsIds = duplicateAlerts .map((alert) => alert?.id) .filter((id): id is string => !!id); /* Alerts are immutable, delete the existing instead */ const alertIdsToDelete = duplicateAlertsIds.map((id) => id); if (alertIdsToDelete.length > 0) { await this.deleteAlerts({ ids: alertIdsToDelete }); } const mutation = await this._service.createFusionAlerts({ input }); NotifiError.throwIfPayloadError(mutation.createFusionAlerts); return mutation.createFusionAlerts; } async deleteAlerts({ ids, }: Readonly<{ ids: Array; }>): Promise { const mutation = await this._service.deleteAlerts({ input: { alertIds: ids }, }); NotifiError.throwIfPayloadError(mutation.deleteAlerts); const result = mutation.deleteAlerts; if (result === undefined) { throw new Error('Failed to delete alerts'); } return result; } async getUnreadNotificationHistoryCount( cardId?: string, ): Promise< Types.GetUnreadNotificationHistoryCountQuery['unreadNotificationHistoryCount'] > { const query = await this._service.getUnreadNotificationHistoryCount({ cardId, }); const result = query.unreadNotificationHistoryCount; if (!result) { throw new Error('Failed to fetch unread notification history count'); } return result; } /** * @returns {string} - The id of the event listener (used to remove the event listener) */ addEventListener( event: K, callBack: (...args: NotifiEmitterEvents[K]) => void, onError?: (error: unknown) => void, onCompleted?: () => void, ): string { return this._service.addEventListener( event, callBack, onError, onCompleted, ); } removeEventListener( event: K, id: string, ) { return this._service.removeEventListener(event, id); } async getUserSettings(): Promise { const query = await this._service.getUserSettings({}); const result = query.userSettings; if (!result) { throw new Error('Failed to fetch user settings'); } return result; } async getFusionNotificationHistory( variables: Types.GetFusionNotificationHistoryQueryVariables, ): Promise< Readonly< Types.GetFusionNotificationHistoryQuery['fusionNotificationHistory'] > > { const query = await this._service.getFusionNotificationHistory(variables); const nodes = query.fusionNotificationHistory?.nodes; const pageInfo = query.fusionNotificationHistory?.pageInfo; if (nodes === undefined || pageInfo === undefined) { throw new Error('Failed to fetch notification history'); } return { pageInfo, nodes }; } async fetchTenantConfig( variables: FindSubscriptionCardParams, ): Promise { const query = await this._service.findTenantConfig({ input: { ...variables, tenant: this._configuration.tenantId, }, }); const result = query.findTenantConfig; if (result === undefined || !result.dataJson || !result.fusionEvents) { throw new Error('Failed to find tenant config'); } const tenantConfigJsonString = result.dataJson; if (tenantConfigJsonString === undefined) { throw new Error('Invalid config data'); } const tenantConfig = parseTenantConfig(tenantConfigJsonString); const fusionEventDescriptors = result.fusionEvents; if (!fusionEventDescriptors) throw new Error('fusionEventDescriptors not found'); const fusionEventDescriptorMap = new Map< string, Types.FusionEventDescriptor >(fusionEventDescriptors.map((item) => [item?.id ?? '', item ?? {}])); fusionEventDescriptorMap.delete(''); const topicMetadatas = tenantConfig.eventTypes.map((eventType) => { if (eventType.type === 'fusion') { const fusionEventDescriptor = fusionEventDescriptorMap.get( typeof eventType.fusionEventId === 'string' ? eventType.fusionEventId : resolveStringRef( 'fetchTenantConfig: Lagacy event - CardConfigItemV1', eventType.fusionEventId, {}, ), ); return { uiConfig: eventType, fusionEventDescriptor, }; } }); if (tenantConfig.version === 'v1') { // V1 deprecated const topicMetadatasV1 = topicMetadatas.filter( (item): item is FusionEventTopic => !!item, ); return { cardConfig: tenantConfig, // NOTE: cardConfig is legacy naming of tenantConfig fusionEventTopics: topicMetadatasV1, }; } // V2 const topicMetadatasV2 = topicMetadatas.filter( (item): item is TopicMetadata => !!item, ); return { cardConfig: tenantConfig, // NOTE: cardConfig is legacy naming of tenantConfig fusionEventTopics: topicMetadatasV2, }; } async sendEmailTargetVerification({ targetId, }: Readonly<{ targetId: string }>): Promise { const emailTarget = await this._service.sendEmailTargetVerificationRequest({ targetId, }); const id = emailTarget.sendEmailTargetVerificationRequest?.id; if (id === undefined) { throw new Error(`Unknown error requesting verification`); } return id; } async createDiscordTarget(input: string) { const mutation = await this._service.createDiscordTarget({ name: input, value: input, }); return mutation.createDiscordTarget; } async markFusionNotificationHistoryAsRead( input: Types.MarkFusionNotificationHistoryAsReadMutationVariables, ): Promise { const mutation = await this._service.markFusionNotificationHistoryAsRead(input); return mutation; } async updateUserSettings( input: Types.UpdateUserSettingsMutationVariables, ): Promise { const mutation = await this._service.updateUserSettings(input); return mutation; } async verifyCbwTarget( input: Types.VerifyCbwTargetMutationVariables, ): Promise { const mutation = await this._service.verifyCbwTarget(input); NotifiError.throwIfPayloadError(mutation.verifyCbwTarget); return mutation; } async verifyXmtpTargetViaXip42( input: Types.VerifyXmtpTargetViaXip42MutationVariables, ): Promise { const mutation = await this._service.verifyXmtpTargetViaXip42(input); NotifiError.throwIfPayloadError(mutation.verifyXmtpTargetViaXip42); return mutation; } async createWebPushTarget( input: Types.CreateWebPushTargetMutationVariables, ): Promise { const mutation = await this._service.createWebPushTarget(input); NotifiError.throwIfPayloadError(mutation.createWebPushTarget); return mutation; } async updateWebPushTarget( input: Types.UpdateWebPushTargetMutationVariables, ): Promise { const mutation = await this._service.updateWebPushTarget(input); NotifiError.throwIfPayloadError(mutation.updateWebPushTarget); return mutation; } async deleteWebPushTarget( input: Types.DeleteWebPushTargetMutationVariables, ): Promise { const mutation = await this._service.deleteWebPushTarget(input); NotifiError.throwIfPayloadError(mutation.deleteWebPushTarget); return mutation; } async getWebPushTargets( input: Types.GetWebPushTargetsQueryVariables, ): Promise { const query = await this._service.getWebPushTargets(input); const result = query.webPushTargets; if (!result) { throw new Error('Failed to fetch webpush targets'); } return result; } async alterTargetGroup( alterTargetGroupParams: AlterTargetGroupParams, ): Promise { return await alterTargetGroupImpl(alterTargetGroupParams, this._service); } /** * ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ * ⬇ ⬇ Deprecated methods ⬇ ⬇ * ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ ⬇ */ /** @deprecated Use renewTargetGroup instead */ async ensureTargetGroup({ name, emailAddress, phoneNumber, telegramId, discordId, slackId, walletId, }: Readonly<{ name: string; emailAddress?: string; phoneNumber?: string; telegramId?: string; discordId?: string; slackId?: string; walletId?: string; }>): Promise { const [ targetGroupsQuery, emailTargetId, smsTargetId, telegramTargetId, discordTargetId, slackTargetId, web3TargetId, ] = await Promise.all([ this._service.getTargetGroups({}), ensureEmail(this._service, emailAddress), ensureSms(this._service, phoneNumber), ensureTelegram(this._service, telegramId), ensureDiscord(this._service, discordId), ensureSlack(this._service, slackId), ensureWeb3(this._service, walletId), ]); const emailTargetIds = emailTargetId === undefined ? [] : [emailTargetId]; const smsTargetIds = smsTargetId === undefined ? [] : [smsTargetId]; const telegramTargetIds = telegramTargetId === undefined ? [] : [telegramTargetId]; const discordTargetIds = discordTargetId === undefined ? [] : [discordTargetId]; const slackChannelTargetIds = slackTargetId === undefined ? [] : [slackTargetId]; const web3TargetIds = web3TargetId === undefined ? [] : [web3TargetId]; const existing = targetGroupsQuery.targetGroup?.find( (it) => it?.name === name, ); if (existing !== undefined) { return this._updateTargetGroup({ existing, emailTargetIds, smsTargetIds, telegramTargetIds, discordTargetIds, slackChannelTargetIds, web3TargetIds, }); } const createMutation = await this._service.createTargetGroup({ name, emailTargetIds, smsTargetIds, telegramTargetIds, discordTargetIds, slackChannelTargetIds, web3TargetIds, }); if (createMutation.createTargetGroup === undefined) { throw new Error('Failed to create target group'); } return createMutation.createTargetGroup; } /** * @deprecated use alterTargetGroup instead * @description !IMPORTANT: the id arguments (telegramId, discordId, slackId, walletId) is the self-defined identity (only within notifi BE). This is NEITHER the user name NOR the user id of associated platform. */ async renewTargetGroup({ name, emailAddress, phoneNumber, telegramId, discordId, slackId, walletId, }: Readonly<{ name: string; emailAddress?: string; phoneNumber?: string; telegramId?: string; discordId?: string; slackId?: string; walletId?: string; }>): Promise { const [ targetGroupsQuery, emailTargetId, smsTargetId, telegramTargetId, discordTargetId, slackTargetId, web3TargetId, ] = await Promise.all([ this._service.getTargetGroups({}), ensureEmail(this._service, emailAddress), ensureSms(this._service, phoneNumber), renewTelegram(this._service, telegramId), ensureDiscord(this._service, discordId), ensureSlack(this._service, slackId), ensureWeb3(this._service, walletId), ]); const emailTargetIds = emailTargetId === undefined ? [] : [emailTargetId]; const smsTargetIds = smsTargetId === undefined ? [] : [smsTargetId]; const telegramTargetIds = telegramTargetId === undefined ? [] : [telegramTargetId]; const discordTargetIds = discordTargetId === undefined ? [] : [discordTargetId]; const slackChannelTargetIds = slackTargetId === undefined ? [] : [slackTargetId]; const web3TargetIds = web3TargetId === undefined ? [] : [web3TargetId]; const existing = targetGroupsQuery.targetGroup?.find( (it) => it?.name === name, ); if (existing !== undefined) { return this._updateTargetGroup({ existing, emailTargetIds, smsTargetIds, telegramTargetIds, discordTargetIds, slackChannelTargetIds, web3TargetIds, }); } const createMutation = await this._service.createTargetGroup({ name, emailTargetIds, smsTargetIds, telegramTargetIds, discordTargetIds, slackChannelTargetIds, web3TargetIds, }); if (createMutation.createTargetGroup === undefined) { throw new Error('Failed to create target group'); } return createMutation.createTargetGroup; } /** * @deprecated all consumers (ensureTargetGroup) are deprecated */ private async _updateTargetGroup({ existing, emailTargetIds, smsTargetIds, telegramTargetIds, discordTargetIds, slackChannelTargetIds, web3TargetIds, }: Readonly<{ existing: Types.TargetGroupFragmentFragment; emailTargetIds: Array; smsTargetIds: Array; telegramTargetIds: Array; discordTargetIds: Array; slackChannelTargetIds: Array; web3TargetIds: Array; }>): Promise { if ( areIdsEqual(emailTargetIds, existing.emailTargets ?? []) && areIdsEqual(smsTargetIds, existing.smsTargets ?? []) && areIdsEqual(telegramTargetIds, existing.telegramTargets ?? []) && areIdsEqual(discordTargetIds, existing.discordTargets ?? []) && areIdsEqual(slackChannelTargetIds, existing.slackChannelTargets ?? []) && areIdsEqual(web3TargetIds, existing.web3Targets ?? []) ) { return existing; } const updateMutation = await this._service.updateTargetGroup({ id: existing.id, name: existing.name ?? existing.id, emailTargetIds, smsTargetIds, telegramTargetIds, discordTargetIds, slackChannelTargetIds, web3TargetIds, }); const updated = updateMutation.updateTargetGroup; if (updated === undefined) { throw new Error('Failed to update target group'); } return updated; } /** * @deprecated Use `deleteAlerts` instead */ async deleteAlert({ id, }: Readonly<{ id: string; }>): Promise { const mutation = await this._service.deleteAlert({ id }); const result = mutation.deleteAlert?.id; if (result === undefined) { throw new Error('Failed to delete alert'); } } /**@deprecated Use frontendClient.auth.userState instead */ get userState() { return this._authManager.userState; } /**@deprecated Use frontendClient.auth.initialize instead */ async initialize() { return await this._authManager.initialize(); } /**@deprecated Use frontendClient.auth.logOut instead */ async logOut() { return await this._authManager.logOut(); } /**@deprecated Use frontendClient.auth.logIn instead */ async logIn(loginParams: LoginParams) { const user = await this._authManager.logIn(loginParams); return user; } /**@deprecated Use frontendClient.auth.beginLoginViaTransaction instead */ async beginLoginViaTransaction(props: BeginLoginProps) { return this._authManager.beginLoginViaTransaction(props); } /**@deprecated Use frontendClient.auth.completeLoginViaTransaction instead */ async completeLoginViaTransaction(props: CompleteLoginProps) { return this._authManager.completeLoginViaTransaction(props); } }