[WIP] Fix QA

This commit is contained in:
HeesungB 2022-12-16 00:43:18 +09:00
parent bb58fb54ea
commit bcf8827186
25 changed files with 490 additions and 196 deletions

4
.pnp.cjs generated
View File

@ -38,6 +38,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@types/react", "npm:18.0.25"],\ ["@types/react", "npm:18.0.25"],\
["@types/react-dom", "npm:18.0.9"],\ ["@types/react-dom", "npm:18.0.9"],\
["@types/react-modal", "npm:3.13.1"],\ ["@types/react-modal", "npm:3.13.1"],\
["@types/semver", "npm:7.3.13"],\
["@types/styled-components", "npm:5.1.26"],\ ["@types/styled-components", "npm:5.1.26"],\
["@typescript-eslint/eslint-plugin", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.45.0"],\ ["@typescript-eslint/eslint-plugin", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.45.0"],\
["@typescript-eslint/parser", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.45.0"],\ ["@typescript-eslint/parser", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.45.0"],\
@ -62,6 +63,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["react-is", "npm:18.2.0"],\ ["react-is", "npm:18.2.0"],\
["react-modal", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:3.16.1"],\ ["react-modal", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:3.16.1"],\
["react-typed", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:1.2.0"],\ ["react-typed", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:1.2.0"],\
["semver", "npm:7.3.8"],\
["styled-components", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.3.6"],\ ["styled-components", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.3.6"],\
["typescript", "patch:typescript@npm%3A4.9.3#~builtin<compat/typescript>::version=4.9.3&hash=d73830"]\ ["typescript", "patch:typescript@npm%3A4.9.3#~builtin<compat/typescript>::version=4.9.3&hash=d73830"]\
],\ ],\
@ -3985,6 +3987,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@types/react", "npm:18.0.25"],\ ["@types/react", "npm:18.0.25"],\
["@types/react-dom", "npm:18.0.9"],\ ["@types/react-dom", "npm:18.0.9"],\
["@types/react-modal", "npm:3.13.1"],\ ["@types/react-modal", "npm:3.13.1"],\
["@types/semver", "npm:7.3.13"],\
["@types/styled-components", "npm:5.1.26"],\ ["@types/styled-components", "npm:5.1.26"],\
["@typescript-eslint/eslint-plugin", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.45.0"],\ ["@typescript-eslint/eslint-plugin", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.45.0"],\
["@typescript-eslint/parser", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.45.0"],\ ["@typescript-eslint/parser", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.45.0"],\
@ -4009,6 +4012,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["react-is", "npm:18.2.0"],\ ["react-is", "npm:18.2.0"],\
["react-modal", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:3.16.1"],\ ["react-modal", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:3.16.1"],\
["react-typed", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:1.2.0"],\ ["react-typed", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:1.2.0"],\
["semver", "npm:7.3.8"],\
["styled-components", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.3.6"],\ ["styled-components", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.3.6"],\
["typescript", "patch:typescript@npm%3A4.9.3#~builtin<compat/typescript>::version=4.9.3&hash=d73830"]\ ["typescript", "patch:typescript@npm%3A4.9.3#~builtin<compat/typescript>::version=4.9.3&hash=d73830"]\
],\ ],\

View File

@ -0,0 +1,61 @@
import { FunctionComponent } from "react";
import styled from "styled-components";
import color from "../../styles/color";
import ArrowLeftIcon from "../../public/images/svg/arrow-left.svg";
import Image from "next/image";
export const BackButton: FunctionComponent = () => {
return (
<Container
onClick={() => {
location.href = "/";
}}
>
<ContentContainer>
<IconContainer>
<Image
src={ArrowLeftIcon}
fill={true}
sizes="1rem"
alt="arrow left icon"
/>
</IconContainer>
<div>BACK</div>
</ContentContainer>
</Container>
);
};
const Container = styled.div`
display: flex;
justify-content: flex-end;
width: 100%;
padding: 0.7rem 0.5rem;
font-family: "Inter", serif;
font-style: normal;
font-weight: 500;
font-size: 0.8rem;
line-height: 0.8rem;
color: ${color.grey["400"]};
cursor: pointer;
`;
const ContentContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 0.25rem;
`;
const IconContainer = styled.div`
position: relative;
width: 1rem;
height: 1rem;
`;

View File

@ -32,6 +32,7 @@ export const AllChainsItem: FunctionComponent<Props> = (props) => {
<ChainItemContainer <ChainItemContainer
key={chainItem.prefix} key={chainItem.prefix}
isLoading={false} isLoading={false}
checked={allChecked}
onClick={checkHandler} onClick={checkHandler}
> >
<ChainImageContainer width="3rem" height="3rem"> <ChainImageContainer width="3rem" height="3rem">
@ -57,5 +58,5 @@ export const AllChainsItem: FunctionComponent<Props> = (props) => {
const AllChainsContainer = styled.div` const AllChainsContainer = styled.div`
width: 100%; width: 100%;
background-color: ${color.grey["800"]}; background-color: ${color.grey["900"]};
`; `;

View File

@ -15,7 +15,7 @@ interface Props {
export const ChainItem: FunctionComponent<Props> = (props) => { export const ChainItem: FunctionComponent<Props> = (props) => {
const { chainItem, checkedItemHandler, checkedItems, disabled } = props; const { chainItem, checkedItemHandler, checkedItems, disabled } = props;
const [checked, setChecked] = useState(disabled); const [checked, setChecked] = useState(!!disabled);
const checkHandler = () => { const checkHandler = () => {
if (!disabled) { if (!disabled) {
@ -35,6 +35,7 @@ export const ChainItem: FunctionComponent<Props> = (props) => {
key={chainItem.prefix} key={chainItem.prefix}
isLoading={false} isLoading={false}
disabled={disabled} disabled={disabled}
checked={checked}
onClick={checkHandler} onClick={checkHandler}
> >
<ChainImageContainer width="3rem" height="3rem"> <ChainImageContainer width="3rem" height="3rem">
@ -71,10 +72,17 @@ export const ChainItemContainer = styled.div<{
cursor: pointer; cursor: pointer;
opacity: ${(props) => (props.disabled ? "0.3" : "1")}; opacity: ${(props) => (props.disabled ? "0.5" : "1")};
background-color: ${(props) =>
props.disabled
? color.black
: props.checked
? color.grey["800"]
: color.grey["900"]};
&:hover { &:hover {
background: ${(props) => (props.isLoading ? null : color.grey["600"])}; background: ${(props) => (props.isLoading ? null : color.grey["700"])};
} }
`; `;

View File

@ -52,7 +52,7 @@ export const ChainList: FunctionComponent<Props> = (props) => {
}, [checkedItems]); }, [checkedItems]);
return ( return (
<ChainContainer color={color.grey["800"]}> <ChainContainer color={color.grey["900"]}>
{chainList.map((chainItem) => ( {chainList.map((chainItem) => (
<ChainItem <ChainItem
key={chainItem.address} key={chainItem.address}
@ -78,8 +78,10 @@ export const ChainContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
max-height: 33rem; //max-height: 33rem;
overflow: scroll; overflow: scroll;
flex: 1;
background-color: ${(props) => props.color}; background-color: ${(props) => props.color};
`; `;

View File

@ -1,24 +1,34 @@
import { FunctionComponent } from "react"; import { FunctionComponent, useEffect, useState } from "react";
import ArrowRightIcon from "../../public/images/svg/arrow-right.svg"; import ArrowRightIcon from "../../public/images/svg/arrow-right.svg";
import color from "../../styles/color"; import color from "../../styles/color";
import { Flex1 } from "../../styles/flex-1"; import { Flex1 } from "../../styles/flex-1";
import styled from "styled-components"; import styled from "styled-components";
import Image from "next/image"; import Image from "next/image";
import { import {
MINIMUM_VERSION,
SELECTED_WALLET_KEY, SELECTED_WALLET_KEY,
WALLET_INSTALL_URL, WALLET_INSTALL_URL,
WalletType, WalletType,
} from "../../constants/wallet"; } from "../../constants/wallet";
import { getKeplrFromWindow, KeplrWallet } from "../../wallets"; import { getKeplrFromWindow, KeplrWallet } from "../../wallets";
import { loginWithTwitter } from "../../queries"; import { loginWithTwitter } from "../../queries";
import {
KEPLR_NOT_FOUND_ERROR,
KEPLR_VERSION_ERROR,
} from "../../constants/error-message";
import semver from "semver/preload";
interface Props { interface Props {
wallet: WalletType; wallet: WalletType;
} }
// Todo: Wallet 관련된 부분을 Context로 빼는 부분
export const WalletItem: FunctionComponent<Props> = (props: Props) => { export const WalletItem: FunctionComponent<Props> = (props: Props) => {
const { wallet } = props; const { wallet } = props;
const [isInstalled, setIsInstalled] = useState<boolean>();
useEffect(() => {
setIsInstalled(!!window.keplr);
}, []);
const onClickWalletItem = async () => { const onClickWalletItem = async () => {
try { try {
@ -38,6 +48,11 @@ export const WalletItem: FunctionComponent<Props> = (props: Props) => {
if (keplr === undefined) { if (keplr === undefined) {
window.location.href = WALLET_INSTALL_URL; window.location.href = WALLET_INSTALL_URL;
throw new Error(KEPLR_NOT_FOUND_ERROR);
}
if (semver.lt(keplr.version, MINIMUM_VERSION)) {
throw new Error(KEPLR_VERSION_ERROR);
} }
if (keplr) { if (keplr) {
@ -62,7 +77,11 @@ export const WalletItem: FunctionComponent<Props> = (props: Props) => {
</WalletIcon> </WalletIcon>
<WalletContentContainer> <WalletContentContainer>
<WalletName>{wallet.name}</WalletName> <WalletName>{wallet.name}</WalletName>
{wallet.isReady ? null : ( {wallet.isReady ? (
isInstalled ? null : (
<WalletDescription>Go to install Keplr Extension</WalletDescription>
)
) : (
<WalletDescription>Comming soon</WalletDescription> <WalletDescription>Comming soon</WalletDescription>
)} )}
</WalletContentContainer> </WalletContentContainer>

View File

@ -31,10 +31,9 @@ export const LogoContainer = styled.div`
justify-content: center; justify-content: center;
position: absolute; position: absolute;
width: 10rem; width: 10rem;
height: 5rem; height: 5rem;
margin-top: 80px; margin-top: 5rem;
margin-left: 80px; margin-left: 5rem;
`; `;

View File

