Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified packages/bitbadgesjs-sdk/bun.lockb
Binary file not shown.
112 changes: 112 additions & 0 deletions packages/bitbadgesjs-sdk/src/api-indexer/BitBadgesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ import {
SignOutSuccessResponse,
SimulateClaimSuccessResponse,
SimulateTxSuccessResponse,
GetNotificationsSuccessResponse,
GetUnreadNotificationCountSuccessResponse,
MarkNotificationsReadSuccessResponse,
UpdateNotificationPreferencesSuccessResponse,
UpdateAccountInfoSuccessResponse,
UpdateClaimSuccessResponse,
UpdateDeveloperAppSuccessResponse,
Expand Down Expand Up @@ -310,6 +314,14 @@ import {
iSimulateClaimSuccessResponse,
iSimulateTxPayload,
iSimulateTxSuccessResponse,
iGetNotificationsPayload,
iGetNotificationsSuccessResponse,
iGetUnreadNotificationCountPayload,
iGetUnreadNotificationCountSuccessResponse,
iMarkNotificationsReadPayload,
iMarkNotificationsReadSuccessResponse,
iUpdateNotificationPreferencesPayload,
iUpdateNotificationPreferencesSuccessResponse,
iUpdateAccountInfoPayload,
iUpdateAccountInfoSuccessResponse,
iUpdateClaimPayload,
Expand Down Expand Up @@ -3428,6 +3440,106 @@ export class BitBadgesAdminAPI<T extends NumberType> extends BitBadgesAPI<T> {
}
}

/**
* Gets the signed-in user's in-app notifications (inbox), newest first.
*
* **API Route**: `GET /api/v0/notifications`
*
* **Authentication**: Must be signed in.
*/
public async getNotifications(payload?: iGetNotificationsPayload): Promise<GetNotificationsSuccessResponse<T>> {
try {
const validateRes: typia.IValidation<iGetNotificationsPayload> = typia.validate<iGetNotificationsPayload>(payload ?? {});
if (!validateRes.success) {
throw new Error('Invalid payload: ' + JSON.stringify(validateRes.errors));
}

const response = await this.axios.get<iGetNotificationsSuccessResponse<string>>(
`${this.BACKEND_URL}${BitBadgesApiRoutes.GetNotificationsRoute()}`,
{ params: payload }
);
return new GetNotificationsSuccessResponse(response.data).convert(this.ConvertFunction);
} catch (error) {
await this.handleApiError(error);
return Promise.reject(error);
}
}

/**
* Gets the signed-in user's unread notification count.
*
* **API Route**: `GET /api/v0/notifications/unreadCount`
*
* **Authentication**: Must be signed in.
*/
public async getUnreadNotificationCount(
payload?: iGetUnreadNotificationCountPayload
): Promise<GetUnreadNotificationCountSuccessResponse> {
try {
const response = await this.axios.get<iGetUnreadNotificationCountSuccessResponse>(
`${this.BACKEND_URL}${BitBadgesApiRoutes.GetUnreadNotificationCountRoute()}`,
{ params: payload ?? {} }
);
return new GetUnreadNotificationCountSuccessResponse(response.data);
} catch (error) {
await this.handleApiError(error);
return Promise.reject(error);
}
}

/**
* Marks notifications as read/unread for the signed-in user.
*
* **API Route**: `POST /api/v0/notifications/read`
*
* **Authentication**: Must be signed in.
*/
public async markNotificationsRead(payload: iMarkNotificationsReadPayload): Promise<MarkNotificationsReadSuccessResponse> {
try {
const validateRes: typia.IValidation<iMarkNotificationsReadPayload> = typia.validate<iMarkNotificationsReadPayload>(payload ?? {});
if (!validateRes.success) {
throw new Error('Invalid payload: ' + JSON.stringify(validateRes.errors));
}

const response = await this.axios.post<iMarkNotificationsReadSuccessResponse>(
`${this.BACKEND_URL}${BitBadgesApiRoutes.MarkNotificationsReadRoute()}`,
payload
);
return new MarkNotificationsReadSuccessResponse(response.data);
} catch (error) {
await this.handleApiError(error);
return Promise.reject(error);
}
}

/**
* Updates the signed-in user's in-app notification preferences.
*
* **API Route**: `POST /api/v0/notifications/preferences`
*
* **Authentication**: Must be signed in.
*/
public async updateNotificationPreferences(
payload: iUpdateNotificationPreferencesPayload
): Promise<UpdateNotificationPreferencesSuccessResponse> {
try {
const validateRes: typia.IValidation<iUpdateNotificationPreferencesPayload> =
typia.validate<iUpdateNotificationPreferencesPayload>(payload ?? ({} as iUpdateNotificationPreferencesPayload));
if (!validateRes.success) {
throw new Error('Invalid payload: ' + JSON.stringify(validateRes.errors));
}

const response = await this.axios.post<iUpdateNotificationPreferencesSuccessResponse>(
`${this.BACKEND_URL}${BitBadgesApiRoutes.UpdateNotificationPreferencesRoute()}`,
payload
);
return new UpdateNotificationPreferencesSuccessResponse(response.data);
} catch (error) {
await this.handleApiError(error);
return Promise.reject(error);
}
}

/**
* A generic route for verifying SIWBB requests. Used as a helper if implementing on your own.
*
Expand Down
55 changes: 55 additions & 0 deletions packages/bitbadgesjs-sdk/src/api-indexer/docs-types/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BaseNumberTypeClass, CustomTypeClass, convertClassPropertiesAndMaintain
import type { NumberType } from '@/common/string-numbers.js';
import type { SupportedChain } from '@/common/types.js';
import { AddressList } from '@/core/addressLists.js';
import { TransferActivityDoc } from '@/api-indexer/docs-types/activity.js';
import {
ApprovalInfoDetails,
ChallengeDetails,
Expand Down Expand Up @@ -79,6 +80,10 @@ import {
type iEmailVerificationStatus,
type iFetchDoc,
type iIPFSTotalsDoc,
type iNotificationDoc,
type iNotificationPayload,
type iTransferActivityDoc,
type NotificationType,
type iLatestBlockStatus,
type iMerkleChallengeTrackerDoc,
type iNotificationPreferences,
Expand Down Expand Up @@ -472,6 +477,9 @@ export class NotificationPreferences<T extends NumberType>
claimActivity?: boolean;
ignoreIfInitiator?: boolean;
signInAlertsEnabled?: boolean;
inAppEnabled?: boolean;
inAppTransferActivity?: boolean;
inAppClaimActivity?: boolean;
};

constructor(data: iNotificationPreferences<T>) {
Expand All @@ -487,6 +495,53 @@ export class NotificationPreferences<T extends NumberType>
}
}

/**
* @inheritDoc iNotificationDoc
* @category Indexer
*/
export class NotificationDoc<T extends NumberType> extends BaseNumberTypeClass<NotificationDoc<T>> implements iNotificationDoc<T> {
_docId: string;
_id?: string;
bitbadgesAddress: BitBadgesAddress;
type: NotificationType;
read: boolean;
createdAt: T;
title: string;
message?: string;
link?: string;
collectionId?: CollectionId;
address?: BitBadgesAddress;
payload?: iNotificationPayload<T>;

constructor(data: iNotificationDoc<T>) {
super();
this._docId = data._docId;
this._id = data._id;
this.bitbadgesAddress = data.bitbadgesAddress;
this.type = data.type;
this.read = data.read;
this.createdAt = data.createdAt;
this.title = data.title;
this.message = data.message;
this.link = data.link;
this.collectionId = data.collectionId;
this.address = data.address;
// Rebuild the embedded activity as a real class instance so convert() recurses into
// its number fields; the rest of the payload is display-ready and passes through as-is.
this.payload = data.payload
? { ...data.payload, activity: data.payload.activity ? new TransferActivityDoc(data.payload.activity) : undefined }
: undefined;
}

getNumberFieldNames(): string[] {
return ['createdAt'];
}

convert<U extends NumberType>(convertFunction: (item: NumberType) => U, options?: ConvertOptions): NotificationDoc<U> {
return convertClassPropertiesAndMaintainNumberTypes(this, convertFunction, options) as NotificationDoc<U>;
}
}

/**
* @inheritDoc iEmailVerificationStatus
* @category Accounts
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './activity.js';
export * from './docs.js';
export * from './interfaces.js';
export * from './websockets.js';
118 changes: 103 additions & 15 deletions packages/bitbadgesjs-sdk/src/api-indexer/docs-types/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,109 @@ export interface iNotificationPreferences<T extends NumberType> {
claimActivity?: boolean;
ignoreIfInitiator?: boolean;
signInAlertsEnabled?: boolean;
/** Master toggle for the in-app notification inbox. Defaults to enabled when undefined. */
inAppEnabled?: boolean;
/** In-app inbox: receive transfer notifications. Defaults to enabled when undefined. */
inAppTransferActivity?: boolean;
/** In-app inbox: receive claim notifications. Defaults to enabled when undefined. */
inAppClaimActivity?: boolean;
};
}

/**
* The in-app notification categories (inbox event types). This is the discriminator
* for {@link iNotificationDoc} — every type shares the same presentation envelope and
* carries its own structured detail in {@link iNotificationPayload}. New event types
* (e.g. swaps, votes) are added here + given a `payload` shape + a frontend renderer.
*
* @category Interfaces
*/
export type NotificationType = 'transfer' | 'bank_send' | 'claim' | 'points' | 'list' | 'system' | 'intent_satisfied';

/**
* A coin (denom) amount, as display-ready strings (no number-type conversion needed).
*
* @category Interfaces
*/
export interface iNotificationCoin {
amount: string;
denom: string;
}

/**
* Type-specific detail for a notification's drilldown view. The populated key is
* determined by {@link iNotificationDoc.type}; renderers narrow on `type`. This is the
* extension point: a new notification type adds its structured detail here.
*
* @category Interfaces
*/
export interface iNotificationPayload<T extends NumberType> {
/** `transfer`: the source on-chain token transfer activity (embedded for rich rendering). */
activity?: iTransferActivityDoc<T>;
/** `bank_send`: a native coin (denom) transfer. */
bankSend?: {
fromAddress: BitBadgesAddress;
toAddress: BitBadgesAddress;
amount: iNotificationCoin[];
txHash?: string;
};
/** `intent_satisfied`: a standing intent (approval) was filled — multiple on-chain legs collapsed into one event. */
intentSatisfied?: {
/** The intent approval that was consumed. */
approvalId: string;
/** Who set up the intent (the maker / "my intent was satisfied" recipient). */
approverAddress: BitBadgesAddress;
/** Who filled the intent (the taker / initiator). */
filler: BitBadgesAddress;
collectionId: CollectionId;
txHash?: string;
/** Coins that moved as part of the fill (payment/payout), display-ready. */
coins: iNotificationCoin[];
/** Compact summary of token IDs involved, e.g. "1-3, 7". */
tokenIds?: string;
/** How many on-chain transfer legs were collapsed into this single notification. */
legCount: number;
};
/** Misc display-ready scalars for any type (tokenIds, claimId, points, …). */
extra?: Record<string, string>;
}

