From b46fce71ef34cd0105a86ae66e07ceb95afcf114 Mon Sep 17 00:00:00 2001 From: Matt Upham <30577966+mattupham@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:50:35 -0400 Subject: [PATCH 1/4] mattupham/staking mobile (#2120) * Clean up whitespace * Cleanup on mobile * validator next step modal * Clean up table * Clean up 2 * Remove comment --- packages/web/components/cards/rewards-card.tsx | 2 +- packages/web/components/cards/stake-dashboard.tsx | 7 ++++--- packages/web/modals/validator-next-step.tsx | 10 +++++----- packages/web/modals/validator-squad.tsx | 2 +- packages/web/pages/stake/index.tsx | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/web/components/cards/rewards-card.tsx b/packages/web/components/cards/rewards-card.tsx index c17698264b..c5e9b387a6 100644 --- a/packages/web/components/cards/rewards-card.tsx +++ b/packages/web/components/cards/rewards-card.tsx @@ -28,7 +28,7 @@ export const RewardsCard: React.FC<{ {image}
{title} -
+
diff --git a/packages/web/components/cards/stake-dashboard.tsx b/packages/web/components/cards/stake-dashboard.tsx index 643ecdecc7..c0963703a5 100644 --- a/packages/web/components/cards/stake-dashboard.tsx +++ b/packages/web/components/cards/stake-dashboard.tsx @@ -87,7 +87,7 @@ export const StakeDashboard: React.FC<{ return ( -
+
= ({ title, dollarAmount, osmoAmount }) => { return ( -
+
+ {/*
*/} {title} -

{dollarAmount}

+

{dollarAmount}

{osmoAmount} diff --git a/packages/web/modals/validator-next-step.tsx b/packages/web/modals/validator-next-step.tsx index c2aa9249f4..0a20de00b6 100644 --- a/packages/web/modals/validator-next-step.tsx +++ b/packages/web/modals/validator-next-step.tsx @@ -62,7 +62,7 @@ export const ValidatorNextStepModal: FunctionComponent<

{t("stake.validatorNextStep.newUser.description")}{" "} - + {/* TODO - add link to learn here */} {t("stake.validatorNextStep.newUser.learnMore")} {"->"} @@ -71,7 +71,7 @@ export const ValidatorNextStepModal: FunctionComponent< @@ -81,16 +81,16 @@ export const ValidatorNextStepModal: FunctionComponent<

{t("stake.validatorNextStep.existingUser.description")}

-
+
diff --git a/packages/web/pages/stake/index.tsx b/packages/web/pages/stake/index.tsx index 8b00943eae..b3cf9ccd55 100644 --- a/packages/web/pages/stake/index.tsx +++ b/packages/web/pages/stake/index.tsx @@ -308,7 +308,7 @@ export const Staking: React.FC = observer(() => { } return ( -
+
Date: Mon, 11 Sep 2023 13:19:30 -0600 Subject: [PATCH 2/4] Asset Integrations: Add USDC.wh and WETH.wh (#2116) * add USDC.wh and WETH.wh * Update source-chain-infos.ts --- .../source-chain-infos.ts | 16 +++++++++++++ packages/web/config/ibc-assets.ts | 20 ++++++++++++++++ packages/web/config/price.ts | 23 +++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/packages/web/config/generate-chain-infos/source-chain-infos.ts b/packages/web/config/generate-chain-infos/source-chain-infos.ts index 3ec62af19f..fb143cc4b1 100644 --- a/packages/web/config/generate-chain-infos/source-chain-infos.ts +++ b/packages/web/config/generate-chain-infos/source-chain-infos.ts @@ -4129,6 +4129,22 @@ export const mainnetChainInfos: SimplifiedChainInfo[] = [ coinGeckoId: "pool:usdt.wh", coinImageUrl: "/tokens/usdt.hole.svg", }, + { + coinDenom: "USDC.wh", + coinMinimalDenom: + "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/GGh9Ufn1SeDGrhzEkMyRKt5568VbbxZK2yvWNsd6PbXt", + coinDecimals: 6, + coinGeckoId: "pool:usdc.wh", + coinImageUrl: "/tokens/usdc.hole.svg", + }, + { + coinDenom: "WETH.wh", + coinMinimalDenom: + "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/5BWqpR48Lubd55szM5i62zK7TFkddckhbT48yy6mNbDp", + coinDecimals: 8, + coinGeckoId: "pool:weth.wh", + coinImageUrl: "/tokens/weth.hole.svg", + }, ], features: ["ibc-transfer", "ibc-go", "cosmwasm"], explorerUrlToTx: "https://bigdipper.live/wormhole/transactions/{txHash}", diff --git a/packages/web/config/ibc-assets.ts b/packages/web/config/ibc-assets.ts index 0947a94e35..8bb6929f68 100644 --- a/packages/web/config/ibc-assets.ts +++ b/packages/web/config/ibc-assets.ts @@ -2082,6 +2082,26 @@ export const IBCAssetInfos: (IBCAsset & { depositUrlOverride: "https://blue.kujira.app/ibc?destination=osmosis-1&source=kaiyo-1&denom=factory%2Fkujira1643jxg8wasy5cfcn7xm8rd742yeazcksqlg4d7%2Fumnta", }, + { + counterpartyChainId: "wormchain", + sourceChannelId: "channel-2186", + destChannelId: "channel-3", + coinMinimalDenom: + "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/GGh9Ufn1SeDGrhzEkMyRKt5568VbbxZK2yvWNsd6PbXt", + isVerified: true, + depositUrlOverride: "https://portalbridge.com/cosmos/", + withdrawUrlOverride: "https://portalbridge.com/cosmos/", + }, + { + counterpartyChainId: "wormchain", + sourceChannelId: "channel-2186", + destChannelId: "channel-3", + coinMinimalDenom: + "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/5BWqpR48Lubd55szM5i62zK7TFkddckhbT48yy6mNbDp", + isVerified: true, + depositUrlOverride: "https://portalbridge.com/cosmos/", + withdrawUrlOverride: "https://portalbridge.com/cosmos/", + }, ].filter((ibcAsset) => { // validate IBC asset config if ( diff --git a/packages/web/config/price.ts b/packages/web/config/price.ts index 1c0a050d0b..ce653f98bd 100644 --- a/packages/web/config/price.ts +++ b/packages/web/config/price.ts @@ -2113,6 +2113,29 @@ const mainnetPoolPriceRoutes: IntermediateRoute[] = [ spotPriceDestDenom: "uosmo", destCoinId: "pool:uosmo", }, + { + alternativeCoinId: "pool:usdc.wh", + poolId: "1171", + spotPriceSourceDenom: DenomHelper.ibcDenom( + [{ portId: "transfer", channelId: "channel-2186" }], + "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/GGh9Ufn1SeDGrhzEkMyRKt5568VbbxZK2yvWNsd6PbXt" + ), + spotPriceDestDenom: "uosmo", + destCoinId: "pool:uosmo", + }, + { + alternativeCoinId: "pool:weth.wh", + poolId: "1149", + spotPriceSourceDenom: DenomHelper.ibcDenom( + [{ portId: "transfer", channelId: "channel-2186" }], + "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/5BWqpR48Lubd55szM5i62zK7TFkddckhbT48yy6mNbDp" + ), + spotPriceDestDenom: DenomHelper.ibcDenom( + [{ portId: "transfer", channelId: "channel-208" }], + "weth-wei" + ), + destCoinId: "pool:weth-wei", + }, ]; const testnetPoolPriceRoutes: IntermediateRoute[] = []; From b39bd3d294f4eb1169b49d412c57201a7d8ad49d Mon Sep 17 00:00:00 2001 From: JeremyParish69 <95667791+JeremyParish69@users.noreply.github.com> Date: Mon, 11 Sep 2023 15:31:24 -0600 Subject: [PATCH 3/4] Asset Integrations: Add DGL token (#2114) * add DGL token * Update source-chain-infos.ts * Update ibc-assets.ts * yarn --- .../generate-chain-infos/source-chain-infos.ts | 8 ++++++++ packages/web/config/ibc-assets.ts | 7 +++++++ packages/web/config/price.ts | 10 ++++++++++ packages/web/public/tokens/dgl.png | Bin 0 -> 66696 bytes 4 files changed, 25 insertions(+) create mode 100644 packages/web/public/tokens/dgl.png diff --git a/packages/web/config/generate-chain-infos/source-chain-infos.ts b/packages/web/config/generate-chain-infos/source-chain-infos.ts index fb143cc4b1..325f8f5347 100644 --- a/packages/web/config/generate-chain-infos/source-chain-infos.ts +++ b/packages/web/config/generate-chain-infos/source-chain-infos.ts @@ -1416,6 +1416,14 @@ export const mainnetChainInfos: SimplifiedChainInfo[] = [ coinGeckoId: "pool:watr", coinImageUrl: "/tokens/watr.png", }, + { + coinDenom: "DGL", + coinMinimalDenom: + "factory/juno1u805lv20qc6jy7c3ttre7nct6uyl20pfky5r7e/DGL", + coinDecimals: 6, + coinGeckoId: "pool:dgl", + coinImageUrl: "/tokens/dgl.png", + }, ], features: ["ibc-transfer", "ibc-go", "wasmd_0.24+", "cosmwasm"], explorerUrlToTx: "https://www.mintscan.io/juno/txs/{txHash}", diff --git a/packages/web/config/ibc-assets.ts b/packages/web/config/ibc-assets.ts index 8bb6929f68..3ef05c0d8f 100644 --- a/packages/web/config/ibc-assets.ts +++ b/packages/web/config/ibc-assets.ts @@ -2102,6 +2102,13 @@ export const IBCAssetInfos: (IBCAsset & { depositUrlOverride: "https://portalbridge.com/cosmos/", withdrawUrlOverride: "https://portalbridge.com/cosmos/", }, + { + counterpartyChainId: "juno-1", + sourceChannelId: "channel-42", + destChannelId: "channel-0", + coinMinimalDenom: + "factory/juno1u805lv20qc6jy7c3ttre7nct6uyl20pfky5r7e/DGL", + }, ].filter((ibcAsset) => { // validate IBC asset config if ( diff --git a/packages/web/config/price.ts b/packages/web/config/price.ts index ce653f98bd..b635cd2662 100644 --- a/packages/web/config/price.ts +++ b/packages/web/config/price.ts @@ -2136,6 +2136,16 @@ const mainnetPoolPriceRoutes: IntermediateRoute[] = [ ), destCoinId: "pool:weth-wei", }, + { + alternativeCoinId: "pool:dgl", + poolId: "1143", + spotPriceSourceDenom: DenomHelper.ibcDenom( + [{ portId: "transfer", channelId: "channel-42" }], + "factory/juno1u805lv20qc6jy7c3ttre7nct6uyl20pfky5r7e/DGL" + ), + spotPriceDestDenom: "uosmo", + destCoinId: "pool:uosmo", + }, ]; const testnetPoolPriceRoutes: IntermediateRoute[] = []; diff --git a/packages/web/public/tokens/dgl.png b/packages/web/public/tokens/dgl.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf926d56f58443b0a0d6ae112e6713ddee882dd GIT binary patch literal 66696 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

{O7`~*E5(e0e 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 P3_@SqNb8jbGW>r5%B3&7 literal 0 HcmV?d00001 From 51f7e58b9dc81dd979d41e0a9aed4be24ee990f8 Mon Sep 17 00:00:00 2001 From: eric-notifi <127958634+eric-notifi@users.noreply.github.com> Date: Tue, 12 Sep 2023 07:01:21 +0800 Subject: [PATCH 4/4] [Notifi] notification popover integration V2 + V3 (#2035) * Notifi integration: feat: target verify/ discard modal/ major refactor/ other fixes * Notification integration: feat: unread count badge --------- Co-authored-by: Eric Lee --- packages/web/components/layouts/main.tsx | 25 +- packages/web/components/navbar/index.tsx | 19 +- .../notifi/hooks/use-notifi-breadcrumb.tsx | 57 +-- .../notifi/hooks/use-notifi-setting.tsx | 154 ++++++ .../notifi/notifi-modal-context.tsx | 22 +- .../web/integrations/notifi/notifi-modal.tsx | 51 +- .../integrations/notifi/notifi-popover.tsx | 191 ++++--- .../fetched-card/alert-setting-list.tsx | 75 ++- .../fetched-card/alert-setting-row.tsx | 109 ++-- .../fetched-card/edit-view.tsx | 465 ++++++++---------- .../fetched-card/history-detail-view.tsx | 4 +- .../fetched-card/history-rows.tsx | 65 ++- .../fetched-card/history-view.tsx | 40 +- .../fetched-card/input-email.tsx | 146 ++++++ .../fetched-card/input-sms.tsx | 129 +++++ .../fetched-card/input-telegram.tsx | 155 ++++++ .../fetched-card/input-with-icon.tsx | 10 +- .../notifi-subscription-card.tsx | 19 +- packages/web/localizations/en.json | 4 +- packages/web/localizations/es.json | 4 +- packages/web/localizations/fa.json | 4 +- packages/web/localizations/fr.json | 4 +- packages/web/localizations/ko.json | 4 +- packages/web/localizations/pl.json | 4 +- packages/web/localizations/pt-br.json | 4 +- packages/web/localizations/ro.json | 4 +- packages/web/localizations/tr.json | 4 +- packages/web/localizations/zh-cn.json | 4 +- packages/web/localizations/zh-hk.json | 4 +- packages/web/localizations/zh-tw.json | 4 +- packages/web/package.json | 4 +- yarn.lock | 78 ++- 32 files changed, 1304 insertions(+), 562 deletions(-) create mode 100644 packages/web/integrations/notifi/hooks/use-notifi-setting.tsx create mode 100644 packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-email.tsx create mode 100644 packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-sms.tsx create mode 100644 packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-telegram.tsx diff --git a/packages/web/components/layouts/main.tsx b/packages/web/components/layouts/main.tsx index 7810e5d049..c7031f62a5 100644 --- a/packages/web/components/layouts/main.tsx +++ b/packages/web/components/layouts/main.tsx @@ -8,8 +8,7 @@ import { MainMenu } from "~/components/main-menu"; import { NavBar } from "~/components/navbar"; import NavbarOsmoPrice from "~/components/navbar-osmo-price"; import { MainLayoutMenu } from "~/components/types"; -import { useCurrentLanguage, useFeatureFlags, useWindowSize } from "~/hooks"; -import { NotifiContextProvider } from "~/integrations/notifi"; +import { useCurrentLanguage, useWindowSize } from "~/hooks"; export const MainLayout: FunctionComponent<{ menus: MainLayoutMenu[]; @@ -28,8 +27,6 @@ export const MainLayout: FunctionComponent<{ ({ selectionTest }) => selectionTest?.test(router.pathname) ?? false ); - const featureFlags = useFeatureFlags(); - return ( {showFixedLogo && ( @@ -51,21 +48,11 @@ export const MainLayout: FunctionComponent<{

- {featureFlags.notifications ? ( - - - - ) : ( - - )} +
{children}
diff --git a/packages/web/components/navbar/index.tsx b/packages/web/components/navbar/index.tsx index faab4f4b31..92a3416d42 100644 --- a/packages/web/components/navbar/index.tsx +++ b/packages/web/components/navbar/index.tsx @@ -28,8 +28,11 @@ import { } from "~/hooks"; import { useFeatureFlags } from "~/hooks/use-feature-flags"; import { useWalletSelect } from "~/hooks/wallet-select"; -import { NotifiModal, NotifiPopover } from "~/integrations/notifi"; -import { useNotifiBreadcrumb } from "~/integrations/notifi/hooks"; +import { + NotifiContextProvider, + NotifiModal, + NotifiPopover, +} from "~/integrations/notifi"; import { ModalBase, ModalBaseProps, SettingsModal } from "~/modals"; import { ProfileModal } from "~/modals/profile"; import { UserUpgradesModal } from "~/modals/user-upgrades"; @@ -116,8 +119,6 @@ export const NavBar: FunctionComponent< const router = useRouter(); const { isLoading: isWalletLoading } = useWalletSelect(); - const { hasUnreadNotification } = useNotifiBreadcrumb(); - useEffect(() => { const handler = () => { closeMobileMenuRef.current(); @@ -309,16 +310,14 @@ export const NavBar: FunctionComponent<
)} {featureFlags.notifications && walletSupportsNotifications && ( - <> - + + - + )} { accountStore, } = useStore(); const { client } = useNotifiClientContext(); - const [hasUnreadNotification, setHasUnreadNotification] = useState(false); + const [unreadNotificationCount, setUnreadNotificationCount] = useState(0); + const hasUnreadNotification = useMemo( + () => (unreadNotificationCount > 0 ? true : false), + [unreadNotificationCount] + ); useEffect(() => { + const wallet = accountStore.getWallet(chainId); + if (!wallet?.address || !client?.isAuthenticated) return; + + client + .getUnreadNotificationHistoryCount() + .then((res) => { + const unreadNotificationCount = res.count; + setUnreadNotificationCount(unreadNotificationCount); + }) + .catch((_e) => { + /* Intentionally empty (Concurrent can only possibly happens here instead of inside interval) */ + }); + const interval = setInterval(() => { const wallet = accountStore.getWallet(chainId); - if (!wallet?.address || !client?.isAuthenticated) - return setHasUnreadNotification(true); - const localStorageKey = `lastStoredTimestamp:${wallet.address}`; - - client - .getNotificationHistory({ first: 1 }) - .then((res) => { - const newestHistoryItem = res.nodes?.[0]; - const newestNotificationCreatedDate = newestHistoryItem?.createdDate - ? dayjs(newestHistoryItem?.createdDate) - : dayjs("2022-01-05T12:30:00.792Z"); - - const lastStoredTimestamp = dayjs( - window.localStorage.getItem(localStorageKey) - ).isValid() - ? dayjs(window.localStorage.getItem(localStorageKey)) - : dayjs("2022-01-05T10:30:00.792Z"); - - if (newestNotificationCreatedDate.isAfter(lastStoredTimestamp)) { - setHasUnreadNotification(true); - } else { - setHasUnreadNotification(false); - } - }) - .catch(() => setHasUnreadNotification(true)); - }, 5000); + if (!wallet?.address || !client?.isAuthenticated) return; + + client.getUnreadNotificationHistoryCount().then((res) => { + const unreadNotificationCount = res.count; + setUnreadNotificationCount(unreadNotificationCount); + }); + }, Math.floor(Math.random() * 5000) + 5000); // a random interval between 5 and 10 seconds to avoid spamming the server return () => clearInterval(interval); }, [client?.isAuthenticated]); - return { hasUnreadNotification }; + return { hasUnreadNotification, unreadNotificationCount }; }; diff --git a/packages/web/integrations/notifi/hooks/use-notifi-setting.tsx b/packages/web/integrations/notifi/hooks/use-notifi-setting.tsx new file mode 100644 index 0000000000..875421e29e --- /dev/null +++ b/packages/web/integrations/notifi/hooks/use-notifi-setting.tsx @@ -0,0 +1,154 @@ +import { NotifiFrontendClient } from "@notifi-network/notifi-frontend-client"; +import { + useNotifiClientContext, + useNotifiForm, +} from "@notifi-network/notifi-react-card"; +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { useNotifiConfig } from "~/integrations/notifi/notifi-config-context"; + +export type TargetGroupFragment = Awaited< + ReturnType +>[number]; + +type TargetStates = Readonly<{ + targetGroup: TargetGroupFragment | undefined; + emailSelected: boolean; + telegramSelected: boolean; + smsSelected: boolean; +}>; + +export const useNotifiSetting = () => { + const config = useNotifiConfig(); + const { client } = useNotifiClientContext(); + const { + setEmail: setFormEmail, + setPhoneNumber: setFormPhoneNumber, + setTelegram: setFormTelegram, + } = useNotifiForm(); + const [alertStates, setAlertStates] = useState>({}); + const [targetStates, setTargetStates] = useState({ + targetGroup: undefined, + emailSelected: false, + telegramSelected: false, + smsSelected: false, + }); + + const initialAlertStates = useMemo>(() => { + if (config.state !== "fetched") { + return {}; + } + + const alerts = client.data?.alerts ?? []; + const newStates: Record = {}; + config.data.eventTypes.forEach((row) => { + const isActive = alerts.find((it) => it?.name === row.name) !== undefined; + newStates[row.name] = isActive; + }); + return newStates; + }, [client, config]); + + useEffect(() => { + if ( + Object.keys(alertStates).length === 0 && + Object.keys(initialAlertStates).length !== 0 + ) { + setAlertStates(initialAlertStates); + } + }, [initialAlertStates, alertStates]); + + const needsSave = useMemo<"alerts" | "targets" | null>(() => { + // Changed alerts need save + if (config.state === "fetched") { + for (let i = 0; i < config.data.eventTypes.length; ++i) { + const row = config.data.eventTypes[i]; + if (initialAlertStates[row.name] !== alertStates[row.name]) { + return "alerts"; + } + } + } + + const isOriginalEmailExist = !!targetStates.targetGroup?.emailTargets?.[0]; + const isOriginalPhoneNumberExist = + !!targetStates.targetGroup?.smsTargets?.[0]; + const isOriginalTelegramExist = + !!targetStates.targetGroup?.telegramTargets?.[0]; + if ( + (isOriginalEmailExist && !targetStates.emailSelected) || + (isOriginalPhoneNumberExist && !targetStates.smsSelected) || + (isOriginalTelegramExist && !targetStates.telegramSelected) + ) { + return "targets"; + } else { + return null; + } + }, [config, targetStates, initialAlertStates, alertStates]); + + const revertChanges = useCallback(() => { + setAlertStates(initialAlertStates); + const targetGroup = client.data?.targetGroups?.find( + (it) => it.name === "Default" + ); + if (targetGroup === undefined) { + return; + } + const emailTarget = targetGroup.emailTargets?.[0]; + const emailSelected = emailTarget !== undefined; + const telegramTarget = targetGroup.telegramTargets?.[0]; + const telegramSelected = telegramTarget !== undefined; + const smsTarget = targetGroup.smsTargets?.[0]; + const smsSelected = smsTarget !== undefined; + setFormEmail(emailTarget?.emailAddress ?? ""); + setFormTelegram(telegramTarget?.telegramId ?? ""); + setFormPhoneNumber(smsTarget?.phoneNumber ?? ""); + setTargetStates({ + targetGroup, + emailSelected, + telegramSelected, + smsSelected, + }); + }, []); + + useEffect(() => { + const targetGroup = client.data?.targetGroups?.find( + (it) => it.name === "Default" + ); + if (targetGroup === targetStates.targetGroup) { + return; + } + + if (targetGroup !== undefined) { + const emailTarget = targetGroup.emailTargets?.[0]; + const emailSelected = emailTarget !== undefined; + const telegramTarget = targetGroup.telegramTargets?.[0]; + const telegramSelected = telegramTarget !== undefined; + const smsTarget = targetGroup.smsTargets?.[0]; + const smsSelected = smsTarget !== undefined; + setFormEmail(emailTarget?.emailAddress ?? ""); + setFormTelegram(telegramTarget?.telegramId ?? ""); + setFormPhoneNumber(smsTarget?.phoneNumber ?? ""); + setTargetStates({ + targetGroup, + emailSelected, + telegramSelected, + smsSelected, + }); + } else { + setTargetStates({ + targetGroup: undefined, + emailSelected: false, + telegramSelected: false, + smsSelected: false, + }); + } + }, [client, targetStates, setFormEmail, setFormPhoneNumber, setFormTelegram]); + + return { + alertStates, + setAlertStates, + targetStates, + setTargetStates, + needsSave, + revertChanges, + }; +}; diff --git a/packages/web/integrations/notifi/notifi-modal-context.tsx b/packages/web/integrations/notifi/notifi-modal-context.tsx index b561637a5c..2c478b37a6 100644 --- a/packages/web/integrations/notifi/notifi-modal-context.tsx +++ b/packages/web/integrations/notifi/notifi-modal-context.tsx @@ -19,13 +19,21 @@ interface NotifiModalFunctions { account: string; location: Location; innerState: Partial; - isOverLayEnabled: boolean; - setIsOverLayEnabled: (isOverLayEnabled: boolean) => void; selectedHistoryEntry?: HistoryRowData; setSelectedHistoryEntry: React.Dispatch< React.SetStateAction >; renderView: (location: Location) => void; + setInnerState: React.Dispatch>>; + /** The following 8 states for modalBase/Popover pop-up status*/ + isOverLayEnabled: boolean; // The background overlay (outside of the card) + setIsOverLayEnabled: React.Dispatch>; + isInCardOverlayEnabled: boolean; // The background overlay (inside of the card) + setIsInCardOverlayEnabled: React.Dispatch>; + isCardOpen: boolean; + setIsCardOpen: React.Dispatch>; + isPreventingCardClosed: boolean; // Preventing card from closing while isCardOpen is true + setIsPreventingCardClosed: React.Dispatch>; } const NotifiModalContext = createContext({ @@ -39,6 +47,9 @@ export const NotifiModalContextProvider: FunctionComponent< const [innerState, setInnerState] = useState>({}); const [location, setLocation] = useState("signup"); const [isOverLayEnabled, setIsOverLayEnabled] = useState(false); + const [isInCardOverlayEnabled, setIsInCardOverlayEnabled] = useState(false); + const [isPreventingCardClosed, setIsPreventingCardClosed] = useState(false); + const [isCardOpen, setIsCardOpen] = useState(true); const [selectedHistoryEntry, setSelectedHistoryEntry] = useState< HistoryRowData | undefined >(undefined); @@ -116,7 +127,14 @@ export const NotifiModalContextProvider: FunctionComponent< setSelectedHistoryEntry, location, isOverLayEnabled, + isInCardOverlayEnabled, setIsOverLayEnabled, + setIsInCardOverlayEnabled, + isPreventingCardClosed, + setIsPreventingCardClosed, + isCardOpen, + setIsCardOpen, + setInnerState, }} > {children} diff --git a/packages/web/integrations/notifi/notifi-modal.tsx b/packages/web/integrations/notifi/notifi-modal.tsx index 39c3b2662e..6fc618d077 100644 --- a/packages/web/integrations/notifi/notifi-modal.tsx +++ b/packages/web/integrations/notifi/notifi-modal.tsx @@ -1,28 +1,65 @@ +import { useNotifiClientContext } from "@notifi-network/notifi-react-card"; import { FunctionComponent } from "react"; import { Icon } from "~/components/assets"; import IconButton from "~/components/buttons/icon-button"; +import { useWindowSize } from "~/hooks"; import { useNotifiModalContext } from "~/integrations/notifi/notifi-modal-context"; import { NotifiSubscriptionCard } from "~/integrations/notifi/notifi-subscription-card"; import { ModalBase, ModalBaseProps } from "~/modals"; +import { useStore } from "~/stores"; -export const NotifiModal: FunctionComponent = (props) => { +interface Props extends ModalBaseProps { + onOpenNotifi: () => void; +} + +export const NotifiModal: FunctionComponent = (props) => { const { innerState } = useNotifiModalContext(); const finalProps = { ...props, ...innerState }; - const { innerState: { onRequestBack, backIcon } = {}, setIsOverLayEnabled } = - useNotifiModalContext(); + const { + innerState: { onRequestBack, backIcon } = {}, + setIsOverLayEnabled, + isPreventingCardClosed, + isInCardOverlayEnabled, + setIsInCardOverlayEnabled, + } = useNotifiModalContext(); + + const { + chainStore: { + osmosis: { chainId }, + }, + accountStore, + } = useStore(); + + const { client } = useNotifiClientContext(); + + const { isMobile } = useWindowSize(); + + if (!accountStore.getWallet(chainId) || !client?.isInitialized || !isMobile) { + return null; + } return ( { finalProps.onRequestClose(); setIsOverLayEnabled(false); }} title={finalProps.title} + className="sm:px-2" >
+ {isInCardOverlayEnabled ? ( +
{ + setIsInCardOverlayEnabled(false); + finalProps.onOpenNotifi(); // modal remains open while inCardOverlay is clicked + }} + /> + ) : null} {onRequestBack && ( = (props) => { size="unstyled" className={`top-9.5 absolute ${ backIcon !== "setting" ? "left" : "right" - }-8 z-50 mt-1 w-fit rotate-180 cursor-pointer py-0 text-osmoverse-400 md:top-7 md:left-7`} + }-8 z-50 mt-1 w-fit rotate-180 cursor-pointer py-0 text-osmoverse-400 md:left-7 md:top-7`} icon={ } onClick={onRequestBack} /> )} - +
); diff --git a/packages/web/integrations/notifi/notifi-popover.tsx b/packages/web/integrations/notifi/notifi-popover.tsx index 02259a178a..16a696b265 100644 --- a/packages/web/integrations/notifi/notifi-popover.tsx +++ b/packages/web/integrations/notifi/notifi-popover.tsx @@ -1,6 +1,13 @@ import { WalletStatus } from "@cosmos-kit/core"; +import { useNotifiClientContext } from "@notifi-network/notifi-react-card"; import classNames from "classnames"; -import React, { ComponentProps, Fragment, FunctionComponent } from "react"; +import React, { + ComponentProps, + ElementRef, + Fragment, + FunctionComponent, + useRef, +} from "react"; import { forwardRef } from "react"; import { Icon } from "~/components/assets"; @@ -8,20 +15,22 @@ import { Button } from "~/components/buttons"; import IconButton from "~/components/buttons/icon-button"; import { Popover } from "~/components/popover"; import { EventName } from "~/config"; -import { useAmplitudeAnalytics } from "~/hooks"; +import { useAmplitudeAnalytics, useWindowSize } from "~/hooks"; +import { useNotifiBreadcrumb } from "~/integrations/notifi/hooks"; import { useNotifiModalContext } from "~/integrations/notifi/notifi-modal-context"; import { NotifiSubscriptionCard } from "~/integrations/notifi/notifi-subscription-card"; import { useStore } from "~/stores"; export interface NotifiButtonProps { className?: string; - hasUnreadNotification: boolean; } -const NotifiIconButton = forwardRef< - HTMLButtonElement, +const NotifiIconButton: FunctionComponent< ComponentProps & { hasUnreadNotification?: boolean } ->(({ hasUnreadNotification, ...buttonProps }, ref) => { +> = forwardRef(({ ...buttonProps }, ref) => { + const { unreadNotificationCount, hasUnreadNotification } = + useNotifiBreadcrumb(); + return ( <> } {...buttonProps} /> + {hasUnreadNotification ? ( -
+
- +
+
+ {unreadNotificationCount > 99 ? "99" : unreadNotificationCount} +
+
) : null} @@ -55,7 +71,6 @@ const NotifiIconButton = forwardRef< export const NotifiPopover: FunctionComponent = ({ className, - hasUnreadNotification, }: NotifiButtonProps) => { const { chainStore: { @@ -67,17 +82,28 @@ export const NotifiPopover: FunctionComponent = ({ const osmosisWallet = accountStore.getWallet(chainId); + const { client } = useNotifiClientContext(); const { innerState: { onRequestBack, backIcon, title } = {}, location, + isInCardOverlayEnabled, isOverLayEnabled, setIsOverLayEnabled, + setIsInCardOverlayEnabled, + isPreventingCardClosed, } = useNotifiModalContext(); - if (osmosisWallet?.walletStatus !== WalletStatus.Connected) { + const { isMobile } = useWindowSize(); + const notifiIconButtonRef = useRef>(null); + + if ( + osmosisWallet?.walletStatus !== WalletStatus.Connected || + !client?.isInitialized || + isMobile + ) { return (
-
+
); @@ -87,61 +113,88 @@ export const NotifiPopover: FunctionComponent = ({ <> {isOverLayEnabled && (
setIsOverLayEnabled(false)} >
)} - - - { - if (isOverLayEnabled) setIsOverLayEnabled(false); - logEvent([EventName.Notifications.iconClicked]); - }} - /> - - -
- {onRequestBack && ( - - } - onClick={onRequestBack} - /> - )} - {typeof title === "string" ? ( -
-
{title}
-
- ) : ( - <>{title} - )} -
-
+ {({ open: popOverOpen }) => { + return ( + <> + + { + if (isOverLayEnabled) setIsOverLayEnabled(false); + logEvent([EventName.Notifications.iconClicked]); + }} + /> + + {popOverOpen || isPreventingCardClosed ? ( + + {isInCardOverlayEnabled ? ( +
{ + { + setIsInCardOverlayEnabled(false); + notifiIconButtonRef.current?.click(); // popoverOpen=true while inCardOverlay is clicked + } + }} + /> + ) : null} + +
+ {onRequestBack && ( + + } + onClick={onRequestBack} + /> + )} + {typeof title === "string" ? ( +
+
{title}
+
+ ) : ( + <>{title} + )} +
+
- -
- + > + +
+
+ ) : null} + + ); + }} ); diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-list.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-list.tsx index d9f3a39601..0ec79472b9 100644 --- a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-list.tsx +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-list.tsx @@ -1,11 +1,16 @@ -import { resolveStringRef } from "@notifi-network/notifi-react-card"; +import { + EventTypeItem, + LabelEventTypeItem, +} from "@notifi-network/notifi-frontend-client"; import { FunctionComponent, useMemo } from "react"; import { useNotifiConfig } from "~/integrations/notifi/notifi-config-context"; -import { useNotifiModalContext } from "~/integrations/notifi/notifi-modal-context"; import { AlertSettingRow } from "~/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-row"; -import { EVENT_TYPE_ID } from "~/integrations/notifi/notifi-subscription-card/fetched-card/history-rows"; +export type LabelWithAlerts = { + label: LabelEventTypeItem; + alerts: EventTypeItem[]; +}; interface Props { disabled: boolean; toggleStates: Record; @@ -16,52 +21,40 @@ interface Props { export const AlertSettingsList: FunctionComponent = (props) => { const config = useNotifiConfig(); - const { account } = useNotifiModalContext(); - const sortedRows = useMemo(() => { - if (config.state !== "fetched") { - return []; + const labelsWithAlerts = useMemo(() => { + if (config.state === "fetched") { + const labelsWithAlerts: LabelWithAlerts[] = []; + let currentLabel: LabelWithAlerts | undefined = undefined; + config.data.eventTypes.forEach((row) => { + if (row.type === "label") { + currentLabel = { + label: row, + alerts: [], + }; + labelsWithAlerts.push(currentLabel); + } else { + currentLabel?.alerts.push(row); + } + }); + return labelsWithAlerts; } - const inputs: Record = { - userWallet: account, - }; - return config.data.eventTypes.filter( - (row) => - // "Transaction status" alert is not supported for now, so hide it from the list - !( - row.type === "fusion" && - resolveStringRef(row.name, row.fusionEventId, inputs) === - EVENT_TYPE_ID.TRANSACTION_STATUSES - ) - ); - }, [config, account]); + return []; + }, [config.state]); if (config.state !== "fetched") { return null; } return ( -
    - {sortedRows.map((row) => { - if (row.type === "label") { - return ( -
  • -

    {row.name}

    -

    - {row.tooltipContent} -

    -
  • - ); - } - return ( -
  • - -
  • - ); - })} +
      + {labelsWithAlerts.map((labelWithAlerts) => ( + + ))}
    ); }; diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-row.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-row.tsx index 57e92dc4c6..25c3df930c 100644 --- a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-row.tsx +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-row.tsx @@ -1,51 +1,102 @@ -import { EventTypeItem } from "@notifi-network/notifi-frontend-client"; -import { FunctionComponent } from "react"; +import { resolveStringRef } from "@notifi-network/notifi-react-card"; +import { FunctionComponent, useState } from "react"; +import { Icon } from "~/components/assets"; import { Switch } from "~/components/control"; import { EventName } from "~/config"; import { useAmplitudeAnalytics } from "~/hooks"; - +import { useNotifiModalContext } from "~/integrations/notifi/notifi-modal-context"; +import { LabelWithAlerts } from "~/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-list"; +import { EVENT_TYPE_ID } from "~/integrations/notifi/notifi-subscription-card/fetched-card/history-rows"; interface Props { - row: EventTypeItem; disabled: boolean; toggleStates: Record; setToggleStates: React.Dispatch< React.SetStateAction> >; + labelWithAlerts: LabelWithAlerts; } export const AlertSettingRow: FunctionComponent = ({ - row, disabled, toggleStates, setToggleStates, + labelWithAlerts, }) => { + const { account } = useNotifiModalContext(); + + const [isExpended, setIsExpended] = useState(false); + const { logEvent } = useAmplitudeAnalytics(); + const sortedRows = labelWithAlerts.alerts.filter((row) => { + // "Transaction status" alert is not supported for now, so hide it from the list + const inputs: Record = { + userWallet: account, + }; + + return !( + row.type === "fusion" && + resolveStringRef(row.name, row.fusionEventId, inputs) === + EVENT_TYPE_ID.TRANSACTION_STATUSES + ); + }, []); + return ( - { - if (value) { - logEvent([ - EventName.Notifications.enableAlertClicked, - { type: row.name }, - ]); - } else { - logEvent([ - EventName.Notifications.disableAlertClicked, - { type: row.name }, - ]); - } - setToggleStates((previous) => ({ - ...previous, - [row.name]: value, - })); - }} - > - {row.name} - + <> +
  • +
    setIsExpended((prev) => !prev)} + > +

    + {labelWithAlerts.label.name} +

    +

    + {labelWithAlerts.label.tooltipContent} +

    + +
    + + {isExpended + ? sortedRows.map((alert, key) => ( +
    + { + if (value) { + logEvent([ + EventName.Notifications.enableAlertClicked, + { type: alert.name }, + ]); + } else { + logEvent([ + EventName.Notifications.disableAlertClicked, + { type: alert.name }, + ]); + } + setToggleStates((previous) => ({ + ...previous, + [alert.name]: value, + })); + }} + > + {alert.name} + +
    + )) + : null} +
  • + ); }; diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/edit-view.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/edit-view.tsx index a666abd430..73fadba740 100644 --- a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/edit-view.tsx +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/edit-view.tsx @@ -1,4 +1,3 @@ -import { NotifiFrontendClient } from "@notifi-network/notifi-frontend-client"; import { AlertConfiguration, useNotifiClientContext, @@ -7,37 +6,25 @@ import { useNotifiSubscriptionContext, } from "@notifi-network/notifi-react-card"; import classNames from "classnames"; -import { - FunctionComponent, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; +import { FunctionComponent, useCallback, useEffect, useMemo } from "react"; import { useTranslation } from "react-multi-lang"; import { Button } from "~/components/buttons"; import { EventName } from "~/config"; import { useAmplitudeAnalytics } from "~/hooks"; +import { useNotifiSetting } from "~/integrations/notifi/hooks/use-notifi-setting"; import { useNotifiConfig } from "~/integrations/notifi/notifi-config-context"; import { useNotifiModalContext } from "~/integrations/notifi/notifi-modal-context"; import { AlertSettingsList } from "~/integrations/notifi/notifi-subscription-card/fetched-card/alert-setting-list"; import styles from "~/integrations/notifi/notifi-subscription-card/fetched-card/edit-view.module.css"; - -type TargetGroupFragment = Awaited< - ReturnType ->[number]; - -type EditState = Readonly<{ - targetGroup: TargetGroupFragment | undefined; - emailSelected: boolean; - telegramSelected: boolean; - smsSelected: boolean; -}>; +import { InputEmail } from "~/integrations/notifi/notifi-subscription-card/fetched-card/input-email"; +import { InputSms } from "~/integrations/notifi/notifi-subscription-card/fetched-card/input-sms"; +import { InputTelegram } from "~/integrations/notifi/notifi-subscription-card/fetched-card/input-telegram"; export const EditView: FunctionComponent = () => { const config = useNotifiConfig(); - const { account } = useNotifiModalContext(); + const { account, setIsInCardOverlayEnabled, isInCardOverlayEnabled } = + useNotifiModalContext(); const { subscribe } = useNotifiSubscribe({ targetGroupName: "Default", }); @@ -45,83 +32,64 @@ export const EditView: FunctionComponent = () => { const { client } = useNotifiClientContext(); const { logEvent } = useAmplitudeAnalytics(); - const { - email: originalEmail, - phoneNumber: originalPhoneNunmber, - telegramId: originalTelegram, - loading, - } = useNotifiSubscriptionContext(); + const { loading, setLoading } = useNotifiSubscriptionContext(); const { formState, setEmail, setPhoneNumber, setTelegram } = useNotifiForm(); - const initialToggleStates = useMemo>(() => { - if (config.state !== "fetched") { - return {}; - } + const { setIsPreventingCardClosed, isPreventingCardClosed } = + useNotifiModalContext(); - const alerts = client.data?.alerts ?? []; - const newStates: Record = {}; - config.data.eventTypes.forEach((row) => { - const isActive = alerts.find((it) => it?.name === row.name) !== undefined; - newStates[row.name] = isActive; - }); + const { + alertStates, + setAlertStates, + targetStates, + setTargetStates, + needsSave, + revertChanges, + } = useNotifiSetting(); + const { setInnerState, renderView, isCardOpen } = useNotifiModalContext(); - return newStates; - }, [client, config]); + const toggleDiscardChangesModal = useCallback( + (enable?: boolean) => { + setIsInCardOverlayEnabled((prev) => enable ?? !prev); + setIsPreventingCardClosed((prev) => enable ?? !prev); + }, + [setIsInCardOverlayEnabled] + ); - const [toggleStates, setToggleStates] = useState>({}); - const [editState, setEditState] = useState({ - targetGroup: undefined, - emailSelected: false, - telegramSelected: false, - smsSelected: false, - }); + useEffect(() => { + if (isPreventingCardClosed && !isCardOpen) { + toggleDiscardChangesModal(true); + } + }, [isCardOpen]); - const needsSave = useMemo<"alerts" | "targets" | null>(() => { - if (config.state === "fetched") { - for (let i = 0; i < config.data.eventTypes.length; ++i) { - const row = config.data.eventTypes[i]; - if (initialToggleStates[row.name] !== toggleStates[row.name]) { - return "alerts"; + useEffect(() => { + // Reason not in hooks: adopt custom logic with local variable before renderView + setInnerState((prev) => ({ + ...prev, + onRequestBack: () => { + if (needsSave) { + toggleDiscardChangesModal(true); + return setIsPreventingCardClosed(true); } - } - } + renderView("history"); + }, + })); - if (editState.targetGroup === undefined) { - return (editState.emailSelected && formState.email !== "") || - (editState.telegramSelected && formState.telegram !== "") || - (editState.smsSelected && formState.phoneNumber !== "") - ? "targets" - : null; - } else { - let tgId = originalTelegram; - if (tgId.startsWith("@")) { - tgId = tgId.substring(1); - } - return (editState.emailSelected && formState.email !== originalEmail) || - (!editState.emailSelected && originalEmail !== "") || - (editState.smsSelected && - formState.phoneNumber !== originalPhoneNunmber) || - (!editState.smsSelected && originalPhoneNunmber !== "") || - (editState.telegramSelected && formState.telegram !== tgId) || - (!editState.telegramSelected && tgId !== "") - ? "targets" - : null; - } - }, [ - config, - editState, - formState, - initialToggleStates, - originalEmail, - originalPhoneNunmber, - originalTelegram, - toggleStates, - ]); + setIsPreventingCardClosed(needsSave ? true : false); + return () => toggleDiscardChangesModal(false); + }, [needsSave]); + + const isSaveOrDiscardModalShown = useMemo( + () => isPreventingCardClosed && isInCardOverlayEnabled, + [isPreventingCardClosed, isInCardOverlayEnabled] + ); const onClickSave = useCallback(async () => { logEvent([EventName.Notifications.saveChangesClicked]); + if (config.state !== "fetched") return new Error("config not fetched"); + if (needsSave === null) return; const broadcastMessageConfiguration = ( await import("@notifi-network/notifi-react-card") ).broadcastMessageConfiguration; @@ -130,23 +98,20 @@ export const EditView: FunctionComponent = () => { ).fusionToggleConfiguration; const resolveStringRef = (await import("@notifi-network/notifi-react-card")) .resolveStringRef; - if (needsSave === null) { - return; - } const { email: emailToSave, phoneNumber: smsToSave, telegram: telegramToSave, } = formState; - const { emailSelected, telegramSelected, smsSelected } = editState; + const { emailSelected, telegramSelected, smsSelected } = targetStates; const inputs: Record = { userWallet: account, }; try { - if (needsSave === "alerts" && config.state === "fetched") { + if (needsSave === "alerts") { const alertConfigurations: Record = {}; for (let i = 0; i < config.data.eventTypes.length; ++i) { @@ -155,7 +120,7 @@ export const EditView: FunctionComponent = () => { (it) => it?.name === row.name ); - const isEnabled = toggleStates[row.name] === true; + const isEnabled = alertStates[row.name] === true; if (alert === undefined && isEnabled) { let alertConfiguration = null; if (row.type === "broadcast") { @@ -209,193 +174,150 @@ export const EditView: FunctionComponent = () => { await subscribe( alertConfigurations as Record ); - } else if (needsSave === "targets") { - await client.ensureTargetGroup({ - name: "Default", - emailAddress: emailSelected ? emailToSave : undefined, - phoneNumber: smsSelected ? smsToSave : undefined, - telegramId: telegramSelected ? telegramToSave : undefined, - discordId: undefined, - }); } + await client.ensureTargetGroup({ + name: "Default", + emailAddress: emailSelected ? emailToSave : undefined, + phoneNumber: smsSelected ? smsToSave : undefined, + telegramId: telegramSelected ? telegramToSave : undefined, + includeDiscord: false, + }); } catch (e: unknown) {} }, [ account, client, config, - editState, + targetStates, formState, needsSave, subscribe, logEvent, - toggleStates, + alertStates, ]); - useEffect(() => { - if ( - Object.keys(toggleStates).length === 0 && - Object.keys(initialToggleStates).length !== 0 - ) { - setToggleStates(initialToggleStates); - } - }, [initialToggleStates, toggleStates]); - - useEffect(() => { - const targetGroup = client.data?.targetGroups?.find( - (it) => it.name === "Default" - ); - if (targetGroup === editState.targetGroup) { - return; - } - - if (targetGroup !== undefined) { - const emailTarget = targetGroup.emailTargets?.[0]; - const emailSelected = emailTarget !== undefined; - const telegramTarget = targetGroup.telegramTargets?.[0]; - const telegramSelected = telegramTarget !== undefined; - const smsTarget = targetGroup.smsTargets?.[0]; - const smsSelected = smsTarget !== undefined; - - setEmail(emailTarget?.emailAddress ?? ""); - setTelegram(telegramTarget?.telegramId ?? ""); - setPhoneNumber(smsTarget?.phoneNumber ?? ""); - setEditState({ - targetGroup, - emailSelected, - telegramSelected, - smsSelected, + const verifyTargets = useCallback( + async (preventDefault?: boolean) => { + if (preventDefault) return; + return await client.ensureTargetGroup({ + name: "Default", + emailAddress: targetStates.emailSelected ? formState.email : undefined, + phoneNumber: targetStates.smsSelected + ? formState.phoneNumber + : undefined, + telegramId: targetStates.telegramSelected + ? formState.telegram.startsWith("@") + ? formState.telegram.substring(1) + : formState.telegram + : undefined, + includeDiscord: false, }); - } else { - setEditState({ - targetGroup: undefined, - emailSelected: false, - telegramSelected: false, - smsSelected: false, - }); - } - }, [client, editState, setEmail, setPhoneNumber, setTelegram]); - - // DO NOT REMOVE: Might support target-subscription - // const telegramVerificationLink = useMemo(() => { - // const targetGroup = client.data?.targetGroups?.find((it) => it.name === "Default"); - // const telegramTarget = targetGroup?.telegramTargets?.[0]; - // if (telegramTarget === undefined || telegramTarget.isConfirmed) { - // return undefined; - // } - - // return telegramTarget.confirmationUrl; - // }, [client]); + }, + [client, targetStates, formState] + ); return ( -
    - {/* DO NOT REMOVE: Might support target-subscription next phase -

    - Add destinations for your notifications. +

    +

    + Add additional channels to receive your notifications.

    - { - const newValue = e.currentTarget.value; - if (newValue === "") { - setEditState((previous) => ({ - ...previous, - emailSelected: false, - })); - } else { - setEditState((previous) => ({ - ...previous, - emailSelected: true, - })); - } - setEmail(newValue); - }} - selected={editState.emailSelected} - setSelected={(selected) => { - setEditState((previous) => ({ - ...previous, - emailSelected: selected, - })); - }} - /> - { - const newValue = e.currentTarget.value; - if (newValue === "") { - setEditState((previous) => ({ - ...previous, - telegramSelected: false, - })); - } else { - setEditState((previous) => ({ +
    + { + const newValue = e.currentTarget.value; + if (newValue === "") { + setTargetStates((previous) => ({ + ...previous, + emailSelected: false, + })); + } else { + setTargetStates((previous) => ({ + ...previous, + emailSelected: true, + })); + } + setEmail(newValue); + }} + selected={targetStates.emailSelected} + setSelected={(selected) => { + setTargetStates((previous) => ({ ...previous, - telegramSelected: true, + emailSelected: selected, })); - } - setTelegram(newValue); - }} - selected={editState.telegramSelected} - setSelected={(selected) => { - setEditState((previous) => ({ - ...previous, - telegramSelected: selected, - })); - }} - /> - {telegramVerificationLink !== undefined ? ( -

    - - Verify telegram - -

    - ) : null} - { - let newValue = e.currentTarget.value; - if (newValue !== "" && !newValue.startsWith("+")) { - newValue = "+1" + newValue; - } - if (newValue === "") { - setEditState((previous) => ({ + }} + /> + { + const newValue = e.currentTarget.value; + if (newValue === "") { + setTargetStates((previous) => ({ + ...previous, + telegramSelected: false, + })); + } else { + setTargetStates((previous) => ({ + ...previous, + telegramSelected: true, + })); + } + setTelegram(newValue); + }} + selected={targetStates.telegramSelected} + setSelected={(selected) => { + setTargetStates((previous) => ({ ...previous, - smsSelected: false, + telegramSelected: selected, })); - } else if (newValue !== "") { - setEditState((previous) => ({ + }} + /> + { + let newValue = e.currentTarget.value; + if (newValue !== "" && !newValue.startsWith("+")) { + newValue = "+1" + newValue; + } + if (newValue === "") { + setTargetStates((previous) => ({ + ...previous, + smsSelected: false, + })); + } else if (newValue !== "") { + setTargetStates((previous) => ({ + ...previous, + smsSelected: true, + })); + } + setPhoneNumber(newValue); + }} + selected={targetStates.smsSelected} + setSelected={(selected) => { + setTargetStates((previous) => ({ ...previous, - smsSelected: true, + smsSelected: selected, })); - } - setPhoneNumber(newValue); - }} - selected={editState.smsSelected} - setSelected={(selected) => { - setEditState((previous) => ({ - ...previous, - smsSelected: selected, - })); - }} - /> */} + }} + /> +
    - {needsSave !== null ? ( + {!!needsSave && !isSaveOrDiscardModalShown ? (
    {
    ) : null} +
    +
    +

    + {t("notifi.unsavedChangeModalInfo")} +

    + + +
    +
    ); }; diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-detail-view.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-detail-view.tsx index 3ec05548ec..467637d4e7 100644 --- a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-detail-view.tsx +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-detail-view.tsx @@ -59,7 +59,9 @@ export const HistoryDetailView: FunctionComponent = ({
-
{message}
+
+ {message} +
); diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-rows.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-rows.tsx index 3dd7784854..bb4ad9f7d0 100644 --- a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-rows.tsx +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-rows.tsx @@ -15,9 +15,11 @@ import { EventName } from "~/config"; import { useAmplitudeAnalytics } from "~/hooks"; import { useNotifiModalContext } from "~/integrations/notifi/notifi-modal-context"; -export type HistoryRowData = Awaited< - ReturnType ->["nodes"][number]; +export type HistoryRowData = NonNullable< + NonNullable< + Awaited> + >["nodes"] +>[number]; export const HistoryRows: FunctionComponent<{ rows: ReadonlyArray; @@ -138,18 +140,20 @@ export const HistoryRow: FunctionComponent = ({ row }) => { const eventTypeId = jsonDetail?.NotifiData?.EventTypeId; switch (eventTypeId) { case EVENT_TYPE_ID.TRANSACTION_STATUSES: - const poolEventDetailsJson = jsonDetail as StatusesEventDetailsJson; + const poolEventDetailsJson = jsonDetail as + | StatusesEventDetailsJson + | undefined; const poolId = poolEventDetailsJson?.EventData?.pool?.poolId; - if (poolEventDetailsJson.EventData.isAssetTransfer) { + if (poolEventDetailsJson?.EventData.isAssetTransfer) { const txHash = poolEventDetailsJson?.EventData.assetTransfer?.transaction.hash; const blockHeight = poolEventDetailsJson?.EventData.assetTransfer?.transaction .height; const token = - poolEventDetailsJson?.EventData.assetTransfer?.denomMetadata - .display; + poolEventDetailsJson?.EventData.assetTransfer?.denomMetadata.display?.toUpperCase() ?? + "UNKNOWN"; const amount = poolEventDetailsJson?.EventData.assetTransfer ?.transferAmountFormatted; @@ -163,7 +167,7 @@ export const HistoryRow: FunctionComponent = ({ row }) => { txHash && (rowProps.popOutUrl = `https://www.mintscan.io/osmosis/txs/${txHash}?height=${blockHeight}`); } - if (poolEventDetailsJson.EventData.isPoolExited) { + if (poolEventDetailsJson?.EventData.isPoolExited) { const txHash = poolEventDetailsJson?.EventData?.pool?.transaction?.hash; const blockHeight = @@ -178,9 +182,9 @@ export const HistoryRow: FunctionComponent = ({ row }) => { txHash && (rowProps.popOutUrl = `https://www.mintscan.io/osmosis/txs/${txHash}?height=${blockHeight}`); } - if (poolEventDetailsJson.EventData.isPoolJoined) { + if (poolEventDetailsJson?.EventData.isPoolJoined) { const tokens = poolEventDetailsJson?.EventData?.pool?.tokens.map( - (token) => token.denom + (token) => token.denom.toUpperCase() ); rowProps.title = t("notifi.poolJoinedHistoryTitle"); rowProps.message = `${t("notifi.poolJoinedHistoryMessage")}${ @@ -189,7 +193,7 @@ export const HistoryRow: FunctionComponent = ({ row }) => { rowProps.emoji = ; rowProps.popOutUrl = `/pool/${poolId}`; } - if (poolEventDetailsJson.EventData.isTokenSwapped) { + if (poolEventDetailsJson?.EventData.isTokenSwapped) { const txHash = poolEventDetailsJson?.EventData?.tokenSwapped?.transaction ?.hash; @@ -200,12 +204,14 @@ export const HistoryRow: FunctionComponent = ({ row }) => { poolEventDetailsJson?.EventData?.tokenSwapped ?.amountInFormatted; const amountOut = - poolEventDetailsJson?.EventData?.tokenSwapped + poolEventDetailsJson.EventData?.tokenSwapped ?.amountOutFormatted; const tokenIn = - poolEventDetailsJson?.EventData?.tokenSwapped?.denomIn; + poolEventDetailsJson?.EventData?.tokenSwapped?.denomIn?.toUpperCase() ?? + "UNKNOWN"; const tokenOut = - poolEventDetailsJson?.EventData?.tokenSwapped?.denomOut; + poolEventDetailsJson?.EventData?.tokenSwapped?.denomOut?.toUpperCase() ?? + "UNKNOWN"; rowProps.title = t("notifi.swapHistoryTitle"); rowProps.message = ` ${ parseInt(amountIn || "") > 999999 ? ">1,000,000" : amountIn @@ -219,13 +225,14 @@ export const HistoryRow: FunctionComponent = ({ row }) => { break; case EVENT_TYPE_ID.ASSETS_RECEIVED: - const transferEventDetailsJson = - jsonDetail as TransferEventDetailsJson; + const transferEventDetailsJson = jsonDetail as + | TransferEventDetailsJson + | undefined; const txHash = transferEventDetailsJson?.EventData.transaction.hash; const blockHeight = transferEventDetailsJson?.EventData.transaction.height; const token = - transferEventDetailsJson?.EventData.denomMetadata.display; + transferEventDetailsJson?.EventData.denomMetadata.display.toUpperCase(); const amount = transferEventDetailsJson?.EventData.transferAmountFormatted; rowProps.title = `${t( @@ -239,12 +246,15 @@ export const HistoryRow: FunctionComponent = ({ row }) => { break; case EVENT_TYPE_ID.POSITION_OUT_OF_RANGE: - const positionEventDetailsJson = - jsonDetail as PositionEventDetailsJson; + const positionEventDetailsJson = jsonDetail as + | PositionEventDetailsJson + | undefined; const asset0 = - positionEventDetailsJson?.EventData?.token0DisplayDenom; + positionEventDetailsJson?.EventData?.token0DisplayDenom.toUpperCase() ?? + "UNKNOWN"; const asset1 = - positionEventDetailsJson?.EventData?.token1DisplayDenom; + positionEventDetailsJson?.EventData?.token1DisplayDenom.toUpperCase() ?? + "UNKNOWN"; const positionPoolId = positionEventDetailsJson?.EventData?.position?.position?.poolId; rowProps.title = `${t("notifi.positionOutOfRangeHistoryTitle")}`; @@ -312,7 +322,18 @@ export const HistoryRow: FunctionComponent = ({ row }) => {
-
{message}
+
+ {message} +
{timestamp}
diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-view.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-view.tsx index 2a8ccf6bc1..a8d42118e9 100644 --- a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-view.tsx +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-view.tsx @@ -1,5 +1,4 @@ import { useNotifiClientContext } from "@notifi-network/notifi-react-card"; -import dayjs from "dayjs"; import { FunctionComponent, useCallback, @@ -15,7 +14,6 @@ import { HistoryRows, } from "~/integrations/notifi/notifi-subscription-card/fetched-card/history-rows"; import { LoadingCard } from "~/integrations/notifi/notifi-subscription-card/loading-card"; -import { useStore } from "~/stores"; type CursorInfo = Readonly<{ hasNextPage: boolean; @@ -37,20 +35,17 @@ export const HistoryView: FunctionComponent = () => { }); const fetchedRef = useRef(false); const isQuerying = useRef(false); - const { - accountStore, - chainStore: { - osmosis: { chainId }, - }, - } = useStore(); useEffect(() => { - // A hack to implement the feat of breadcrumbs (Will move to BE approach) - window.localStorage.setItem( - `lastStoredTimestamp:${accountStore.getWallet(chainId)?.address}`, - dayjs(Date.now()).utc().format("YYYY-MM-DDTHH:mm:ss.SSS[Z]") - ); - }, []); + if (!allNodes[0]?.id) return; + + client + .markFusionNotificationHistoryAsRead({ + ids: [], + beforeId: allNodes[0].id, + }) + .catch((e) => console.log("Failed to mark as read", e)); + }, [allNodes]); const getNotificationHistory = useCallback( async ({ refresh }: { refresh: boolean }) => { @@ -67,11 +62,15 @@ export const HistoryView: FunctionComponent = () => { } isQuerying.current = true; - const result = await client.getNotificationHistory({ + const result = await client.getFusionNotificationHistory({ first: MESSAGES_PER_PAGE, after: refresh ? undefined : cursorInfo.endCursor, }); + if (!result) { + return; + } + const nodes = result.nodes ?? []; setAllNodes((existing) => existing.concat(nodes)); setCursorInfo(result.pageInfo); @@ -96,13 +95,18 @@ export const HistoryView: FunctionComponent = () => { if (!cursorInfo.hasNextPage) return; setIsLoadingMore(true); client - .getNotificationHistory({ + .getFusionNotificationHistory({ first: MESSAGES_PER_PAGE, after: cursorInfo.endCursor, }) .then((result) => { - setAllNodes((existing) => existing.concat(result.nodes ?? [])); - setCursorInfo(result.pageInfo); + setAllNodes((existing) => existing.concat(result?.nodes ?? [])); + setCursorInfo( + result?.pageInfo ?? { + hasNextPage: false, + endCursor: undefined, + } + ); }) .finally(() => setIsLoadingMore(false)); }; diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-email.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-email.tsx new file mode 100644 index 0000000000..2f3f657b54 --- /dev/null +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-email.tsx @@ -0,0 +1,146 @@ +import { + useNotifiClientContext, + useNotifiSubscribe, +} from "@notifi-network/notifi-react-card"; +import { + FunctionComponent, + InputHTMLAttributes, + useEffect, + useMemo, + useState, +} from "react"; + +import { Button } from "~/components/buttons"; +import { Switch } from "~/components/control"; +import { SpriteIconId } from "~/config"; +import { TargetGroupFragment } from "~/integrations/notifi/hooks/use-notifi-setting"; +import { InputWithIcon } from "~/integrations/notifi/notifi-subscription-card/fetched-card/input-with-icon"; + +interface Props extends InputHTMLAttributes { + iconId: SpriteIconId; + selected: boolean; + setSelected: (selected: boolean) => void; + verifyTargets: () => Promise; +} + +export const InputEmail: FunctionComponent = ({ + iconId, + selected, + setSelected, + verifyTargets, + ...inputProps +}: Props) => { + const { client } = useNotifiClientContext(); + + const [isVerificationRequested, setIsVerificationRequested] = + useState(false); + + const targetGroup = useMemo(() => { + return client.data?.targetGroups?.find((it) => it.name === "Default"); + }, [client]); + + const { reload, isAuthenticated, resendEmailVerificationLink } = + useNotifiSubscribe({ + targetGroupName: "Default", + }); + + const emailRegex = new RegExp("^[^s@]+@[^s@]+.[^s@]+$"); + + useEffect(() => { + return () => { + setIsVerificationRequested(false); + }; + }, [(targetGroup?.emailTargets ?? []).length]); + + useEffect(() => { + const handler = () => { + // Ensure target is up-to-date after user returns to tab from 3rd party verification site + if (!isAuthenticated) { + return; + } + reload(); + }; + + window.addEventListener("focus", handler); + return () => { + window.removeEventListener("focus", handler); + }; + }, [isAuthenticated]); + + const onClick = async () => { + if (inputProps.value?.toString()) { + const existingTargetId = targetGroup?.emailTargets?.[0]?.id; + const originalEmail = targetGroup?.emailTargets?.[0]?.emailAddress; + const newEmail = inputProps.value?.toString(); + + if (existingTargetId && originalEmail === newEmail) { + return resendEmailVerificationLink(existingTargetId) + .catch(() => { + console.log("error", "You cannot send link too often"); + }) + .finally(() => setIsVerificationRequested(true)); + } + + verifyTargets() + .then(() => { + setIsVerificationRequested(true); + }) + .catch((e) => { + console.log("error", e); + }); + } + }; + + inputProps.onFocus = () => { + if (!inputProps.value) { + setSelected(true); + } + }; + inputProps.onBlur = () => { + if (!inputProps.value) { + setSelected(false); + } + }; + + return ( +
+ + setSelected(toggled)} + > + + +
+ ); +}; diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-sms.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-sms.tsx new file mode 100644 index 0000000000..ce2879186b --- /dev/null +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-sms.tsx @@ -0,0 +1,129 @@ +import { + useNotifiClientContext, + useNotifiSubscribe, +} from "@notifi-network/notifi-react-card"; +import { + FunctionComponent, + InputHTMLAttributes, + useEffect, + useMemo, + useState, +} from "react"; + +import { Button } from "~/components/buttons"; +import { Switch } from "~/components/control"; +import { SpriteIconId } from "~/config"; +import { TargetGroupFragment } from "~/integrations/notifi/hooks/use-notifi-setting"; +import { InputWithIcon } from "~/integrations/notifi/notifi-subscription-card/fetched-card/input-with-icon"; + +interface Props extends InputHTMLAttributes { + iconId: SpriteIconId; + selected: boolean; + setSelected: (selected: boolean) => void; + verifyTargets: () => Promise; +} + +export const InputSms: FunctionComponent = ({ + iconId, + selected, + setSelected, + verifyTargets, + ...inputProps +}: Props) => { + const { client } = useNotifiClientContext(); + + const [isVerificationRequested, setIsVerificationRequested] = + useState(false); + + const targetGroup = useMemo(() => { + return client.data?.targetGroups?.find((it) => it.name === "Default"); + }, [client]); + + const { reload, isAuthenticated } = useNotifiSubscribe({ + targetGroupName: "Default", + }); + + const smsRegex = new RegExp(/^(\+1)([0-9]{10})$/); + + useEffect(() => { + return () => { + setIsVerificationRequested(false); + }; + }, [(targetGroup?.smsTargets ?? []).length]); + + useEffect(() => { + const handler = () => { + // Ensure target is up-to-date after user returns to tab from 3rd party verification site + if (!isAuthenticated) { + return; + } + reload(); + }; + + window.addEventListener("focus", handler); + return () => { + window.removeEventListener("focus", handler); + }; + }, [isAuthenticated]); + + const onClick = async () => { + if (inputProps.value?.toString()) { + verifyTargets() + .then(() => { + setIsVerificationRequested(true); + }) + .catch((e) => { + console.log("error", e); + }); + } + }; + + inputProps.onFocus = () => { + if (!inputProps.value) { + setSelected(true); + } + }; + inputProps.onBlur = () => { + if (!inputProps.value) { + setSelected(false); + } + }; + + return ( +
+ + setSelected(toggled)} + > + + +
+ ); +}; diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-telegram.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-telegram.tsx new file mode 100644 index 0000000000..84685285f3 --- /dev/null +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-telegram.tsx @@ -0,0 +1,155 @@ +import { + useNotifiClientContext, + useNotifiSubscribe, +} from "@notifi-network/notifi-react-card"; +import { + FunctionComponent, + InputHTMLAttributes, + useEffect, + useMemo, + useState, +} from "react"; + +import { Button } from "~/components/buttons"; +import { Switch } from "~/components/control"; +import { SpriteIconId } from "~/config"; +import { TargetGroupFragment } from "~/integrations/notifi/hooks/use-notifi-setting"; +import { InputWithIcon } from "~/integrations/notifi/notifi-subscription-card/fetched-card/input-with-icon"; + +interface Props extends InputHTMLAttributes { + iconId: SpriteIconId; + selected: boolean; + setSelected: (selected: boolean) => void; + verifyTargets: () => Promise; +} + +export const InputTelegram: FunctionComponent = ({ + iconId, + selected, + setSelected, + verifyTargets, + ...inputProps +}: Props) => { + const { client } = useNotifiClientContext(); + + const [isVerificationRequested, setIsVerificationRequested] = + useState(false); + + const targetGroup = useMemo(() => { + return client.data?.targetGroups?.find((it) => it.name === "Default"); + }, [client]); + + const { reload, isAuthenticated } = useNotifiSubscribe({ + targetGroupName: "Default", + }); + + const telegramVerificationLink = useMemo(() => { + const telegramTarget = targetGroup?.telegramTargets?.[0]; + if (telegramTarget === undefined || telegramTarget.isConfirmed) { + return undefined; + } + return telegramTarget.confirmationUrl; + }, [client, targetGroup]); + + const telegramRegex = new RegExp(/^(\@)?([A-Za-z0-9_]{1,})$/); + + useEffect(() => { + return () => { + setIsVerificationRequested(false); + }; + }, [(targetGroup?.emailTargets ?? []).length]); + + useEffect(() => { + const handler = () => { + // Ensure target is up-to-date after user returns to tab from 3rd party verification site + if (!isAuthenticated) { + return; + } + reload(); + }; + + window.addEventListener("focus", handler); + return () => { + window.removeEventListener("focus", handler); + }; + }, [isAuthenticated]); + + const onClick = async () => { + if (inputProps.value?.toString()) { + const existingTargetId = targetGroup?.telegramTargets?.[0]?.id; + const originalTelegramId = targetGroup?.telegramTargets?.[0]?.telegramId; + const newTelegramId = inputProps.value?.toString(); + + if (existingTargetId && originalTelegramId === newTelegramId) { + window.open(telegramVerificationLink ?? "", "_blank"); + setIsVerificationRequested(true); + return; + } + + verifyTargets() + .then((targetGroup) => { + const telegramTarget = targetGroup?.telegramTargets?.[0]; + if (!telegramTarget || telegramTarget.isConfirmed) return; + window.open(telegramTarget.confirmationUrl ?? "", "_blank"); + setIsVerificationRequested(true); + }) + .catch((e) => { + console.log("error", e); + }); + } + }; + + inputProps.onFocus = () => { + if (!inputProps.value) { + setSelected(true); + } + }; + inputProps.onBlur = () => { + if (!inputProps.value) { + setSelected(false); + } + }; + + return ( +
+ + setSelected(toggled)} + > + + +
+ ); +}; diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-with-icon.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-with-icon.tsx index d261130242..60e8ffd05d 100644 --- a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-with-icon.tsx +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/input-with-icon.tsx @@ -7,20 +7,24 @@ import { SpriteIconId } from "~/config"; interface Props extends InputHTMLAttributes { iconId: SpriteIconId; errorMessage?: string; + className?: string; } export const InputWithIcon: FunctionComponent = ({ iconId, errorMessage, + className, ...inputProps }) => { return ( <> -
- +
+ { - const { setIsOverLayEnabled, renderView } = useNotifiModalContext(); +type Props = { + parentType?: "popover" | "modalBase"; +}; + +export const NotifiSubscriptionCard: FunctionComponent = ({ + parentType, +}) => { + const { setIsOverLayEnabled, renderView, setIsCardOpen } = + useNotifiModalContext(); const { client } = useNotifiClientContext(); const config = useNotifiConfig(); - const { cardView, setCardView, setEmail, setTelegramId, setPhoneNumber } = + const { cardView, setEmail, setTelegramId, setPhoneNumber } = useNotifiSubscriptionContext(); const firstLoadRef = useRef(false); + useEffect(() => { + setIsCardOpen(parentType ? true : false); + }, [parentType]); + useEffect(() => { if (client.isInitialized && firstLoadRef.current !== true) { firstLoadRef.current = true; @@ -35,7 +46,7 @@ export const NotifiSubscriptionCard: FunctionComponent = () => { renderView("signup"); } } - }, [client, setCardView, renderView]); + }, [client, renderView]); const { data } = client; useEffect(() => { diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index 3a82b6d5aa..047773afef 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -390,7 +390,9 @@ "signupPageTitle": "Notifications are now available.", "signupPageMessage": "Never miss an important update again, from new token listings to critical position statuses.", "signupPageButton": "Enable Notifications", - "loadMore": "Load more" + "loadMore": "Load more", + "unsavedChangeModalInfo": "You have unsaved changes. Would you like to", + "discardButton": "Discard changes" }, "pool": { "24hrTradingVolume": "24h Trading volume", diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index 4855ef7293..103d4c0aab 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -702,7 +702,9 @@ "signupPageButton": "Permitir notificaciones", "loadMore": "Carga más", "emptyHistoryTitle": "Sin título", - "emptyHistoryMessage": "Sin mensaje" + "emptyHistoryMessage": "Sin mensaje", + "unsavedChangeModalInfo": "Tiene cambios sin guardar. Le gustaría", + "discardButton": "Descartar los cambios" }, "convertToStake": { "adTitle": "Hemos detectado una mejor estrategia de ingresos.", diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index ec5f529e33..f613d707c6 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -702,7 +702,9 @@ "signupPageButton": "اعلان ها را فعال کنید", "loadMore": "بارگذاری بیشتر", "emptyHistoryTitle": "بدون عنوان", - "emptyHistoryMessage": "بدون پیام" + "emptyHistoryMessage": "بدون پیام", + "unsavedChangeModalInfo": "شما تغییرات ذخیره نشده ای دارید. آیا شما می خواهید", + "discardButton": "لغو تغییرات" }, "convertToStake": { "adTitle": "ما استراتژی کسب درآمد بهتری را شناسایی کرده ایم.", diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index fd3641b788..e35cc49e75 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -702,7 +702,9 @@ "signupPageButton": "Activer les notifications", "loadMore": "Charger plus", "emptyHistoryTitle": "Pas de titre", - "emptyHistoryMessage": "Pas de message" + "emptyHistoryMessage": "Pas de message", + "unsavedChangeModalInfo": "Vous avez des changements non enregistrés. Souhaitez-vous", + "discardButton": "Annuler les modifications" }, "convertToStake": { "adTitle": "Nous avons détecté une meilleure stratégie de gains.", diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 342043db03..50aa45b8a6 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -702,7 +702,9 @@ "signupPageButton": "알림 활성화", "loadMore": "더 로드", "emptyHistoryTitle": "제목 없음", - "emptyHistoryMessage": "메시지 없음" + "emptyHistoryMessage": "메시지 없음", + "unsavedChangeModalInfo": "저장되지 않은 변경사항이 있습니다. 하시겠습니까?", + "discardButton": "변경 사항을 취소" }, "convertToStake": { "adTitle": "더 나은 수익 전략을 발견했습니다.", diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index a0931d97a2..789179e027 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -702,7 +702,9 @@ "signupPageButton": "Włącz powiadomienia", "loadMore": "Załaduj więcej", "emptyHistoryTitle": "Bez tytułu", - "emptyHistoryMessage": "Brak wiadomości" + "emptyHistoryMessage": "Brak wiadomości", + "unsavedChangeModalInfo": "Masz niezapisane zmiany. Czy chciałbyś", + "discardButton": "Odrzucać zmiany" }, "convertToStake": { "adTitle": "Wykryliśmy lepszą strategię zarabiania.", diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 2a6a3e49a1..e3760a30b0 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -702,7 +702,9 @@ "signupPageButton": "Ativar notificações", "loadMore": "Carregue mais", "emptyHistoryTitle": "sem título", - "emptyHistoryMessage": "Nenhuma mensagem" + "emptyHistoryMessage": "Nenhuma mensagem", + "unsavedChangeModalInfo": "Você tem alterações não salvas. Você gostaria de", + "discardButton": "Descartar mudanças" }, "convertToStake": { "adTitle": "Detectamos uma estratégia de ganhos melhor.", diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index 533630ec85..582af5e582 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -702,7 +702,9 @@ "signupPageButton": "Activați notificările", "loadMore": "Incarca mai mult", "emptyHistoryTitle": "Fara titlu", - "emptyHistoryMessage": "Niciun mesaj" + "emptyHistoryMessage": "Niciun mesaj", + "unsavedChangeModalInfo": "Aveți modificări nesalvate. Aţi dori să", + "discardButton": "Renunțați la modificări" }, "convertToStake": { "adTitle": "Am detectat o strategie de câștig mai bună.", diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 61fd33dc7f..fd379dd39d 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -702,7 +702,9 @@ "signupPageButton": "Bildirimleri Etkinleştir", "loadMore": "Daha fazla yükle", "emptyHistoryTitle": "Başlık yok", - "emptyHistoryMessage": "Mesaj yok" + "emptyHistoryMessage": "Mesaj yok", + "unsavedChangeModalInfo": "Kaydedilmemiş değişiklikleriniz mevcut. Arzu eder misiniz", + "discardButton": "Değişiklikleri gözardı et" }, "convertToStake": { "adTitle": "Daha iyi bir kazanç stratejisi tespit ettik.", diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index f0e883e0e3..66d114635b 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -702,7 +702,9 @@ "signupPageButton": "启用通知", "loadMore": "装载更多", "emptyHistoryTitle": "无题", - "emptyHistoryMessage": "没有消息" + "emptyHistoryMessage": "没有消息", + "unsavedChangeModalInfo": "您有未保存的更改。你是否想要", + "discardButton": "放弃更改" }, "convertToStake": { "adTitle": "我们发现了更好的盈利策略。", diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index a907e38707..5f70df57ae 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -702,7 +702,9 @@ "signupPageButton": "啟用通知", "loadMore": "裝載更多", "emptyHistoryTitle": "無題", - "emptyHistoryMessage": "沒有消息" + "emptyHistoryMessage": "沒有消息", + "unsavedChangeModalInfo": "您有未保存的更改。你是否想要", + "discardButton": "放棄更改" }, "convertToStake": { "adTitle": "我們發現了更好的盈利策略。", diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index dfb5dd7ee6..57749a3a9b 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -702,7 +702,9 @@ "signupPageButton": "啟用通知", "loadMore": "裝載更多", "emptyHistoryTitle": "無題", - "emptyHistoryMessage": "沒有消息" + "emptyHistoryMessage": "沒有消息", + "unsavedChangeModalInfo": "您有未保存的更改。你是否想要", + "discardButton": "放棄更改" }, "convertToStake": { "adTitle": "我們發現了更好的盈利策略。", diff --git a/packages/web/package.json b/packages/web/package.json index f249a0d2d1..9ef73f8fac 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -47,8 +47,8 @@ "@keplr-wallet/wc-client": "0.10.24-ibc.go.v7.hot.fix", "@metamask/onboarding": "^1.0.1", "@next/bundle-analyzer": "^12.1.6", - "@notifi-network/notifi-frontend-client": "0.76.0", - "@notifi-network/notifi-react-card": "0.76.0", + "@notifi-network/notifi-frontend-client": "0.82.1", + "@notifi-network/notifi-react-card": "0.82.1", "@osmosis-labs/math": "^5.0.0", "@osmosis-labs/pools": "^5.0.0", "@osmosis-labs/stores": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index fc3c7efb63..f017638101 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4803,66 +4803,65 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@notifi-network/notifi-axios-adapter@^0.76.0": - version "0.76.0" - resolved "https://registry.yarnpkg.com/@notifi-network/notifi-axios-adapter/-/notifi-axios-adapter-0.76.0.tgz#554b72632cf8befca2cc8fcef967733a884883fe" - integrity sha512-THsxI16q1A1yi/N3KXKf4e8PQPNI2Gun6E9S0ud1k2awH8c77xmarzGi8s7ZssUz+po9d1GEMMg4DbGuB7W1GA== +"@notifi-network/notifi-axios-adapter@^0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@notifi-network/notifi-axios-adapter/-/notifi-axios-adapter-0.82.1.tgz#664a16b8b0ab0c70fb0c823e027ae53fc30d2a39" + integrity sha512-46eTQqk4Xm1xz6eXPQcol0B/wNLnN35cE5gOrFRtczbKwBSYiDEhb7JkeYvJ294NyiknggfOUFcAzg2Fa9P+/A== dependencies: - "@notifi-network/notifi-axios-utils" "^0.76.0" + "@notifi-network/notifi-axios-utils" "^0.82.1" -"@notifi-network/notifi-axios-utils@^0.76.0": - version "0.76.0" - resolved "https://registry.yarnpkg.com/@notifi-network/notifi-axios-utils/-/notifi-axios-utils-0.76.0.tgz#3b33575f11a2d5048e35d817f18e9f49032c954f" - integrity sha512-UG3seY59wir1bMYE6nkeq5mbDlUSwK+6jtZaloiWN/jaCFcZ9dEEq0CUadiGM0xPTJ8xoM0FdowGuJxOaJM0Vg== +"@notifi-network/notifi-axios-utils@^0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@notifi-network/notifi-axios-utils/-/notifi-axios-utils-0.82.1.tgz#e4ab1bf4918b54256e81afcc04273753a14c2700" + integrity sha512-ylBW/lHZPhq5P0Gn2JH5xUMQPfMmgOACTKg9HsOcvBIg/ulj/YvdEJd1dm0aSXyVCnN7CzzQsyZ2D1yFC8uLJg== -"@notifi-network/notifi-core@^0.76.0": - version "0.76.0" - resolved "https://registry.yarnpkg.com/@notifi-network/notifi-core/-/notifi-core-0.76.0.tgz#c155cea5ad40cd6401119e408b68f2f1569ae315" - integrity sha512-pNAvO7akWLmKyCV0hpG0bQplbwE25zQaLP1ljkZ/1tfLBG+IgwSbt43Lwbf61Ghtco92SMu/WqOv1D5L4G5isQ== +"@notifi-network/notifi-core@^0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@notifi-network/notifi-core/-/notifi-core-0.82.1.tgz#715cb43b19a0c7473c2fd7804e84b75dd31ba8b1" + integrity sha512-ZbxzEEt+U7wjR/nkV2HXZAby1HR0uGx3kz9EX/xxQszG60S5UzHu8kTekFy1nvrPrcZX7Dq6cs18DrqRVsvtwQ== dependencies: - "@notifi-network/notifi-graphql" "^0.76.0" + "@notifi-network/notifi-graphql" "^0.82.1" -"@notifi-network/notifi-frontend-client@0.76.0", "@notifi-network/notifi-frontend-client@^0.76.0": - version "0.76.0" - resolved "https://registry.yarnpkg.com/@notifi-network/notifi-frontend-client/-/notifi-frontend-client-0.76.0.tgz#35468c0809ffc69374424f64d57840f72fa17ddc" - integrity sha512-L/zEY3+b1Au6K1zcD6H6z10Jf9MHvSAq9xcZYzsjIqloquJtz9NQhH2nxHYU8OfQ+vfnoszHfb6kM2JRLd5dJA== +"@notifi-network/notifi-frontend-client@0.82.1", "@notifi-network/notifi-frontend-client@^0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@notifi-network/notifi-frontend-client/-/notifi-frontend-client-0.82.1.tgz#aa3f45e621cfbd298eef7c789491b0a5e2cee760" + integrity sha512-3OnMkAMC++PPUa06mo/THDK1rnERJJbzbDzhZ2wnTMCEBVUci6PE6DGOP8b1P0svJfE8t1FjeY5E1Sb21A/zGA== dependencies: - "@notifi-network/notifi-graphql" "^0.76.0" + "@notifi-network/notifi-graphql" "^0.82.1" graphql-request "5.1.0" localforage "^1.10.0" -"@notifi-network/notifi-graphql@^0.76.0": - version "0.76.0" - resolved "https://registry.yarnpkg.com/@notifi-network/notifi-graphql/-/notifi-graphql-0.76.0.tgz#0beffd17f31f6d4e6f788b677a4a6d8b3828ccaf" - integrity sha512-7xzwz7wLXjyE5BfsCrVKIcPsUXXI26EhLJAEBvgbdjOaz5gNdkFe0hhxH+yTcjwE5R5Dc8Nn/drZEtmjxSFLZw== +"@notifi-network/notifi-graphql@^0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@notifi-network/notifi-graphql/-/notifi-graphql-0.82.1.tgz#3d183a15d152b33262f97d23742ef6141126a26f" + integrity sha512-ojYMfwLri4eG9kqdjzii3Eypkk6Y0OkZWF6y5aHE51PulCu/WiuqwIcNET/8ppF5eh/JrbHnKhNzavH+/q+E+w== dependencies: graphql "^16.6.0" graphql-request "5.1.0" uuid "^8.3.2" -"@notifi-network/notifi-react-card@0.76.0": - version "0.76.0" - resolved "https://registry.yarnpkg.com/@notifi-network/notifi-react-card/-/notifi-react-card-0.76.0.tgz#de1d1c71efd23b41ca74dc155674c0c1871cb097" - integrity sha512-6T0ozFNPcqcHA63jpGiLgjSNMfG8LMzGzubJTgiMn8Y6XCI0WvMvv92XwRN9TrwqxOR/c2LllMH9gNWK2kF6FA== +"@notifi-network/notifi-react-card@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@notifi-network/notifi-react-card/-/notifi-react-card-0.82.1.tgz#426f6f15f1698ba86a0cc6857a827369708f362c" + integrity sha512-sCGEuf/1ia9P5FcEGJsQaaAwWfjg+SecwcEWpneqMuuNNoBYbyyzLxd5H+pNglqnwFSuAlAPn3MWr1ssmnsU1g== dependencies: - "@notifi-network/notifi-frontend-client" "^0.76.0" - "@notifi-network/notifi-react-hooks" "^0.76.0" + "@notifi-network/notifi-frontend-client" "^0.82.1" + "@notifi-network/notifi-react-hooks" "^0.82.1" clsx "^1.2.1" date-fns "^2.29.3" libphonenumber-js "^1.10.13" react-virtuoso "^3.1.1" -"@notifi-network/notifi-react-hooks@^0.76.0": - version "0.76.0" - resolved "https://registry.yarnpkg.com/@notifi-network/notifi-react-hooks/-/notifi-react-hooks-0.76.0.tgz#45c106e46a502da28da4237940ae4ece475b4793" - integrity sha512-S4Tt7DnO3TeGHbzU97qopUkKysu9JLgIicOzlP7JEeBSnUgYkylsiyAQAf6LKsOGseiQyxUXTFU+W1bAXR5NAg== +"@notifi-network/notifi-react-hooks@^0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@notifi-network/notifi-react-hooks/-/notifi-react-hooks-0.82.1.tgz#a794477868e09a2c7e0a9d5a864c4c289cf4d531" + integrity sha512-5XcVYq1HxC3IEyj6UYjHrNlBOHU97YICF3e3dX0Tw24K1fBlNGyuQJTgETpbLcCLeDM9N+/zKXbYaVHwydMAqw== dependencies: - "@notifi-network/notifi-axios-adapter" "^0.76.0" - "@notifi-network/notifi-axios-utils" "^0.76.0" - "@notifi-network/notifi-core" "^0.76.0" + "@notifi-network/notifi-axios-adapter" "^0.82.1" + "@notifi-network/notifi-axios-utils" "^0.82.1" + "@notifi-network/notifi-core" "^0.82.1" axios "^0.26.0" localforage "^1.10.0" - typedoc-plugin-missing-exports "^0.22.6" typescript "^4.5.5" "@npmcli/ci-detect@^1.0.0": @@ -17068,11 +17067,6 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typedoc-plugin-missing-exports@^0.22.6: - version "0.22.6" - resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-0.22.6.tgz#7467c60f1cd26507124103f0b9bca271d5aa8d71" - integrity sha512-1uguGQqa+c5f33nWS3v1mm0uAx4Ii1lw4Kx2zQksmYFKNEWTmrmMXbMNBoBg4wu0p4dFCNC7JIWPoRzpNS6pFA== - typeforce@^1.11.5: version "1.18.0" resolved "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz"