diff --git a/.env.development b/.env.development index e1a719a71..cec98b4a0 100644 --- a/.env.development +++ b/.env.development @@ -6,7 +6,7 @@ VITE_GRAFANA_URL=https://grafana-api.play.hydration.cloud/api/ds/query VITE_GRAFANA_DSN=11 VITE_ENV=development VITE_TRSRY_ADDR=7L53bUTBopuwFt3mKUfmkzgGLayYa1Yvn1hAg9v5UMrQzTfh -VITE_WC_PROJECT_ID=c47a5369367ec2dad6b49c478eb772f9 +VITE_WC_PROJECT_ID=265a3fea03b46c14a46a201fbd6c552e VITE_HDX_CAIP_ID=polkadot:afdc188f45c71dacbaa0b62e16a91f72 VITE_STABLECOIN_ASSET_ID=10 VITE_FF_SETTINGS_ENABLED=true @@ -17,4 +17,6 @@ VITE_EVM_CHAIN_ID=222222 VITE_EVM_PROVIDER_URL=https://rpc.nice.hydration.cloud VITE_EVM_EXPLORER_URL=https://explorer.nice.hydration.cloud VITE_EVM_NATIVE_ASSET_ID=20 +VITE_MIGRATION_TRIGGER_DOMAIN="deploy-preview-1334--testnet-hydra-app.netlify.app" +VITE_MIGRATION_TARGET_DOMAIN="testnet-app.hydradx.io" diff --git a/.env.production b/.env.production index 4dd708b09..e2df5e0a7 100644 --- a/.env.production +++ b/.env.production @@ -1,12 +1,12 @@ VITE_PROVIDER_URL=wss://rpc.hydradx.cloud -VITE_DOMAIN_URL=https://app.hydradx.io +VITE_DOMAIN_URL=https://app.hydration.net VITE_INDEXER_URL=https://explorer.hydradx.cloud/graphql VITE_SQUID_URL=https://hydra-data-squid.play.hydration.cloud/graphql VITE_GRAFANA_URL=https://grafana.hydradx.cloud/api/ds/query VITE_GRAFANA_DSN=10 VITE_ENV=production VITE_TRSRY_ADDR=7L53bUTBopuwFt3mKUfmkzgGLayYa1Yvn1hAg9v5UMrQzTfh -VITE_WC_PROJECT_ID=c47a5369367ec2dad6b49c478eb772f9 +VITE_WC_PROJECT_ID=265a3fea03b46c14a46a201fbd6c552e VITE_HDX_CAIP_ID=polkadot:afdc188f45c71dacbaa0b62e16a91f72 VITE_STABLECOIN_ASSET_ID=10 VITE_FF_SETTINGS_ENABLED=false @@ -16,4 +16,6 @@ VITE_REFERENDUM_DATA_URL=https://hydradx.subsquare.io/api/democracy/referendums VITE_EVM_CHAIN_ID=222222 VITE_EVM_PROVIDER_URL=https://rpc.hydradx.cloud VITE_EVM_EXPLORER_URL=https://explorer.evm.hydration.cloud -VITE_EVM_NATIVE_ASSET_ID=20 \ No newline at end of file +VITE_EVM_NATIVE_ASSET_ID=20 +VITE_MIGRATION_TRIGGER_DOMAIN="app.hydradx.io" +VITE_MIGRATION_TARGET_DOMAIN="app.hydration.net" \ No newline at end of file diff --git a/.env.rococo b/.env.rococo index 2d469f8bb..0ae7bfda7 100644 --- a/.env.rococo +++ b/.env.rococo @@ -6,7 +6,7 @@ VITE_GRAFANA_URL=https://grafana-api.play.hydration.cloud/api/ds/query VITE_GRAFANA_DSN=11 VITE_ENV=rococo VITE_TRSRY_ADDR=7L53bUTBopuwFt3mKUfmkzgGLayYa1Yvn1hAg9v5UMrQzTfh -VITE_WC_PROJECT_ID=c47a5369367ec2dad6b49c478eb772f9 +VITE_WC_PROJECT_ID=265a3fea03b46c14a46a201fbd6c552e VITE_HDX_CAIP_ID=polkadot:afdc188f45c71dacbaa0b62e16a91f72 VITE_STABLECOIN_ASSET_ID=10 VITE_FF_SETTINGS_ENABLED=false @@ -17,3 +17,5 @@ VITE_EVM_CHAIN_ID= VITE_EVM_PROVIDER_URL= VITE_EVM_EXPLORER_URL= VITE_EVM_NATIVE_ASSET_ID=20 +VITE_MIGRATION_TRIGGER_DOMAIN="" +VITE_MIGRATION_TARGET_DOMAIN="" \ No newline at end of file diff --git a/src/assets/icons/migration/MigrationLogo.svg b/src/assets/icons/migration/MigrationLogo.svg new file mode 100644 index 000000000..a59f327cb --- /dev/null +++ b/src/assets/icons/migration/MigrationLogo.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/AppProviders/AppProviders.tsx b/src/components/AppProviders/AppProviders.tsx index 547fdef41..cb9f571cf 100644 --- a/src/components/AppProviders/AppProviders.tsx +++ b/src/components/AppProviders/AppProviders.tsx @@ -9,6 +9,7 @@ import { theme } from "theme" import * as React from "react" import * as Apps from "@galacticcouncil/apps" import { createComponent } from "@lit-labs/react" +import { MigrationProvider } from "sections/migration/MigrationProvider" const AppsPersistenceProvider = createComponent({ tagName: "gc-database-provider", @@ -18,21 +19,23 @@ const AppsPersistenceProvider = createComponent({ export const AppProviders: FC = ({ children }) => { return ( - - - - - - {children} - - - - - - + + + + + + + {children} + + + + + + + ) } diff --git a/src/components/WarningMessage/WarningMessage.styled.ts b/src/components/WarningMessage/WarningMessage.styled.ts index 692bbc3c3..a337817c9 100644 --- a/src/components/WarningMessage/WarningMessage.styled.ts +++ b/src/components/WarningMessage/WarningMessage.styled.ts @@ -1,8 +1,28 @@ +import { css } from "@emotion/react" import styled from "@emotion/styled" import { theme } from "theme" -export const SWarningMessageContainer = styled.div` - background: linear-gradient(90deg, #ff1f7a 41.09%, #57b3eb 100%); +type WarningMessageVariant = "pink" | "gradient" + +const getStylesByVariant = (variant: WarningMessageVariant) => { + switch (variant) { + case "pink": + return css` + background: #dfb1f3; + color: #240e32; + ` + case "gradient": + return css` + background: linear-gradient(90deg, #ff1f7a 41.09%, #57b3eb 100%); + color: ${theme.colors.white}; + ` + } +} + +export const SWarningMessageContainer = styled.div<{ + variant?: WarningMessageVariant +}>` + ${({ variant = "gradient" }) => getStylesByVariant(variant)} width: 100%; cursor: pointer; @@ -14,8 +34,6 @@ export const SWarningMessageContainer = styled.div` padding: 8px; z-index: ${theme.zIndices.header}; - - color: ${theme.colors.white}; ` export const SWarningMessageContent = styled.div` diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index cb62a3330..adf0bb186 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -896,5 +896,13 @@ "banner.newFarms.link": "Go to Liquidity \u2192", "searchFilter.empty.title": "No results found", "searchFilter.empty.desc": "Didn't find what you're looking for?", - "searchFilter.empty.link": "Join the Telegram" + "searchFilter.empty.link": "Join the Telegram", + "migration.export.title": "HydraDX is now Hydration", + "migration.export.description": "As part of the rebranding, this app has moved to app.hydration.net.", + "migration.export.question": "Would you like to migrate all past app settings to the new app?", + "migration.export.button": "Migrate Settings", + "migration.import.title": "Do you wish to overwrite your settings?", + "migration.import.description": "You already transferred your settings on {{ date, dd/MM/yyyy hh:mm:ss }}. Do you wish to overwrite your current settings?", + "migration.import.button": "Overwrite Settings", + "migration.warning.text": "You can transfer Your app settings from HydraDX to Hydration" } diff --git a/src/sections/migration/MigrationProvider.tsx b/src/sections/migration/MigrationProvider.tsx new file mode 100644 index 000000000..69cbf9a1c --- /dev/null +++ b/src/sections/migration/MigrationProvider.tsx @@ -0,0 +1,70 @@ +import { FC, PropsWithChildren, useState } from "react" +import { useLocation } from "react-use" +import { + MIGRATION_LS_KEYS, + MIGRATION_QUERY_PARAM, + MIGRATION_TARGET_DOMAIN, + MIGRATION_TRIGGER_DOMAIN, + serializeLocalStorage, + useMigrationStore, +} from "sections/migration/MigrationProvider.utils" +import { MigrationWarning } from "sections/migration/components/MigrationWarning" +import { MigrationExportModal } from "./components/MigrationExportModal" +import { MigrationImportModal } from "./components/MigrationImportModal" + +export const MigrationProvider: FC = ({ children }) => { + const { search, host } = useLocation() + const { migrationCompleted, setMigrationCompleted } = useMigrationStore() + + const [migrationCanceled, setMigrationCanceled] = useState(false) + + const paramKey = `?${MIGRATION_QUERY_PARAM}=` + const data = search?.replace(paramKey, "") ?? "" + + const shouldExport = MIGRATION_TRIGGER_DOMAIN === host + const shouldImport = + MIGRATION_TARGET_DOMAIN === host && search?.startsWith(paramKey) + + if (shouldImport) { + return + } + + if (shouldExport && !migrationCanceled) { + return ( + { + const qs = new URLSearchParams(search) + const from = qs.get("from") + + if (from) { + window.location.href = `https://${from}` + } else { + setMigrationCanceled(true) + } + }} + /> + ) + } + + const shouldShowWarning = + MIGRATION_TARGET_DOMAIN === host && + !migrationCompleted && + !migrationCanceled + + return ( + <> + {shouldShowWarning && ( + + (window.location.href = `https://${MIGRATION_TRIGGER_DOMAIN}?from=${MIGRATION_TARGET_DOMAIN}`) + } + onClose={() => { + setMigrationCompleted(new Date().toISOString()) + }} + /> + )} + {children} + + ) +} diff --git a/src/sections/migration/MigrationProvider.utils.ts b/src/sections/migration/MigrationProvider.utils.ts new file mode 100644 index 000000000..a843508ca --- /dev/null +++ b/src/sections/migration/MigrationProvider.utils.ts @@ -0,0 +1,66 @@ +import { Buffer } from "buffer" +import { create } from "zustand" +import { persist } from "zustand/middleware" + +export const MIGRATION_LS_KEYS = [ + "external-tokens", + "address-book", + "toasts", + "rpcUrl", + "hydradx-rpc-list", + "referral-codes", + "dca.settings", + "trade.settings", +] +export const MIGRATION_QUERY_PARAM = "migration" +export const MIGRATION_TRIGGER_DOMAIN = import.meta.env + .VITE_MIGRATION_TRIGGER_DOMAIN as string + +export const MIGRATION_TARGET_DOMAIN = import.meta.env + .VITE_MIGRATION_TARGET_DOMAIN as string + +export const serializeLocalStorage = (keys: string[]): string => { + const data: { [key: string]: any } = {} + + keys.forEach((key) => { + const value = localStorage.getItem(key) + if (value) { + data[key] = JSON.parse(value) + } + }) + + const json = JSON.stringify(data) + return Buffer.from(json, "binary").toString("base64") +} + +export const importToLocalStorage = (data: string) => { + const json = Buffer.from(data, "base64").toString("utf-8") + const ls = JSON.parse(json) + + const keys = Object.keys(ls) + + keys.forEach((key) => { + const value = ls[key] + localStorage.setItem(key, JSON.stringify(value)) + }) +} + +type MigrationStore = { + migrationCompleted: string + setMigrationCompleted: (date: string) => void +} + +export const useMigrationStore = create()( + persist( + (set) => ({ + migrationCompleted: "", + setMigrationCompleted: (date: string) => { + set({ migrationCompleted: date }) + }, + }), + { + name: "hdx-migration", + version: 0, + }, + ), +) diff --git a/src/sections/migration/components/MigrationExportModal.tsx b/src/sections/migration/components/MigrationExportModal.tsx new file mode 100644 index 000000000..a24602956 --- /dev/null +++ b/src/sections/migration/components/MigrationExportModal.tsx @@ -0,0 +1,58 @@ +import { FC } from "react" +import { Button } from "components/Button/Button" +import { Modal } from "components/Modal/Modal" +import { Text } from "components/Typography/Text/Text" +import { + MIGRATION_QUERY_PARAM, + MIGRATION_TARGET_DOMAIN, +} from "sections/migration/MigrationProvider.utils" +import MigrationLogo from "assets/icons/migration/MigrationLogo.svg?react" +import { useTranslation } from "react-i18next" +import { Separator } from "components/Separator/Separator" + +export const MigrationExportModal: FC<{ + data: string + onCancel: () => void +}> = ({ data, onCancel }) => { + const { t } = useTranslation() + + return ( + + + + {t("migration.export.title")} + + + {t("migration.export.description")} + + + {t("migration.export.question")} + + +
+ + + +
+
+ ) +} diff --git a/src/sections/migration/components/MigrationImportModal.tsx b/src/sections/migration/components/MigrationImportModal.tsx new file mode 100644 index 000000000..4d16504f0 --- /dev/null +++ b/src/sections/migration/components/MigrationImportModal.tsx @@ -0,0 +1,85 @@ +import { Button } from "components/Button/Button" +import { Modal } from "components/Modal/Modal" +import { Separator } from "components/Separator/Separator" +import { Text } from "components/Typography/Text/Text" +import { FC, useCallback, useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { + importToLocalStorage, + useMigrationStore, +} from "sections/migration/MigrationProvider.utils" +import MigrationLogo from "assets/icons/migration/MigrationLogo.svg?react" + +export const MigrationImportModal: FC<{ data?: string }> = ({ data }) => { + const { t } = useTranslation() + const [lastImportDate, setLastImportDate] = useState(null) + const { migrationCompleted, setMigrationCompleted } = useMigrationStore() + + const reloadAppWithTimestamp = useCallback( + (date: string) => { + setMigrationCompleted(date) + window.location.href = window.location.origin + }, + [setMigrationCompleted], + ) + + useEffect(() => { + const migrationCompletedOn = + migrationCompleted && migrationCompleted !== "0" + ? new Date(migrationCompleted) + : null + + if (migrationCompletedOn && !!data) { + setLastImportDate(new Date(migrationCompletedOn)) + return + } else if (!!data) { + importToLocalStorage(data) + } + + reloadAppWithTimestamp(data ? new Date().toISOString() : "0") + }, [data, migrationCompleted, reloadAppWithTimestamp]) + + if (!lastImportDate) { + return null + } + + return ( + + + + {t("migration.import.title")} + + + {t("migration.import.description", { date: lastImportDate })} + + +
+ + {data && ( + + )} +
+
+ ) +} diff --git a/src/sections/migration/components/MigrationWarning.tsx b/src/sections/migration/components/MigrationWarning.tsx new file mode 100644 index 000000000..a648844c9 --- /dev/null +++ b/src/sections/migration/components/MigrationWarning.tsx @@ -0,0 +1,72 @@ +import CrossIcon from "assets/icons/CrossIcon.svg?react" +import { + SSecondaryItem, + SWarningMessageContainer, + SWarningMessageContent, +} from "components/WarningMessage/WarningMessage.styled" +import { useTranslation } from "react-i18next" +import Star from "assets/icons/Star.svg?react" +import LinkIcon from "assets/icons/LinkIcon.svg?react" +import { Separator } from "components/Separator/Separator" + +export type MigrationWarningProps = { + onClick: () => void + onClose?: () => void +} + +export const MigrationWarning: React.FC = ({ + onClick, + onClose, +}) => { + const { t } = useTranslation() + return ( + + + + +
+ {t("migration.warning.text")} + + + {t("stats.tiles.link")} + + +
+
+ + { + e.stopPropagation() + onClose?.() + }} + /> + +
+ ) +} diff --git a/src/sections/referrals/ReferralsPage.utils.ts b/src/sections/referrals/ReferralsPage.utils.ts index cbcea7815..a74745271 100644 --- a/src/sections/referrals/ReferralsPage.utils.ts +++ b/src/sections/referrals/ReferralsPage.utils.ts @@ -5,7 +5,7 @@ import { useMemo } from "react" import { useRpcProvider } from "providers/rpcProvider" import { LINKS } from "utils/navigation" -export const REFERRAL_PROD_HOST = "hydradx.io" +export const REFERRAL_PROD_HOST = "hydration.net" export const REFERRAL_PARAM_NAME = "referral" export const REFERRAL_CODE_MAX_LENGTH = 7 export const REFERRAL_CODE_REGEX = /^[a-zA-Z0-9]+$/ diff --git a/src/sections/web3-connect/wallets/WalletConnect.ts b/src/sections/web3-connect/wallets/WalletConnect.ts index d8d9ec1cc..b2eea4445 100644 --- a/src/sections/web3-connect/wallets/WalletConnect.ts +++ b/src/sections/web3-connect/wallets/WalletConnect.ts @@ -19,7 +19,12 @@ import { import { noop } from "utils/helpers" import { EvmParachain } from "@galacticcouncil/xcm-core" -const WC_PROJECT_ID = import.meta.env.VITE_WC_PROJECT_ID as string +// @TODO: Remove when the old domain is deprecated +const isOldDomain = window?.location?.hostname === "app.hydradx.io" + +const WC_PROJECT_ID = isOldDomain + ? "c47a5369367ec2dad6b49c478eb772f9" + : (import.meta.env.VITE_WC_PROJECT_ID as string) const DOMAIN_URL = import.meta.env.VITE_DOMAIN_URL as string export const POLKADOT_CAIP_ID_MAP: Record = { diff --git a/src/state/toasts.ts b/src/state/toasts.ts index cd865fdf2..bd9256c74 100644 --- a/src/state/toasts.ts +++ b/src/state/toasts.ts @@ -92,44 +92,19 @@ const useToastsStore = create()( const storeToasts = window.localStorage.getItem(name) const storeAccount = window.localStorage.getItem("web3-connect") - if (storeAccount == null) return storeToasts + if (!storeAccount) return storeToasts const { state: account } = JSON.parse(storeAccount) - const accountAddress = account?.account.address + const accountAddress = account?.account?.address if (accountAddress) { - const accountToastsDeprecated = window.localStorage.getItem( - `toasts_${accountAddress}`, - ) - - const toastsDeprecated = accountToastsDeprecated - ? safelyParse>(accountToastsDeprecated)?.map( - (toast) => ({ - ...toast, - hidden: true, - }), - ) - : undefined - if (storeToasts != null) { const { state: toastsState } = safelyParse>(storeToasts) ?? {} const allToasts = { ...toastsState?.toasts } - const accountToasts = allToasts[accountAddress] - - if (!accountToasts) { - if (toastsDeprecated != null) { - allToasts[accountAddress] = toastsDeprecated - - window.localStorage.removeItem(`toasts_${accountAddress}`) // remove deprecated storage - } else { - allToasts[accountAddress] = [] - } - } - const allAccounts = Object.keys(allToasts) if (allAccounts?.length) { for (const account of allAccounts) { @@ -167,26 +142,14 @@ const useToastsStore = create()( state: { toasts: allToasts }, }) } else { - if (toastsDeprecated != null) { - window.localStorage.removeItem(`toasts_${accountAddress}`) // remove deprecated storage - return JSON.stringify({ - version: 0, - state: { - toasts: { - [accountAddress]: toastsDeprecated, - }, - }, - }) - } else { - return JSON.stringify({ - version: 0, - state: { - toasts: { - [accountAddress]: [], - }, + return JSON.stringify({ + version: 0, + state: { + toasts: { + [accountAddress]: [], }, - }) - } + }, + }) } } @@ -216,31 +179,7 @@ export const useToast = () => { const toasts = useMemo(() => { if (account?.address) { - const toasts = store.toasts[account.address] - - if (!toasts) { - // check if there is deprecated toast storage - const accountToastsDeprecated = window.localStorage.getItem( - `toasts_${account.address}`, - ) - - if (accountToastsDeprecated) { - const toastsDeprecated = - safelyParse>(accountToastsDeprecated)?.map( - (toast: ToastData) => ({ ...toast, hidden: true }), - ) ?? [] - - store.update(account.address, () => toastsDeprecated) - - window.localStorage.removeItem(`toasts_${account.address}`) // remove deprecated storage - - return toastsDeprecated - } else { - return [] - } - } - - return toasts + return store.toasts[account.address] ?? [] } return [] }, [account?.address, store]) @@ -328,10 +267,11 @@ export const useToast = () => { } const setSidebar = (isOpen: boolean) => { - if (isOpen) + if (isOpen) { store.update(account?.address, (toasts) => toasts.map((toast) => ({ ...toast, hidden: true })), ) + } store.setSidebar(isOpen) }