/**
* An in-app notification (inbox entry). One document per recipient per event.
*
* Shape = a **standardized envelope** (rendered identically for every type in the inbox
* list) + a **type-specific `payload`** surfaced only in the drilldown. `type` is the
* discriminator (see {@link NotificationType}).
*
* @category Interfaces
*/
export interface iNotificationDoc<T extends NumberType> extends Doc {
/** The recipient of this notification. */
bitbadgesAddress: BitBadgesAddress;
/** The category of the notification — the discriminator for `payload` + rendering. */
type: NotificationType;
/** Whether the recipient has read this notification. */
read: boolean;
/** When the notification was created (UNIX ms). */
createdAt: UNIXMilliTimestamp<T>;

// --- Standardized presentation envelope (consistent across all types) ---
/** Headline for the inbox row. */
title: string;
/** Optional one-line body for the inbox row. */
message?: string;
/** Optional in-app link to navigate to (e.g. "view source"). */
link?: string;
/** Subject for the row's leading avatar: a collection (token avatar). */
collectionId?: CollectionId;
/** Subject for the row's leading avatar: an address (user avatar / counterparty). */
address?: BitBadgesAddress;

// --- Type-specific drilldown detail (shape determined by `type`) ---
/** Structured detail surfaced when the row is expanded/drilled into. */
payload?: iNotificationPayload<T>;
}

/**
* The verification status of the user's email.
*
Expand Down Expand Up @@ -618,7 +718,6 @@ export interface iProfileDoc<T extends NumberType> extends Doc {

/** The notifications of the account */
notifications?: iNotificationPreferences<T>;

}

/**
Expand Down Expand Up @@ -793,14 +892,7 @@ export interface iBalanceDocWithDetails<T extends NumberType> extends iBalanceDo
/**
* @category Claims
*/
export type ClaimIntegrationPluginType =
| 'codes'
| 'password'
| 'numUses'
| 'transferTimes'
| 'initiatedBy'
| 'whitelist'
| string;
export type ClaimIntegrationPluginType = 'codes' | 'password' | 'numUses' | 'transferTimes' | 'initiatedBy' | 'whitelist' | string;

/**
* @category Claims
Expand Down Expand Up @@ -1678,16 +1770,12 @@ export type DynamicDataHandlerType = 'addresses';
/**
* @category Interfaces
*/
export type DynamicDataHandlerData<Q extends DynamicDataHandlerType> = Q extends 'addresses'
? { addresses: string[] }
: never;
export type DynamicDataHandlerData<Q extends DynamicDataHandlerType> = Q extends 'addresses' ? { addresses: string[] } : never;

/**
* @category Interfaces
*/
export type DynamicDataHandlerActionPayload<Q extends DynamicDataHandlerType> = Q extends 'addresses'
? { address: string }
: never;
export type DynamicDataHandlerActionPayload<Q extends DynamicDataHandlerType> = Q extends 'addresses' ? { address: string } : never;

/**
* @category Interfaces
Expand Down
Loading
Loading