@ -7,7 +7,6 @@ export const PrimaryButton = styled.button`
border: none; border: none;
background-color: ${color.orange["100"]};
padding: 11px 30px; padding: 11px 30px;
font-family: "Inter", serif; font-family: "Inter", serif;
@ -17,6 +16,7 @@ export const PrimaryButton = styled.button`
line-height: 20px; line-height: 20px;
color: ${color.orange["50"]}; color: ${color.orange["50"]};
background-color: ${color.orange["100"]};
cursor: pointer; cursor: pointer;
@ -26,6 +26,8 @@ export const PrimaryButton = styled.button`
} }
&:disabled { &:disabled {
background-color: ${color.orange["200"]}; opacity: 0.5;
background-color: ${color.orange["300"]};
} }
`; `;

View File

@ -27,99 +27,105 @@ import {
} from "../chain-list"; } from "../chain-list";
export const SkeletonChainList: FunctionComponent = () => ( export const SkeletonChainList: FunctionComponent = () => (
<ContentContainer> <SkeletonContainer>
<ProfileContainer color={color.grey["700"]}> <ContentContainer>
<SkeletonCircle width="5.5rem" height="5.5rem" /> <ProfileContainer color={color.grey["700"]}>
<SkeletonCircle width="5.5rem" height="5.5rem" />
<ProfileContentContainer> <ProfileContentContainer>
<ProfileNameContainer> <ProfileNameContainer>
<SkeletonText width="5rem" height="1.5rem" /> <SkeletonText width="5rem" height="1.5rem" />
</ProfileNameContainer> </ProfileNameContainer>
<ProfileUserNameContainer> <ProfileUserNameContainer>
<SkeletonText width="5rem" height="1rem" /> <SkeletonText width="5rem" height="1rem" />
</ProfileUserNameContainer> </ProfileUserNameContainer>
<ProfileFollowContainer> <ProfileFollowContainer>
<SkeletonText width="8rem" height="1rem" /> <SkeletonText width="8rem" height="1rem" />
<SkeletonText width="8rem" height="1rem" /> <SkeletonText width="8rem" height="1rem" />
</ProfileFollowContainer> </ProfileFollowContainer>
<SkeletonText width="20rem" height="1rem" /> <SkeletonText width="20rem" height="1rem" />
</ProfileContentContainer> </ProfileContentContainer>
</ProfileContainer> </ProfileContainer>
<ChainListTitleContainer> <ChainListTitleContainer>
<SkeletonTitle /> <SkeletonTitle />
</ChainListTitleContainer> </ChainListTitleContainer>
<ChainContainer color={color.grey["700"]}> <ChainContainer color={color.grey["700"]}>
<ChainItemContainer isLoading={true}> <ChainItemContainer isLoading={true}>
<ChainImageContainer width="3rem" height="3rem"> <ChainImageContainer width="3rem" height="3rem">
<SkeletonCircle width="3rem" height="3rem" /> <SkeletonCircle width="3rem" height="3rem" />
</ChainImageContainer> </ChainImageContainer>
<ChainInfoContainer> <ChainInfoContainer>
<SkeletonText width="4rem" height="1rem" /> <SkeletonText width="4rem" height="1rem" />
<SkeletonText width="12rem" height="1rem" /> <SkeletonText width="12rem" height="1rem" />
</ChainInfoContainer> </ChainInfoContainer>
</ChainItemContainer> </ChainItemContainer>
<SkeletonDivider /> <SkeletonDivider />
<ChainItemContainer isLoading={true}> <ChainItemContainer isLoading={true}>
<ChainImageContainer width="3rem" height="3rem"> <ChainImageContainer width="3rem" height="3rem">
<SkeletonCircle width="3rem" height="3rem" /> <SkeletonCircle width="3rem" height="3rem" />
</ChainImageContainer> </ChainImageContainer>
<ChainInfoContainer> <ChainInfoContainer>
<SkeletonText width="4rem" height="1rem" /> <SkeletonText width="4rem" height="1rem" />
<SkeletonText width="12rem" height="1rem" /> <SkeletonText width="12rem" height="1rem" />
</ChainInfoContainer> </ChainInfoContainer>
</ChainItemContainer> </ChainItemContainer>
<SkeletonDivider /> <SkeletonDivider />
<ChainItemContainer isLoading={true}> <ChainItemContainer isLoading={true}>
<ChainImageContainer width="3rem" height="3rem"> <ChainImageContainer width="3rem" height="3rem">
<SkeletonCircle width="3rem" height="3rem" /> <SkeletonCircle width="3rem" height="3rem" />
</ChainImageContainer> </ChainImageContainer>
<ChainInfoContainer> <ChainInfoContainer>
<SkeletonText width="4rem" height="1rem" /> <SkeletonText width="4rem" height="1rem" />
<SkeletonText width="12rem" height="1rem" /> <SkeletonText width="12rem" height="1rem" />
</ChainInfoContainer> </ChainInfoContainer>
</ChainItemContainer> </ChainItemContainer>
<SkeletonDivider /> <SkeletonDivider />
<ChainItemContainer isLoading={true}> <ChainItemContainer isLoading={true}>
<ChainImageContainer width="3rem" height="3rem"> <ChainImageContainer width="3rem" height="3rem">
<SkeletonCircle width="3rem" height="3rem" /> <SkeletonCircle width="3rem" height="3rem" />
</ChainImageContainer> </ChainImageContainer>
<ChainInfoContainer> <ChainInfoContainer>
<SkeletonText width="4rem" height="1rem" /> <SkeletonText width="4rem" height="1rem" />
<SkeletonText width="12rem" height="1rem" /> <SkeletonText width="12rem" height="1rem" />
</ChainInfoContainer> </ChainInfoContainer>
</ChainItemContainer> </ChainItemContainer>
<SkeletonDivider /> <SkeletonDivider />
<ChainItemContainer isLoading={true}> <ChainItemContainer isLoading={true}>
<ChainImageContainer width="3rem" height="3rem"> <ChainImageContainer width="3rem" height="3rem">
<SkeletonCircle width="3rem" height="3rem" /> <SkeletonCircle width="3rem" height="3rem" />
</ChainImageContainer> </ChainImageContainer>
<ChainInfoContainer> <ChainInfoContainer>
<SkeletonText width="4rem" height="1rem" /> <SkeletonText width="4rem" height="1rem" />
<SkeletonText width="12rem" height="1rem" /> <SkeletonText width="12rem" height="1rem" />
</ChainInfoContainer> </ChainInfoContainer>
</ChainItemContainer> </ChainItemContainer>
<SkeletonDivider /> <SkeletonDivider />
</ChainContainer> </ChainContainer>
<ButtonContainer> <ButtonContainer>
<SkeletonButton /> <SkeletonButton />
</ButtonContainer> </ButtonContainer>
</ContentContainer> </ContentContainer>
</SkeletonContainer>
); );
const SkeletonContainer = styled.div`
padding: 2.4rem 0;
`;
const SkeletonTitle = styled.div` const SkeletonTitle = styled.div`
width: 8rem; width: 8rem;
height: 1.5rem; height: 1.5rem;

View File

@ -12,7 +12,7 @@ export const TwitterProfile: FunctionComponent<Props> = (props) => {
const { twitterProfileInformation } = props; const { twitterProfileInformation } = props;
return ( return (
<ProfileContainer color={color.grey["800"]}> <ProfileContainer color={color.grey["900"]}>
<ProfileImageContainer> <ProfileImageContainer>
<Image <Image
src={twitterProfileInformation?.profile_image_url ?? ""} src={twitterProfileInformation?.profile_image_url ?? ""}

View File

@ -2,3 +2,4 @@ export const TWITTER_LOGIN_ERROR = "Twitter login access denied";
export const TWITTER_PROFILE_ERROR = "Twitter auth code is not valid"; export const TWITTER_PROFILE_ERROR = "Twitter auth code is not valid";
export const KEPLR_NOT_FOUND_ERROR = "Can't fount window.keplr"; export const KEPLR_NOT_FOUND_ERROR = "Can't fount window.keplr";
export const KEPLR_VERSION_ERROR = "You should update keplr";

View File

@ -1,9 +1,13 @@
export const MainChainId = "osmo-test-4"; export const MainChainId = "osmo-test-4";
export const REFERRAL_KEY = "icns-referral";
export const RPC_URL = "https://rpc.testnet.osmosis.zone"; export const RPC_URL = "https://rpc.testnet.osmosis.zone";
export const REST_URL = "https://lcd.testnet.osmosis.zone"; export const REST_URL = "https://lcd.testnet.osmosis.zone";
// TODO: .evn에 없으면 디폴트값 설정 // TODO: .evn에 없으면 디폴트값 설정
export const NAME_NFT_ADDRESS =
"osmo1xahnjn872smah6xle8n3z5a5teqq390qr959l805mkuw0kcy8g5smtdagg";
export const REGISTRAR_ADDRESS = export const REGISTRAR_ADDRESS =
"osmo1npn97g7hsgqlp70rw8nhd7c7vyvkukv9x0n25sn4fk5mgcjlz4gq9zlgf3"; "osmo1npn97g7hsgqlp70rw8nhd7c7vyvkukv9x0n25sn4fk5mgcjlz4gq9zlgf3";
export const RESOLVER_ADDRESS = export const RESOLVER_ADDRESS =

View File

@ -7,3 +7,5 @@ export const twitterOAuthScopes = [
]; ];
export const twitterApiBaseUrl = "https://api.twitter.com/2"; export const twitterApiBaseUrl = "https://api.twitter.com/2";
export const SHARE_URL = "https://twitter.com/share";

View File

@ -7,6 +7,8 @@ export const WALLET_INSTALL_URL =
"https://chrome.google.com/webstore/detail/keplr/dmkamcknogkgcdfhhbddcghachkejeap"; "https://chrome.google.com/webstore/detail/keplr/dmkamcknogkgcdfhhbddcghachkejeap";
export const SELECTED_WALLET_KEY = "SELECTED_WALLET_KEY"; export const SELECTED_WALLET_KEY = "SELECTED_WALLET_KEY";
export const MINIMUM_VERSION = "0.11.22";
export type WalletName = "Keplr" | "Cosmostation"; export type WalletName = "Keplr" | "Cosmostation";
export interface WalletType { export interface WalletType {
name: WalletName; name: WalletName;

View File

@ -7,6 +7,7 @@ export const makeClaimMessage = (
senderAddress: string, senderAddress: string,
twitterUserName: string, twitterUserName: string,
verificationList: any[], verificationList: any[],
referral?: string,
): CosmwasmExecuteMessageResult => { ): CosmwasmExecuteMessageResult => {
return makeCosmwasmExecMsg( return makeCosmwasmExecMsg(
senderAddress, senderAddress,
@ -26,6 +27,7 @@ export const makeClaimMessage = (
}; };
} }
}), }),
referral,
}, },
}, },
[ContractFee], [ContractFee],

View File

@ -24,6 +24,7 @@
"react-is": "^18.2.0", "react-is": "^18.2.0",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-typed": "^1.2.0", "react-typed": "^1.2.0",
"semver": "^7.3.8",
"styled-components": "^5.3.6" "styled-components": "^5.3.6"
}, },
"devDependencies": { "devDependencies": {
@ -33,6 +34,7 @@
"@types/react": "18.0.25", "@types/react": "18.0.25",
"@types/react-dom": "18.0.9", "@types/react-dom": "18.0.9",
"@types/react-modal": "^3", "@types/react-modal": "^3",
"@types/semver": "^7",
"@types/styled-components": "^5", "@types/styled-components": "^5",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0", "@typescript-eslint/parser": "^5.45.0",

View File

@ -9,31 +9,56 @@ import color from "../../styles/color";
import AlertCircleOutlineIcon from "../../public/images/svg/alert-circle-outline.svg"; import AlertCircleOutlineIcon from "../../public/images/svg/alert-circle-outline.svg";
import TwitterIcon from "../../public/images/svg/twitter-icon.svg"; import TwitterIcon from "../../public/images/svg/twitter-icon.svg";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { TendermintTxTracer } from "@keplr-wallet/cosmos"; import { TendermintTxTracer } from "@keplr-wallet/cosmos";
import { queryAddressesFromTwitterName } from "../../queries";
import { RegisteredAddresses } from "../../types";
import { SHARE_URL } from "../../constants/twitter";
export default function CompletePage() { export default function CompletePage() {
const router = useRouter(); const router = useRouter();
const [registeredAddressed, setRegisteredAddressed] =
useState<RegisteredAddresses[]>();
const [availableAddress, setAvailableAddress] = useState("");
useEffect(() => { useEffect(() => {
const { txHash } = router.query; const { txHash, twitterUsername } = router.query;
if (txHash) { if (txHash && twitterUsername) {
traceTX(txHash as string); initialize(txHash as string, twitterUsername as string);
} }
}, []); }, [router.query]);
const traceTX = async (txHash: string) => { const initialize = async (txHash: string, twitterUserName: string) => {
const txTracer = new TendermintTxTracer( const txTracer = new TendermintTxTracer(
"https://rpc.testnet.osmosis.zone", "https://rpc.testnet.osmosis.zone",
"/websocket", "/websocket",
); );
const result = await txTracer.traceTx(Buffer.from(txHash, "hex")); const result: { code?: number } = await txTracer.traceTx(
Buffer.from(txHash, "hex"),
);
console.log(result); if (result.code || result.code === 0) {
const addresses = await queryAddressesFromTwitterName(twitterUserName);
setRegisteredAddressed(addresses.data.addresses);
}
};
// Todo rsult => 확인 후에 확인 const onClickShareButton = () => {
const { twitterUsername } = router.query;
const width = 500;
const height = 700;
window.open(
`${SHARE_URL}?url=https://www.icns.xyz/&text=${twitterUsername}`,
"Share Twitter",
`top=${(window.screen.height - height) / 2}, left=${
(window.screen.width - width) / 2
}, width=${width}, height=${height}, status=no, menubar=no, toolbar=no, resizable=no`,
);
}; };
return ( return (
@ -41,22 +66,31 @@ export default function CompletePage() {
<Logo /> <Logo />
<MainContainer> <MainContainer>
<MainTitle>Your Name is Active Now!</MainTitle> <MainTitle>
<div>Your Name is Active Now!</div>
</MainTitle>
<ContentContainer> <ContentContainer>
<RecipentContainer> <RecipentContainer>
<RecipentTitle>Recipent</RecipentTitle> <RecipentTitle>Recipent</RecipentTitle>
<AddressContainer> <AddressContainer>
kingstarcookies. {`${router.query.twitterUsername}.`}
<Typed {registeredAddressed && (
strings={["osmo", "cosmos"]} <Typed
typeSpeed={150} strings={registeredAddressed.map(
backSpeed={150} (address) => address.bech32_prefix,
backDelay={1000} )}
loop typeSpeed={150}
smartBackspace backSpeed={150}
/> backDelay={1000}
loop
smartBackspace
onStringTyped={(arrayPos: number) => {
setAvailableAddress(registeredAddressed[arrayPos].address);
}}
/>
)}
</AddressContainer> </AddressContainer>
<AvailableAddressText>available address</AvailableAddressText> <AvailableAddressText>{availableAddress}</AvailableAddressText>
</RecipentContainer> </RecipentContainer>
</ContentContainer> </ContentContainer>
@ -70,7 +104,7 @@ export default function CompletePage() {
</DescriptionText> </DescriptionText>
</DescriptionContainer> </DescriptionContainer>
<ShareButtonContainer> <ShareButtonContainer onClick={onClickShareButton}>
<ShareButtonText>SHARE MY NAME</ShareButtonText> <ShareButtonText>SHARE MY NAME</ShareButtonText>
<Image src={TwitterIcon} alt="twitter icon" /> <Image src={TwitterIcon} alt="twitter icon" />
</ShareButtonContainer> </ShareButtonContainer>
@ -95,19 +129,22 @@ const MainContainer = styled.div`
`; `;
const MainTitle = styled.div` const MainTitle = styled.div`
display: flex;
align-items: center;
justify-content: center;
font-family: "Inter", serif; font-family: "Inter", serif;
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-size: 2rem; font-size: 2rem;
line-height: 2rem; line-height: 2rem;
padding: 1rem; height: 5rem;
`; `;
const ContentContainer = styled.div` const ContentContainer = styled.div`
width: 30rem; width: 30rem;
margin-top: 1rem;
padding: 2rem 2rem; padding: 2rem 2rem;
background-color: ${color.grey["900"]}; background-color: ${color.grey["900"]};
@ -152,19 +189,22 @@ const AvailableAddressText = styled.div`
font-size: 0.75rem; font-size: 0.75rem;
line-height: 0.75rem; line-height: 0.75rem;
min-height: 0.75rem;
color: ${color.blue}; color: ${color.blue};
`; `;
const DescriptionContainer = styled.div` const DescriptionContainer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center;
gap: 1rem; gap: 1rem;
width: 30rem; width: 30rem;
margin-top: 1.5rem; margin-top: 1.5rem;
padding: 1.5rem 2rem; padding: 1.25rem 2rem;
background-color: ${color.grey["900"]}; background-color: ${color.grey["900"]};
`; `;
@ -183,7 +223,7 @@ const DescriptionText = styled.div`
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-size: 0.8rem; font-size: 0.8rem;
line-height: 0.8rem; line-height: 140%;
color: ${color.grey["400"]}; color: ${color.grey["400"]};
`; `;

View File

@ -16,6 +16,8 @@ import CheckIcon from "../public/images/svg/check-icon.svg";
import { Logo } from "../components/logo"; import { Logo } from "../components/logo";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SELECTED_WALLET_KEY } from "../constants/wallet"; import { SELECTED_WALLET_KEY } from "../constants/wallet";
import { replaceToInstallPage } from "../utils/url";
import { REFERRAL_KEY } from "../constants/icns";
export default function Home() { export default function Home() {
const [isModalOpen, setModalOpen] = useState(false); const [isModalOpen, setModalOpen] = useState(false);
@ -25,6 +27,17 @@ export default function Home() {
}; };
useEffect(() => { useEffect(() => {
localStorage.removeItem(REFERRAL_KEY);
if (window.location.search) {
const [, referral] =
window.location.search.match(/^(?=.*referral=([^&]+)|).+$/) || [];
if (referral) {
localStorage.setItem(REFERRAL_KEY, referral);
}
}
localStorage.removeItem(SELECTED_WALLET_KEY); localStorage.removeItem(SELECTED_WALLET_KEY);
}, []); }, []);
@ -62,7 +75,8 @@ export default function Home() {
/> />
</CheckIconContainer> </CheckIconContainer>
You are a <CheckBoldText>&nbsp;keplr&nbsp;</CheckBoldText> user. You are a <CheckBoldText>&nbsp;keplr&nbsp;</CheckBoldText> user.
if not, you can install here if not, you can install&nbsp;
<InstallLInk onClick={replaceToInstallPage}>HERE</InstallLInk>
</CheckContainer> </CheckContainer>
<CheckContainer> <CheckContainer>
<CheckIconContainer> <CheckIconContainer>
@ -182,7 +196,16 @@ const CheckContainer = styled.div`
text-transform: uppercase; text-transform: uppercase;
color: ${color.grey["300"]}; padding-left: 0.75rem;
color: ${color.grey["400"]};
`;
const InstallLInk = styled.a`
color: ${color.grey["400"]};
text-decoration: underline;
cursor: pointer;
`; `;
const CheckBoldText = styled.span` const CheckBoldText = styled.span`

View File

@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
// Types // Types
import { import {
ChainItemType, ChainItemType,
QueryError,
RegisteredAddresses, RegisteredAddresses,
TwitterProfileType, TwitterProfileType,
} from "../../types"; } from "../../types";
@ -32,11 +33,17 @@ import { ChainIdHelper } from "@keplr-wallet/cosmos";
import AllChainsIcon from "../../public/images/svg/all-chains-icon.svg"; import AllChainsIcon from "../../public/images/svg/all-chains-icon.svg";
import { AllChainsItem } from "../../components/chain-list/all-chains-item"; import { AllChainsItem } from "../../components/chain-list/all-chains-item";
import { SearchInput } from "../../components/search-input"; import { SearchInput } from "../../components/search-input";
import { MainChainId, RESOLVER_ADDRESS, REST_URL } from "../../constants/icns"; import {
MainChainId,
REFERRAL_KEY,
RESOLVER_ADDRESS,
REST_URL,
} from "../../constants/icns";
import { import {
fetchTwitterInfo, fetchTwitterInfo,
queryAddressesFromTwitterName, queryAddressesFromTwitterName,
queryOwnerOfTwitterName,
queryRegisteredTwitterId, queryRegisteredTwitterId,
verifyTwitterAccount, verifyTwitterAccount,
} from "../../queries"; } from "../../queries";
@ -46,6 +53,8 @@ import {
TWITTER_LOGIN_ERROR, TWITTER_LOGIN_ERROR,
} from "../../constants/error-message"; } from "../../constants/error-message";
import { makeClaimMessage, makeSetRecordMessage } from "../../messages"; import { makeClaimMessage, makeSetRecordMessage } from "../../messages";
import Axios, { AxiosError } from "axios";
import { BackButton } from "../../components/back-button";
export default function VerificationPage() { export default function VerificationPage() {
const router = useRouter(); const router = useRouter();
@ -69,6 +78,9 @@ export default function VerificationPage() {
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const [isOwner, setIsOwner] = useState(false);
const [isAgree, setIsAgree] = useState(false);
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
if (window.location.search) { if (window.location.search) {
@ -78,7 +90,7 @@ export default function VerificationPage() {
); );
// Initialize Wallet // Initialize Wallet
await initWallet(); const keplrWallet = await initWallet();
// Fetch Twitter Profile // Fetch Twitter Profile
const twitterInfo = await fetchTwitterInfo(state, code); const twitterInfo = await fetchTwitterInfo(state, code);
@ -94,10 +106,19 @@ export default function VerificationPage() {
}); });
if ("data" in registeredQueryResponse) { if ("data" in registeredQueryResponse) {
const ownerOfQueryResponse = await queryOwnerOfTwitterName(
registeredQueryResponse.data.name,
);
const addressesQueryResponse = await queryAddressesFromTwitterName( const addressesQueryResponse = await queryAddressesFromTwitterName(
registeredQueryResponse.data.name, registeredQueryResponse.data.name,
); );
if (keplrWallet) {
const key = await keplrWallet.getKey(MainChainId);
setIsOwner(ownerOfQueryResponse.data.owner === key.bech32Address);
}
setRegisteredChainList(addressesQueryResponse.data.addresses); setRegisteredChainList(addressesQueryResponse.data.addresses);
} }
} catch (error) { } catch (error) {
@ -153,6 +174,8 @@ export default function VerificationPage() {
await fetchChainList(keplrWallet); await fetchChainList(keplrWallet);
setWallet(keplrWallet); setWallet(keplrWallet);
return keplrWallet;
} else { } else {
ErrorHandler(KEPLR_NOT_FOUND_ERROR); ErrorHandler(KEPLR_NOT_FOUND_ERROR);
} }
@ -227,84 +250,99 @@ export default function VerificationPage() {
}; };
const onClickRegistration = async () => { const onClickRegistration = async () => {
const { state, code } = checkTwitterAuthQueryParameter( try {
window.location.search, const { state, code } = checkTwitterAuthQueryParameter(
); window.location.search,
const twitterInfo = await fetchTwitterInfo(state, code);
const adr36Infos = await checkAdr36();
if (wallet && adr36Infos) {
const key = await wallet.getKey(MainChainId);
const icnsVerificationList = await verifyTwitterAccount(
key.bech32Address,
twitterInfo.accessToken,
); );
const twitterInfo = await fetchTwitterInfo(state, code);
const registerMsg = makeClaimMessage( const adr36Infos = await checkAdr36();
key.bech32Address,
twitterInfo.username,
icnsVerificationList,
);
const addressMsgs = adr36Infos.map((adr36Info) => { if (wallet && adr36Infos) {
return makeSetRecordMessage( const key = await wallet.getKey(MainChainId);
const icnsVerificationList = await verifyTwitterAccount(
key.bech32Address,
twitterInfo.accessToken,
);
const registerMsg = makeClaimMessage(
key.bech32Address, key.bech32Address,
twitterInfo.username, twitterInfo.username,
adr36Info, icnsVerificationList,
localStorage.getItem(REFERRAL_KEY) ?? undefined,
); );
});
const aminoMsgs = twitterAuthInfo?.isRegistered const addressMsgs = adr36Infos.map((adr36Info) => {
? [] return makeSetRecordMessage(
: [registerMsg.amino]; key.bech32Address,
const protoMsgs = twitterAuthInfo?.isRegistered twitterInfo.username,
? [] adr36Info,
: [registerMsg.proto]; );
});
for (const addressMsg of addressMsgs) { const aminoMsgs = twitterAuthInfo?.isRegistered
aminoMsgs.push(addressMsg.amino); ? []
protoMsgs.push(addressMsg.proto); : [registerMsg.amino];
const protoMsgs = twitterAuthInfo?.isRegistered
? []
: [registerMsg.proto];
for (const addressMsg of addressMsgs) {
aminoMsgs.push(addressMsg.amino);
protoMsgs.push(addressMsg.proto);
}
const chainInfo = {
chainId: MainChainId,
rest: REST_URL,
};
const simulated = await simulateMsgs(
chainInfo,
key.bech32Address,
{
proto: protoMsgs,
},
{
amount: [],
},
);
const txHash = await sendMsgs(
wallet,
chainInfo,
key.bech32Address,
{
amino: aminoMsgs,
proto: protoMsgs,
},
{
amount: [],
gas: Math.floor(simulated.gasUsed * 1.5).toString(),
},
);
await router.push({
pathname: "complete",
query: {
txHash: Buffer.from(txHash).toString("hex"),
twitterUsername: twitterInfo.username,
},
});
}
} catch (error) {
if (Axios.isAxiosError(error)) {
console.error((error?.response?.data as QueryError).message);
} }
const chainInfo = {
chainId: MainChainId,
rest: REST_URL,
};
const simulated = await simulateMsgs(
chainInfo,
key.bech32Address,
{
proto: protoMsgs,
},
{
amount: [],
},
);
const txHash = await sendMsgs(
wallet,
chainInfo,
key.bech32Address,
{
amino: aminoMsgs,
proto: protoMsgs,
},
{
amount: [],
gas: Math.floor(simulated.gasUsed * 1.5).toString(),
},
);
await router.push({
pathname: "complete",
query: { txHash: Buffer.from(txHash).toString("hex") },
});
} }
}; };
const isRegisterButtonDisable =
checkedItems.size < 1 ||
(!isOwner && registeredChainList.length > 0) ||
!isAgree;
return ( return (
<Container> <Container>
<Logo /> <Logo />
@ -314,6 +352,7 @@ export default function VerificationPage() {
<SkeletonChainList /> <SkeletonChainList />
) : ( ) : (
<ContentContainer> <ContentContainer>
<BackButton />
<TwitterProfile twitterProfileInformation={twitterAuthInfo} /> <TwitterProfile twitterProfileInformation={twitterAuthInfo} />
<ChainListTitleContainer> <ChainListTitleContainer>
@ -351,9 +390,21 @@ export default function VerificationPage() {
setCheckedItems={setCheckedItems} setCheckedItems={setCheckedItems}
/> />
<ButtonContainer> <AgreeContainer>
<AgreeCheckBox
type="checkbox"
checked={isAgree}
onClick={() => {
setIsAgree(!isAgree);
}}
readOnly
/>
I check that Osmo is required for this transaction
</AgreeContainer>
<ButtonContainer disabled={isRegisterButtonDisable}>
<PrimaryButton <PrimaryButton
disabled={checkedItems.size < 1} disabled={isRegisterButtonDisable}
onClick={onClickRegistration} onClick={onClickRegistration}
> >
Register Register
@ -375,6 +426,10 @@ const MainContainer = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
height: 100vh;
padding: 2.7rem 0;
color: white; color: white;
`; `;
@ -384,15 +439,14 @@ export const ContentContainer = styled.div`
align-items: center; align-items: center;
width: 40rem; width: 40rem;
margin-top: 5rem;
`; `;
export const ButtonContainer = styled.div` export const ButtonContainer = styled.div<{ disabled?: boolean }>`
width: 12rem; width: 12rem;
height: 4rem; height: 4rem;
margin-top: 2rem; background-color: ${(props) =>
props.disabled ? color.orange["300"] : color.orange["100"]};
`; `;
export const ChainListTitleContainer = styled.div` export const ChainListTitleContainer = styled.div`
@ -414,3 +468,26 @@ const ChainListTitle = styled.div`
color: ${color.white}; color: ${color.white};
`; `;
const AgreeContainer = styled.div`
display: flex;
align-items: center;
gap: 0.5rem;
font-family: "Inter", serif;
font-style: normal;
font-weight: 500;
font-size: 0.8rem;
line-height: 0.8rem;
text-transform: uppercase;
color: ${color.grey["400"]};
padding: 2rem 0;
`;
const AgreeCheckBox = styled.input.attrs({ type: "checkbox" })`
width: 1.2rem;
height: 1.2rem;
`;

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.57812 14.0625L3.51562 9L8.57812 3.9375M4.21875 9H14.4844" stroke="#5B5B5B" stroke-width="1.5" stroke-linecap="square"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

View File

@ -1,5 +1,6 @@
import { request } from "../utils/url"; import { request } from "../utils/url";
import { import {
NAME_NFT_ADDRESS,
REGISTRAR_ADDRESS, REGISTRAR_ADDRESS,
RESOLVER_ADDRESS, RESOLVER_ADDRESS,
REST_URL, REST_URL,
@ -8,6 +9,7 @@ import { Buffer } from "buffer/";
import { import {
AddressesQueryResponse, AddressesQueryResponse,
NameByTwitterIdQueryResponse, NameByTwitterIdQueryResponse,
OwnerOfQueryResponse,
QueryError, QueryError,
} from "../types"; } from "../types";
@ -20,11 +22,14 @@ export const queryRegisteredTwitterId = async (
const msg = { const msg = {
name_by_twitter_id: { twitter_id: twitterId }, name_by_twitter_id: { twitter_id: twitterId },
}; };
return request<NameByTwitterIdQueryResponse>( return request<NameByTwitterIdQueryResponse>(
getCosmwasmQueryUrl( getCosmwasmQueryUrl(
REGISTRAR_ADDRESS, REGISTRAR_ADDRESS,
Buffer.from(JSON.stringify(msg)).toString("base64"), Buffer.from(JSON.stringify(msg)).toString("base64"),
), ),
{},
true,
); );
}; };
@ -42,3 +47,20 @@ export const queryAddressesFromTwitterName = async (
), ),
); );
}; };
export const queryOwnerOfTwitterName = async (
twitterUsername: string,
): Promise<OwnerOfQueryResponse> => {
const msg = {
owner_of: { token_id: twitterUsername },
};
return request<OwnerOfQueryResponse>(
getCosmwasmQueryUrl(
NAME_NFT_ADDRESS,
Buffer.from(JSON.stringify(msg)).toString("base64"),
),
{},
true,
);
};

View File

@ -16,8 +16,8 @@ const grey = {
400: "#5B5B5B", 400: "#5B5B5B",
500: "#424242", 500: "#424242",
600: "#333333", 600: "#333333",
700: "#2B2B2B", 700: "#222222",
800: "#242424", 800: "#1D1D1D",
900: "#181818", 900: "#181818",
}; };
const black = "#121212"; const black = "#121212";

View File

@ -59,3 +59,9 @@ export interface QueryError {
code: number; code: number;
message: string; message: string;
} }
export interface OwnerOfQueryResponse {
data: {
owner: string;
};
}

View File

@ -1,13 +1,15 @@
import { TwitterLoginSuccess } from "../types"; import { TwitterLoginSuccess } from "../types";
import { TWITTER_LOGIN_ERROR } from "../constants/error-message"; import { TWITTER_LOGIN_ERROR } from "../constants/error-message";
import { WALLET_INSTALL_URL } from "../constants/wallet";
export function request<TResponse>( export function request<TResponse>(
url: string, url: string,
config: RequestInit = {}, config: RequestInit = {},
isIgnore?: boolean,
): Promise<TResponse> { ): Promise<TResponse> {
return fetch(url, config) return fetch(url, config)
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok && !isIgnore) {
throw new Error( throw new Error(
`This is an HTTP error: The status is ${response.status} ${response.statusText}`, `This is an HTTP error: The status is ${response.status} ${response.statusText}`,
); );
@ -44,3 +46,7 @@ export const checkTwitterAuthQueryParameter = (
code, code,
}; };
}; };
export const replaceToInstallPage = () => {
window.location.href = WALLET_INSTALL_URL;
};

View File

@ -1109,7 +1109,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/semver@npm:^7.3.12": "@types/semver@npm:^7, @types/semver@npm:^7.3.12":
version: 7.3.13 version: 7.3.13
resolution: "@types/semver@npm:7.3.13" resolution: "@types/semver@npm:7.3.13"
checksum: 00c0724d54757c2f4bc60b5032fe91cda6410e48689633d5f35ece8a0a66445e3e57fa1d6e07eb780f792e82ac542948ec4d0b76eb3484297b79bd18b8cf1cb0 checksum: 00c0724d54757c2f4bc60b5032fe91cda6410e48689633d5f35ece8a0a66445e3e57fa1d6e07eb780f792e82ac542948ec4d0b76eb3484297b79bd18b8cf1cb0
@ -3175,6 +3175,7 @@ __metadata:
"@types/react": 18.0.25 "@types/react": 18.0.25
"@types/react-dom": 18.0.9 "@types/react-dom": 18.0.9
"@types/react-modal": ^3 "@types/react-modal": ^3
"@types/semver": ^7
"@types/styled-components": ^5 "@types/styled-components": ^5
"@typescript-eslint/eslint-plugin": ^5.45.0 "@typescript-eslint/eslint-plugin": ^5.45.0
"@typescript-eslint/parser": ^5.45.0 "@typescript-eslint/parser": ^5.45.0
@ -3199,6 +3200,7 @@ __metadata:
react-is: ^18.2.0 react-is: ^18.2.0
react-modal: ^3.16.1 react-modal: ^3.16.1
react-typed: ^1.2.0 react-typed: ^1.2.0
semver: ^7.3.8
styled-components: ^5.3.6 styled-components: ^5.3.6
typescript: 4.9.3 typescript: 4.9.3
languageName: unknown languageName: unknown