Compare commits

..
Author SHA1 Message Date
jaredvu a123976646 notification adjustments 2023-09-06 13:53:38 -07:00
jaredvu 788fed9c26 Merge branch 'main' into abacus-tracking 2023-09-05 15:03:36 -07:00
Bill 564192fa2b Deposit/Withdraw status notifications (#9)
* Deposit/Withdraw status notifications

* prevent log out during switch accounts

* fix merge

* check for token approval

* address comments

* update lock

* wait for approval

* address comments
2023-09-05 13:04:53 -07:00
jaredvu b8526a5568 Merge branch 'main' into abacus-tracking 2023-09-05 13:01:07 -07:00
Rui 102b1702ff Add wallets PNGs to be used on mobile (#21) 2023-09-05 12:58:36 -07:00
jaredvu b0cd8e71ac add tracking protocol 2023-09-02 14:25:23 -07:00
28 changed files with 118 additions and 98 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/abacus": "^0.4.7",
"@dydxprotocol/abacus": "^0.4.8",
"@dydxprotocol/v4-client-js": "^0.32.0",
"@dydxprotocol/v4-localization": "^0.0.25",
"@ethersproject/providers": "^5.7.2",
+4 -4
View File
@@ -27,8 +27,8 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/abacus':
specifier: ^0.4.7
version: 0.4.7
specifier: ^0.4.8
version: 0.4.8
'@dydxprotocol/v4-client-js':
specifier: ^0.32.0
version: 0.32.0
@@ -1145,8 +1145,8 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/abacus@0.4.7:
resolution: {integrity: sha512-3c7Dx5H+KeXZYDpAOJs6fqxSNerKqNSD4tuMNn5F7RXAPWUOU8ADQHAFPnrP1PgTxPjfZBZQzLIziKpyXww4NQ==}
/@dydxprotocol/abacus@0.4.8:
resolution: {integrity: sha512-hX31m+a5Bwbs4dr5raPDtnflwUzEu/9BIt+LOlQgt1YOViuIse8dozgpgCHvqvA8MttqY3QGPalx5iRB8j8vtw==}
dev: false
/@dydxprotocol/dydxjs@0.3.0:
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

-4
View File
@@ -80,8 +80,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
{group.items.map((item) => (
<Fragment key={item.value}>
<Styled.Item
// value={item.value} // search by both value and textContent
// value={[group.groupLabel, item.label, item.tag].filter(Boolean).join('|')} // exclude item.value from searchable terms (not guaranteed to be unique)
value={[group.groupLabel, item.value, item.description, item.label, item.tag]
.filter(Boolean)
.join('|')}
@@ -116,8 +114,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
item.subitems?.map((subitem) => (
<Fragment key={subitem.value}>
<Styled.Item
// value={subitem.value} // search by both value and textContent
// value={[group.groupLabel, item.label, subitem.label, subitem.tag].filter(Boolean).join('|')}
value={[
group.groupLabel,
item.value,
+4 -1
View File
@@ -40,7 +40,10 @@ export type AbacusFileSystemProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.FileSystemProtocol,
'__doNotUseOrImplementIt'
>;
export type AbacusTrackingProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.TrackingProtocol,
'__doNotUseOrImplementIt'
>;
export type FileLocation = Abacus.exchange.dydx.abacus.protocols.FileLocation;
export type ThreadingType = Abacus.exchange.dydx.abacus.protocols.ThreadingType;
export const CoroutineTimer = Abacus.exchange.dydx.abacus.utils.CoroutineTimer;
+2
View File
@@ -34,6 +34,8 @@ export const STRING_KEYS = {
...WARNINGS_STRING_KEYS,
};
export type StringKey = keyof typeof STRING_KEYS;
export type LocaleData = typeof EN_LOCALE_DATA;
export type StringGetterFunction = (a: {
+17 -11
View File
@@ -1,4 +1,5 @@
import { StatusResponse } from "@0xsquid/sdk";
import type { ReactNode } from 'react';
import type { StatusResponse } from '@0xsquid/sdk';
/** implemented in useNotificationTypes */
export enum NotificationType {
@@ -18,26 +19,31 @@ export type NotificationTypeConfig<
/** React hook to trigger notifications based on app state */
useTrigger: (_: {
trigger: (
trigger: ({
id,
displayData,
updateKey,
isNew,
}: {
/** Unique ID for the triggered notification */
id: _NotificationId,
id: _NotificationId;
/** Display data for the triggered notification */
displayData: NotificationDisplayData,
displayData: NotificationDisplayData;
/**
* JSON-serializable key.
* Re-triggers the notification if passed a different value from the last trigger() call (even from a previous browser session).
* Suggested usage: data dependency array
*/
updateKey?: NotificationUpdateKey,
updateKey?: NotificationUpdateKey;
/**
* @param true (default): Notification initialized with status NotificationStatus.Triggered
* @param false: Notification initialized with status NotificationStatus.Cleared
*/
isNew?: boolean
) => void;
isNew?: boolean;
}) => void;
lastUpdated: number;
}) => void;
@@ -79,15 +85,15 @@ export type Notifications = Record<NotificationId, Notification<any>>;
/** Notification display data derived from app state at runtime. */
export type NotificationDisplayData = {
icon?: React.ReactNode;
icon?: React.ReactElement<any, 'svg'>;
title?: string;
description?: React.ReactNode;
description?: ReactNode;
customContent?: React.ReactNode;
customContent?: ReactNode;
customMenuContent?: React.ReactNode;
customMenuContent?: ReactNode;
actionDescription?: string;
+38 -39
View File
@@ -1,27 +1,27 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { AbacusOrderStatus, ORDER_SIDES, ORDER_STATUS_STRINGS } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { ORDER_SIDE_STRINGS, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade';
import { ORDER_SIDE_STRINGS } from '@/constants/trade';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { TransferStatusToast } from '@/views/TransferStatus';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { useStringGetter } from './useStringGetter';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
export const notificationTypes = [
{
@@ -58,34 +58,33 @@ export const notificationTypes = [
}
: undefined);
if (order)
trigger(
order.id,
{
icon: (
<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />
),
title: `${stringGetter({
key: TRADE_TYPE_STRINGS[order.type.rawValue as TradeTypes].tradeTypeKey,
})} ${
order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0
? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
: stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] })
}`,
description: `${stringGetter({
key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
})} ${order.size} ${order.marketId} @ $${order.price}`,
actionDescription: 'View Order',
actionAltText: 'View this order in the Orders tab or the Notifications menu.',
toastSensitivity:
order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
toastDuration: 5000,
},
[order.status.name, order.size],
!order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated
);
if (order) console.log(order);
trigger({
id: order.id,
displayData: {
icon: <AssetIcon symbol={order.marketId.split('-')[0]} />, //<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />,
title: `${stringGetter({
key: order.resources.typeStringKey ?? '',
})} ${
order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0
? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
: stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] })
}`,
description: `${stringGetter({
key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
})} ${order.size} ${order.marketId} @ $${order.price}`,
actionDescription: 'View Order',
actionAltText: 'View this order in the Orders tab or the Notifications menu.',
toastSensitivity:
order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
toastDuration: Infinity,
},
updateKey: [order.status.name, order.size],
isNew: !order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated,
});
}
}, [orderIds]);
}, [orderIds, stringGetter]);
},
useNotificationAction: () => {
@@ -120,9 +119,9 @@ export const notificationTypes = [
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const type = toChainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdraw';
trigger(
txHash,
{
trigger({
id: txHash,
displayData: {
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title: stringGetter({ key: getTitleStringKey(type, finished) }),
// TODO: confirm with design what the description should be
@@ -142,10 +141,10 @@ export const notificationTypes = [
customMenuContent: !finished && <TransferStatusSteps status={transfer.status} />,
toastSensitivity: 'foreground',
},
[]
);
updateKey: [],
});
}
}, [transferNotifications]);
}, [transferNotifications, stringGetter]);
},
},
} as NotificationTypeConfig<string, []>,
] satisfies NotificationTypeConfig[];
+17 -22
View File
@@ -92,7 +92,7 @@ const useNotificationsContext = () => {
for (const { type, useTrigger } of notificationTypes)
useTrigger({
trigger: useCallback(
(id, displayData, updateKey, isNew = true) => {
({ id, displayData, updateKey, isNew = true }) => {
const key = getKey({ type, id });
const notification = notifications[key];
@@ -182,28 +182,23 @@ const useNotificationsContext = () => {
const iconUrl =
displayData.icon && (await renderSvgToDataUrl(displayData.icon).catch(() => undefined));
const pushNotification = new globalThis.Notification(displayData.title, {
renotify: true,
tag: getKey(notification),
data: notification,
body: displayData.description,
icon: iconUrl ?? '/favicon.svg',
badge: iconUrl ?? '/favicon.svg',
image: iconUrl ?? '/favicon.svg',
vibrate: displayData.toastSensitivity === 'foreground',
requireInteraction: displayData.toastDuration === Infinity,
// actions: [
// {
// action: displayData.actionDescription,
// title: displayData.actionDescription,
// }
// ].slice(0, globalThis.Notification.maxActions),
});
if (displayData.title) {
const pushNotification = new globalThis.Notification(displayData.title, {
renotify: true,
tag: getKey(notification),
data: notification,
body: displayData.description ?? '',
icon: (iconUrl as string) ?? '/favicon.svg',
badge: (iconUrl as string) ?? '/favicon.svg',
image: (iconUrl as string) ?? '/favicon.svg',
requireInteraction: displayData.toastDuration === Infinity,
});
pushNotification.addEventListener('click', () => {
onNotificationAction(notification);
markSeen(notification);
});
pushNotification.addEventListener('click', () => {
onNotificationAction(notification);
markSeen(notification);
});
}
}
setPushNotificationsLastUpdated(Date.now());
+2 -1
View File
@@ -36,6 +36,7 @@ import AbacusStateNotifier from './stateNotification';
import AbacusLocalizer from './localizer';
import AbacusFormatter from './formatter';
import AbacusThreading from './threading';
import AbacusTracking from './tracking';
import AbacusFileSystem, { ENDPOINTS_PATH } from './filesystem';
import { LocaleSeparators } from '../numbers';
class AbacusStateManager {
@@ -59,7 +60,7 @@ class AbacusStateManager {
new AbacusRest(),
this.websocket,
new AbacusChainTransaction(),
null,
new AbacusTracking(),
new AbacusThreading(),
new CoroutineTimer(),
new AbacusFileSystem()
+9
View File
@@ -0,0 +1,9 @@
import type { AbacusTrackingProtocol, Nullable } from '@/constants/abacus';
class AbacusTracking implements AbacusTrackingProtocol {
log(event: string, data: Nullable<string>) {
console.log({ event, data });
}
}
export default AbacusTracking;
+4 -2
View File
@@ -21,8 +21,10 @@ const applyComputedStyles = (html: string) => {
const toDataUrl = (bytes: string, type = 'image/svg+xml') =>
new Promise<string | ArrayBuffer | null>((resolve, reject) => {
Object.assign(new FileReader(), {
onload: (e) => resolve(e.target.result),
onerror: (e) => reject(e.target.error),
onload: (e: {
target: { result: string | ArrayBuffer | PromiseLike<string | ArrayBuffer | null> | null };
}) => resolve(e.target.result),
onerror: (e: { target: { error: Error } }) => reject(e.target.error),
}).readAsDataURL(new File([bytes], '', { type }));
});
@@ -7,7 +7,7 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { AbacusOrderStatus, AbacusOrderTypes, type Nullable } from '@/constants/abacus';
import { ButtonAction } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { STRING_KEYS, StringKey } from '@/constants/localization';
import { AssetIcon } from '@/components/AssetIcon';
import { Button } from '@/components/Button';
@@ -38,6 +38,9 @@ type ElementProps = {
setIsOpen: (open: boolean) => void;
};
const isStringKey = (key: Nullable<string>): key is StringKey =>
key != null && STRING_KEYS.hasOwnProperty(key);
export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
@@ -47,7 +50,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
const { cancelOrder } = useSubaccount();
const {
asset = {},
asset,
cancelReason,
createdAtMilliseconds,
expiresAtMilliseconds,
@@ -57,7 +60,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
price,
reduceOnly,
totalFilled,
resources = {},
resources,
size,
status,
stepSizeDecimals,
@@ -109,9 +112,9 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
<Styled.Row>
<Styled.StatusIcon iconName={statusIcon} color={statusIconColor} />
<Styled.Status>
{statusStringKey
{isStringKey(statusStringKey)
? stringGetter({ key: statusStringKey })
: resources.statusStringKey
: isStringKey(resources.statusStringKey)
? stringGetter({ key: resources.statusStringKey })
: undefined}
</Styled.Status>
@@ -121,7 +124,9 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
{
key: 'cancel-reason',
label: stringGetter({ key: STRING_KEYS.CANCEL_REASON }),
value: cancelReason ? stringGetter({ key: STRING_KEYS[cancelReason] }) : undefined,
value: isStringKey(cancelReason)
? stringGetter({ key: STRING_KEYS[cancelReason] })
: undefined,
},
{
key: 'amount',
+9 -7
View File
@@ -1,10 +1,12 @@
import React, { useMemo } from 'react';
import { groupBy } from 'lodash';
import styled from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { type Notification, NotificationStatus } from '@/constants/notifications';
import { useStringGetter } from '@/hooks';
import { useNotifications } from '@/hooks/useNotifications';
import { CloseIcon } from '@/icons';
import { Button } from '@/components/Button';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
@@ -13,9 +15,7 @@ import { DialogPlacement } from '@/components/Dialog';
import { Output, OutputType } from '@/components/Output';
import { IconButton } from '@/components/IconButton';
import { Toolbar } from '@/components/Toolbar';
import { CloseIcon } from '@/icons';
import styled from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
@@ -27,6 +27,8 @@ export const NotificationsMenu = ({
slotTrigger,
placement = DialogPlacement.Sidebar,
}: ElementProps) => {
const stringGetter = useStringGetter();
const {
notifications,
getDisplayData,
@@ -69,7 +71,7 @@ export const NotificationsMenu = ({
.map(([status, notifications]) => ({
group: status,
groupLabel: {
[NotificationStatus.Triggered]: 'New',
[NotificationStatus.Triggered]: stringGetter({ key: STRING_KEYS.NEW }),
// [NotificationStatus.Updated]: 'Updates',
[NotificationStatus.Seen]: 'Seen',
[NotificationStatus.Cleared]: 'Archived',
@@ -135,7 +137,7 @@ export const NotificationsMenu = ({
isOpen={isMenuOpen || placement === DialogPlacement.Inline}
setIsOpen={setIsMenuOpen}
items={items}
title="Notifications"
title={stringGetter({ key: STRING_KEYS.NOTIFICATIONS })}
slotTrigger={
<$TriggerContainer>
{slotTrigger}
@@ -168,7 +170,7 @@ export const NotificationsMenu = ({
),
}}
>
Clear All
{stringGetter({ key: STRING_KEYS.CLEAR })}
</Button>
</$FooterToolbar>
}