SPIKE: notifi-sdk-ts consolidation
Background
Currently, The way the following 3 packages interact with each other is like the flowchart below, which is complicated and hard to maintain.
- notifi-react-hook
- notifi-frontend-client
- notifi-react-card
Two key points:
The notifi-react-hook is difficult to be consumed as a standalone package since it is tightly coupled with the 'notifi-react-card'.
The notifi-react-card has many complex logic to handle 'Service logic' inside its useSubscribe hook.
Goal
To solve the problem, consolidating notifi-sdk-ts is needed so that the packages' interaction should become simple like below:
Actions
Action | Description | point |
---|---|---|
Consolidate notifi-react-hook (useNotifiClient) and notifi-frontend-client (NotifiFrontendClient) | See breakdown below | 3 |
Extend FrontendClient to fully support all available chains and event types | See breakdown below | 3 |
Move the logic of useNotifiSubscribe (notifi-react-card) to notifi-frontend-client | See breakdown below | 5 |
Implementation Details
1. Consolidate notifi-react-hooks
and notifi-frontend-client
Since notifi-react-card
will finally use FrontendClient
(notifi-frontend-client) to manipulate service instead of using useNotifiClient
(notifi-react-hooks), making sure the FrontendClient
is able to cover all the functionalities of useNotifiClient
is the first step.
Step1: Implement getter to get the userState
The user state useNotifiSubscribe
and useNotifiClient
below need to be handled somehow.
isAuthenticated
isInitialized
expiry
isTokenExpired
Need to implement a getter in FrontendClient
export class NotifiFrontendClient {
// ...
private _userState: UserState | null = null;
get userState(): UserState | null {
return this._userState;
}
// ...
async initialize(): Promise<UserState> {
// ...
this._service.setJwt(authorization.token);
const userState = {
status: "authenticated",
authorization,
roles: roles ?? [],
};
this._userState = userState;
return userState;
return {
status: "authenticated",
authorization,
roles: roles ?? [],
};
}
}
So we can replace the previous isInitialized
with the following example code:
// ...
const { isInitialized } = useNotifiSubscribe({ targetGroupName: "Default" });
const { userState } = client;
const isInitialized = !!userState;
// ...
Step#2: Consolidate useNotifiClient hook and NotifiFrontendClient object
The following now methods will need to be implemented in FrontendClient
- userState
- getConversationMessages
- sendConversationMessages
- createSupportConversation
See detailed info below
useNotifiClient hook v.s. NotifiFrontendClient object
Info
createAlert is used updateAlertInternal. After making sure the the source is valid, the createAlert will be called. It is included in ensureAlert in frontendClient. So createAlert can be deprecated.
Used in updateAlertInternal, and the updateAlertInternal is used in subscribe. And in subscribe, it iterates through all existing alerts to make sure if the alert to subscribe is valid. It is the same as ensureAlert. So createSource can be deprecated.
In hook implementation (useNotifiClient), we firstly ensure every single source (utils/ensureSource). And then go for ensuring sourceGroup (utils/ensureSourceGroup). But in frontendClient, we only need to use ensureSourceAndFilters(frontend-client/ensureSource.ts). ensureSourceGroup can be deprecated.
fetchData
in useNotifiClient is used inuseNotifiSubscribe
hook to render the client info shown in the cards. We will need to implement a new method inFrontendClient
to replace this one.Used inside subscribeWallet method in useNotifiSubscribe. It allows user to subscribe multiWallet if the dapp tenant enable multiWallet subscription. NotifiFrontendClient needs to impl new subscribeWallet method to replace this one as well as the
subscribeWallet
in useNotifiSubscribe hook.Used inside
useItercomChat
hook. Might need to simply implgetConversationMessages
&sendConversationMessages
stateless method inFrontendClient
. (TBD)Used inside
updateAlertInternal
which is used insubscribe
method in useNotifiSubscribe.subscribe
method can be replaced withFrontendClient.ensureAlert
method. SoupdateAlert
can be deprecated.Used inside
IntercomCard
as a button handler. Might need to simply implcreateSupportConversation
stateless method inFrontendClient
. (TBD)Used in
subscribe
method in useNotifiSubscribe.subscribe
method can be replaced withFrontendClient.ensureAlert
method. SocreateDiscordTarget
can be deprecated.Used in
resendEmailVerificationLink
method inuseNotifiSubscribe
. NotifiFrontendClient has the sendEmailTargetVerification method which will cover bothresendEmailVerificationLink
in useNotifiSubscribe andsendEmailTargetVerification
in useNotifiClient hook.
Step#3: Consolidate useNotifiSubscribe hook and NotifiFrontendClient object
We need to extract the logic of useNotifiSubscribe
hook in notifi-react-card
to NotifiFrontendClient
object in notifi-frontend-client
package.
So the 2nd step will be implementing the following new methods into FrontendClient
- subscribeWallet
- updateWallets
See detailed info below
useNotifiSubscribe hook v.s. NotifiFrontendClient object
FilterOptions.ts
It is copy-paste from core
, consider remove
./packages/notifi-frontend-client/lib/models/FilterOptions.ts
packages/notifi-core/lib/NotifiClient.ts
2. Extend FrontendClient
to fully support all available chains and event types
Step#1: Make all supported event available in notifi-frontend-client
Now, notifi-frontend-client
only supports:
export type EventTypeItem =
| DirectPushEventTypeItem
| BroadcastEventTypeItem
| LabelEventTypeItem
| PriceChangeEventTypeItem
| CustomTopicTypeItem; // Including HealthCheckEventTypeItem
But in notifi-react-card
, we have:
export type EventTypeItem =
| DirectPushEventTypeItem
| BroadcastEventTypeItem
| HealthCheckEventTypeItem
| LabelEventTypeItem
| TradingPairEventTypeItem
| WalletBalanceEventTypeItem
| PriceChangeEventTypeItem
| CustomTopicTypeItem
| XMTPTopicTypeItem;
- WalletBalanceEventTypeItem
- TradingPairEventTypeItem
- XMTPTopicTypeItem;
Related files
packages/notifi-frontend-client/lib/models/SubscriptionCardConfig.ts
--> Add newXXXEventTypeItem
intoEventTypeItem
packages/notifi-frontend-client/lib/client/ensureSource.ts
- Add new
ensureXXXSource(s)
method and implement intoensureSources
used inensureSourceAndFilters
. - Add new
getXXXSourceFilter
method and implement intoensureSourceAndFilters
- Add new
packages/notifi-frontend-client/lib/client/NotifiFrontendClient.ts
Step#2 Make all supported chains available in notifi-frontend-client
Currently, we only have APTOS
, EVM
and SOLANA
supported in notifi-frontend-client
.
We need to add the following 4 supported chains.
- ACALA
- SUI
- NEAR
- INJECTIVE
- Add new NotifiXXXConfiguration type
export type NotifiFrontendConfiguration = NotifiSolanaConfiguration | NotifiAptosConfiguration;
export type NotifiAptosConfiguration = Readonly<{
walletBlockchain: 'APTOS';
authenticationKey: string;
accountAddress: string;
}> &
NotifiEnvironmentConfiguration;
export const newAptosConfig =
// ...
export type NotifiSolanaConfiguration = Readonly<{
walletBlockchain: "SOLANA";
walletPublicKey: string;
}> &
NotifiEnvironmentConfiguration;
export const newSolanaConfig =
// ...
// Need to add the reset of the chains
- Implement signedMessage in
NotifiFrontendClient
for associated chains
private async _signMessage({
signMessageParams,
timestamp,
}: Readonly<{
signMessageParams: SignMessageParams;
timestamp: number;
}>): Promise<string> {
switch (signMessageParams.walletBlockchain) {
case 'ETHEREUM':
case 'POLYGON':
case 'ARBITRUM':
case 'AVALANCHE':
case 'BINANCE':
case 'OPTIMISM': {
// ...
}
case 'SOLANA': {
// ...
}
case 'APTOS': {
// ...
}
/*
** TODO: Need to implement the rest of the chains
** 1. ACALA
** 2. SUI
** 3. NEAR
** 4. INJECTIVE
*/
default:
// Need implementation for other blockchains
return 'Chain not yet supported';
}
}
- Storage prefix setting (TBC)
There is conflict between notifi-frontend-client
and notifi-react-hooks
on the storage prefix. TBC
- notifi-react-hooks: There s no blockchain specific prefix. all set as
${jwtPrefix}:${dappAddress}:${walletPublicKey}
- notifi-frontend-client: Blockchain specific prefix is set. Like
// For APTOS
${jwtPrefix}:${dappAddress}:${accountAddress}:${walletPublicKey}
// Rest
${jwtPrefix}:${dappAddress}:${walletPublicKey}
- Add newbclientFactory for newly added chains
// ...
export const newXXXClient = (config: NotifiXXXConfiguration): NotifiFrontendClient => {
const storage = newNotifiStorage(config);
const service = newNotifiService(config);
return new NotifiFrontendClient(config, storage, service);
};
3. Move the logic of useNotifiSubscribe (notifi-react-card) to notifi-frontend-client
Step#1 Replace the useNotifiClient
dependency with FrontendClient
object.
export const NotifiClientContextProvider: React.FC<NotifiParams> = ({
children,
...params
}: React.PropsWithChildren<NotifiParams>) => {
const client = useNotifiClient(params);
const config: NotifiFrontendConfiguration = "conditional check walletBlcokchain"; // use newXXXconfig to create config
const storage = useMemo(()=> newNotifiStorage(config), [config]);
const service = useMemo (()=> newNotifiService(config), [config]);
const client = useMemo(() => new FrontendClient(params, storage, service), [ storage, service]));
return (
<NotifiClientContext.Provider value={{ client, params }}>
{children}
</NotifiClientContext.Provider>
);
};
Step#2 Use client to replace the methods imported from useNotifiSubscribe
We need to replace the methods imported from useNotifiSubscribe
with FrontendClient
object provided by context. The following components are using useNotifiSubscribe
:
packages/notifi-react-card/lib/components/intercom/IntercomCard.tsx
const { instantSubscribe, isAuthenticated, isInitialized } = useNotifiSubscribe({
targetGroupName: "Intercom",
});
const { client } = useNotifiClientContext();
const { userState, ensureAlert } = client;
const isInitialized = !!userState;
const isAuthenticated = client.userState?.status === "authenticated";
packages/notifi-react-card/lib/components/subscription/EventTypeDirectPushRow.tsx
packages/notifi-react-card/lib/components/subscription/EventTypeCustomHealthCheckRow.tsx
packages/notifi-react-card/lib/components/subscription/EventTypeCustomToggleRow.tsx
packages/notifi-react-card/lib/components/subscription/EventTypeHealthCheckRow.tsx
packages/notifi-react-card/lib/components/subscription/EventTypePriceChangeRow.tsx
packages/notifi-react-card/lib/components/subscription/EventTypeTradingPairsRow.tsx
packages/notifi-react-card/lib/components/subscription/EventTypeWalletBalanceRow.tsx
packages/notifi-react-card/lib/components/subscription/EventTypeXMTPRow.tsx
packages/notifi-react-card/lib/components/subscription/EventTypeBroadcastRow.tsx
const { instantSubscribe } = useNotifiSubscribe({
targetGroupName: "Default",
});
const { client } = useNotifiClientContext();
const { ensureAlert } = client;
packages/notifi-react-card/lib/components/subscription/NotifiSubscribeButton.tsx
const { isInitialized, subscribe, updateTargetGroups } = useNotifiSubscribe({
targetGroupName: "Default",
});
const { client } = useNotifiClientContext();
const { ensureAlert, ensureTargetGroup, userState } = client;
const isInitialized = !!userState;
packages/notifi-react-card/lib/components/subscription/NotifiSubscriptionCardContainer.tsx
const { isInitialized } = useNotifiSubscribe({ targetGroupName: "Default" });
const { client } = useNotifiClientContext();
const { userState } = client;
const isInitialized = !!userState;
-
packages/notifi-react-card/lib/components/subscription/SubscriptionCardV1.tsx
const { isInitialized, isTokenExpired } = useNotifiSubscribe({
targetGroupName: "Default",
});
const { client } = useNotifiClientContext();
const { userState } = client;
const isInitialized = !!userState;
const isTokenExpired = userState?.status === "expired";
packages/notifi-react-card/lib/components/subscription/subscription-card-views/ExpiredTokenViewCard.tsx
const { logIn } = useNotifiSubscribe({ targetGroupName: "Default" });
const { client } = useNotifiClientContext();
const { logIn } = client;
packages/notifi-react-card/lib/components/subscription/subscription-card-views/VerifyWalletView.tsx
const { subscribe, updateWallets } = useNotifiSubscribe({
targetGroupName: "Default",
});
const { client } = useNotifiClientContext();
const { ensureAlert, login, fetchData } = client;
updateWallets
is trying to call login
and ensureSourceGroup
methods. Then finally fetchData
to get updated data
-packages/notifi-react-card/lib/components/WalletList/ConnectWalletRow.tsx
const { subscribeWallet } = useNotifiSubscribe({
targetGroupName: "Default",
});
const { client } = useNotifiClientContext();
const { subscribeWallet } = client;
Step#3 Ensure all direct client usage in notifi-react-card
can be normally used.
packages/notifi-react-card/lib/components/intercom/IntercomCard.tsx
const { client } = useNotifiClientContext();
const result = await client.createSupportConversation();
packages/notifi-react-card/lib/components/subscription/EventTypeDirectPushRow.tsx
const { client } = useNotifiClientContext();
const result = await client.getNotificationHistory({
first,
after,
});
packages/notifi-react-card/lib/hooks/useIntercomCard.ts
const { client } = useNotifiClientContext();
client.fetchSubscriptionCard({
type: "INTERCOM_CARD",
id: cardId,
});
packages/notifi-react-card/lib/hooks/useIntercomChat.ts
const { client } = useNotifiClientContext();
client.getConversationMessages({
first: 50,
getConversationMessagesInput: { conversationId },
});
packages/notifi-react-card/lib/hooks/useSubscriptionCard.ts
const { client } = useNotifiClientContext();
client.fetchSubscriptionCard(input);
References
- type changes from using
notifi-core
tonotifi-graphql
:
Below is an example of type difference between react-frontend-client and react-hook.
async completeLoginViaTransaction({
walletBlockchain,
walletAddress,
transactionSignature,
// The input and output are from notifi-graphql
}: CompleteLoginProps): Promise<Types.CompleteLogInByTransactionMutation>
// ...
type CompleteLoginProps = Omit<
Types.CompleteLogInByTransactionInput,
'dappAddress' | 'randomUuid'
>;
export type CompleteLogInByTransactionInput = {
/** The dapp id for this tenant */
dappAddress: Scalars["String"];
/** Random client generated UUID used in hash generation of nonce+uuid */
randomUuid: Scalars["String"];
/** Timestamp in seconds since Unix epoch. Required for Aptos chain. This will be the timestamp on the transaction. */
timestamp?: InputMaybe<Scalars["Long"]>;
/** Transaction containing the Base64(SHA256(hash(nonce+uuid))) printed to 'Notifi Auth: <value>' */
transactionSignature: Scalars["String"];
/** Address of wallet attempting to log in with */
walletAddress: Scalars["String"];
/** Blockchain of the wallet */
walletBlockchain: WalletBlockchain;
/** Public key of wallet attempting to log in with. Required for Aptos chain. */
walletPublicKey?: InputMaybe<Scalars["String"]>;
};
export type CompleteLogInByTransactionMutation = {
__typename?: "NotifiMutation";
completeLogInByTransaction?:
| {
__typename?: "User";
email?: string | undefined;
emailConfirmed: boolean;
roles?: Array<string | undefined> | undefined;
authorization?: { __typename?: "Authorization"; token: string; expiry: string } | undefined;
}
| undefined;
};
const completeLoginViaTransaction = useCallback(
async (
// The input and output are from notifi-core
input: CompleteLoginViaTransactionInput
): Promise<CompleteLoginViaTransactionResult>
// ...
export type CompleteLoginViaTransactionInput = Readonly<{
transactionSignature: string;
}>;
export type CompleteLoginViaTransactionResult = Readonly<User>;
export type User = Readonly<{
email: string | null;
emailConfirmed: boolean;
authorization: Authorization | null;
roles: ReadonlyArray<string> | null;
}>;
Question: storage modules
packages/notifi-react-hooks/lib/utils/storage.ts
packages/notifi-frontend-client/lib/storage/NotifiFrontendStorage.ts
Questions
- What is oldValue for?
- Why we have to reset the key to newKey with expired authorization when the oldKey is not null?
// ...
const oldKey = `${jwtPrefix}:${dappAddress}:${walletPublicKey}`;
const newKey = `${jwtPrefix}:${dappAddress}:${walletPublicKey}:authorization`;
const getAuthorization = async () => {
const oldValue = await localforage.getItem<string>(oldKey);
if (oldValue !== null) {
const expiry = new Date();
expiry.setMinutes(expiry.getMinutes() - 1); // Assume expired
const migrated: Authorization = {
token: oldValue,
expiry: expiry.toISOString(),
};
await localforage.removeItem(oldKey);
await localforage.setItem(newKey, migrated);
}
return await localforage.getItem<Authorization>(newKey);
};
- It seems like the
oldKey
logic does not appear in frontend-client.
export const createLocalForageStorageDriver = (config: NotifiFrontendConfiguration): StorageDriver => {
let keyPrefix = `${getEnvPrefix(config.env)}:${config.tenantId}`;
switch (config.walletBlockchain) {
case "SOLANA": {
keyPrefix += `:${config.walletPublicKey}`;
break;
}
case "APTOS": {
keyPrefix += `:${config.accountAddress}:${config.authenticationKey}`;
break;
}
}
// ...
};
Note: The duplicated code also happens in
/notifi-frontend-client/lib/storage/InMemoryStorageDriver.ts
and/notifi-frontend-client/lib/storage/LocalForageStorageDriver.ts
- Why does frontend-client has iInMemoryStorageDriver? and what is it for
utils modules mapping (hooks and frontendClient)
alertUtils
is totally not used --> deprecate.fetchDataImpl
only used in hooks doing internal data fetching. In frontendClient, the source or target not exist, error will be directly thrown. --> deprecate.
SubscriptionCardConfig.ts
react-card will make use of the SubscriptionCardConfig from frontend-client --> deprecate the one in react-card.
packages/notifi-react-card/lib/hooks/SubscriptionCardConfig.ts
packages/notifi-frontend-client/lib/models/SubscriptionCardConfig.ts