[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-dom", "npm:18.0.9"],\
["@types/react-modal", "npm:3.13.1"],\
["@types/semver", "npm:7.3.13"],\
["@types/styled-components", "npm:5.1.26"],\
["@typescript-eslint/eslint-plugin", "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-modal", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:3.16.1"],\
["react-typed", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:1.2.0"],\
["semver", "npm:7.3.8"],\
["styled-components", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.3.6"],\
["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-dom", "npm:18.0.9"],\
["@types/react-modal", "npm:3.13.1"],\
["@types/semver", "npm:7.3.13"],\
["@types/styled-components", "npm:5.1.26"],\
["@typescript-eslint/eslint-plugin", "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-modal", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:3.16.1"],\
["react-typed", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:1.2.0"],\
["semver", "npm:7.3.8"],\
["styled-components", "virtual:4b77e00d446246df1ed27001550885fbf1b51be18c660c1b5c357d3d763078ecef2a676194291a120f149b87573081e5af0621dc83cf1f83383639f39ac133c7#npm:5.3.6"],\
["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
key={chainItem.prefix}
isLoading={false}
checked={allChecked}
onClick={checkHandler}
>
<ChainImageContainer width="3rem" height="3rem">
@ -57,5 +58,5 @@ export const AllChainsItem: FunctionComponent<Props> = (props) => {
const AllChainsContainer = styled.div`
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) => {
const { chainItem, checkedItemHandler, checkedItems, disabled } = props;
const [checked, setChecked] = useState(disabled);
const [checked, setChecked] = useState(!!disabled);
const checkHandler = () => {
if (!disabled) {
@ -35,6 +35,7 @@ export const ChainItem: FunctionComponent<Props> = (props) => {
key={chainItem.prefix}
isLoading={false}
disabled={disabled}
checked={checked}
onClick={checkHandler}
>
<ChainImageContainer width="3rem" height="3rem">
@ -71,10 +72,17 @@ export const ChainItemContainer = styled.div<{
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 {
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]);
return (
<ChainContainer color={color.grey["800"]}>
<ChainContainer color={color.grey["900"]}>
{chainList.map((chainItem) => (
<ChainItem
key={chainItem.address}
@ -78,8 +78,10 @@ export const ChainContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
max-height: 33rem;
//max-height: 33rem;
overflow: scroll;
flex: 1;
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 color from "../../styles/color";
import { Flex1 } from "../../styles/flex-1";
import styled from "styled-components";
import Image from "next/image";
import {
MINIMUM_VERSION,
SELECTED_WALLET_KEY,
WALLET_INSTALL_URL,
WalletType,
} from "../../constants/wallet";
import { getKeplrFromWindow, KeplrWallet } from "../../wallets";
import { loginWithTwitter } from "../../queries";
import {
KEPLR_NOT_FOUND_ERROR,
KEPLR_VERSION_ERROR,
} from "../../constants/error-message";
import semver from "semver/preload";
interface Props {
wallet: WalletType;
}
// Todo: Wallet 관련된 부분을 Context로 빼는 부분
export const WalletItem: FunctionComponent<Props> = (props: Props) => {
const { wallet } = props;
const [isInstalled, setIsInstalled] = useState<boolean>();
useEffect(() => {
setIsInstalled(!!window.keplr);
}, []);
const onClickWalletItem = async () => {
try {
@ -38,6 +48,11 @@ export const WalletItem: FunctionComponent<Props> = (props: Props) => {
if (keplr === undefined) {
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) {
@ -62,7 +77,11 @@ export const WalletItem: FunctionComponent<Props> = (props: Props) => {
</WalletIcon>
<WalletContentContainer>
<WalletName>{wallet.name}</WalletName>
{wallet.isReady ? null : (
{wallet.isReady ? (
isInstalled ? null : (
<WalletDescription>Go to install Keplr Extension</WalletDescription>
)
) : (
<WalletDescription>Comming soon</WalletDescription>
)}
</WalletContentContainer>

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ export const TwitterProfile: FunctionComponent<Props> = (props) => {
const { twitterProfileInformation } = props;
return (
<ProfileContainer color={color.grey["800"]}>
<ProfileContainer color={color.grey["900"]}>
<ProfileImageContainer>
<Image
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 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 REFERRAL_KEY = "icns-referral";
export const RPC_URL = "https://rpc.testnet.osmosis.zone";
export const REST_URL = "https://lcd.testnet.osmosis.zone";
// TODO: .evn에 없으면 디폴트값 설정
export const NAME_NFT_ADDRESS =
"osmo1xahnjn872smah6xle8n3z5a5teqq390qr959l805mkuw0kcy8g5smtdagg";
export const REGISTRAR_ADDRESS =
"osmo1npn97g7hsgqlp70rw8nhd7c7vyvkukv9x0n25sn4fk5mgcjlz4gq9zlgf3";
export const RESOLVER_ADDRESS =

View File

@ -7,3 +7,5 @@ export const twitterOAuthScopes = [
];
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";
export const SELECTED_WALLET_KEY = "SELECTED_WALLET_KEY";
export const MINIMUM_VERSION = "0.11.22";
export type WalletName = "Keplr" | "Cosmostation";
export interface WalletType {
name: WalletName;

View File

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

View File

@ -24,6 +24,7 @@
"react-is": "^18.2.0",
"react-modal": "^3.16.1",
"react-typed": "^1.2.0",
"semver": "^7.3.8",
"styled-components": "^5.3.6"
},
"devDependencies": {
@ -33,6 +34,7 @@
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"@types/react-modal": "^3",
"@types/semver": "^7",
"@types/styled-components": "^5",
"@typescript-eslint/eslint-plugin": "^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 TwitterIcon from "../../public/images/svg/twitter-icon.svg";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { TendermintTxTracer } from "@keplr-wallet/cosmos";
import { queryAddressesFromTwitterName } from "../../queries";
import { RegisteredAddresses } from "../../types";
import { SHARE_URL } from "../../constants/twitter";
export default function CompletePage() {
const router = useRouter();
const [registeredAddressed, setRegisteredAddressed] =
useState<RegisteredAddresses[]>();
const [availableAddress, setAvailableAddress] = useState("");
useEffect(() => {
const { txHash } = router.query;
const { txHash, twitterUsername } = router.query;
if (txHash) {
traceTX(txHash as string);
if (txHash && twitterUsername) {
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(
"https://rpc.testnet.osmosis.zone",
"/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 (
@ -41,22 +66,31 @@ export default function CompletePage() {
<Logo />
<MainContainer>
<MainTitle>Your Name is Active Now!</MainTitle>
<MainTitle>
<div>Your Name is Active Now!</div>
</MainTitle>
<ContentContainer>
<RecipentContainer>
<RecipentTitle>Recipent</RecipentTitle>
<AddressContainer>
kingstarcookies.
<Typed
strings={["osmo", "cosmos"]}
typeSpeed={150}
backSpeed={150}
backDelay={1000}
loop
smartBackspace
/>
{`${router.query.twitterUsername}.`}
{registeredAddressed && (
<Typed
strings={registeredAddressed.map(
(address) => address.bech32_prefix,
)}
typeSpeed={150}
backSpeed={150}
backDelay={1000}
loop
smartBackspace
onStringTyped={(arrayPos: number) => {
setAvailableAddress(registeredAddressed[arrayPos].address);
}}
/>
)}
</AddressContainer>
<AvailableAddressText>available address</AvailableAddressText>
<AvailableAddressText>{availableAddress}</AvailableAddressText>
</RecipentContainer>
</ContentContainer>
@ -70,7 +104,7 @@ export default function CompletePage() {
</DescriptionText>
</DescriptionContainer>
<ShareButtonContainer>
<ShareButtonContainer onClick={onClickShareButton}>
<ShareButtonText>SHARE MY NAME</ShareButtonText>
<Image src={TwitterIcon} alt="twitter icon" />
</ShareButtonContainer>
@ -95,19 +129,22 @@ const MainContainer = styled.div`
`;
const MainTitle = styled.div`
display: flex;
align-items: center;
justify-content: center;
font-family: "Inter", serif;
font-style: normal;
font-weight: 700;
font-size: 2rem;
line-height: 2rem;
padding: 1rem;
height: 5rem;
`;
const ContentContainer = styled.div`
width: 30rem;
margin-top: 1rem;
padding: 2rem 2rem;
background-color: ${color.grey["900"]};
@ -152,19 +189,22 @@ const AvailableAddressText = styled.div`
font-size: 0.75rem;
line-height: 0.75rem;
min-height: 0.75rem;
color: ${color.blue};
`;
const DescriptionContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
width: 30rem;
margin-top: 1.5rem;
padding: 1.5rem 2rem;
padding: 1.25rem 2rem;
background-color: ${color.grey["900"]};
`;
@ -183,7 +223,7 @@ const DescriptionText = styled.div`
font-style: normal;
font-weight: 400;
font-size: 0.8rem;
line-height: 0.8rem;
line-height: 140%;
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 { useEffect, useState } from "react";
import { SELECTED_WALLET_KEY } from "../constants/wallet";
import { replaceToInstallPage } from "../utils/url";
import { REFERRAL_KEY } from "../constants/icns";
export default function Home() {
const [isModalOpen, setModalOpen] = useState(false);
@ -25,6 +27,17 @@ export default function Home() {
};
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);
}, []);
@ -62,7 +75,8 @@ export default function Home() {
/>
</CheckIconContainer>
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>
<CheckIconContainer>
@ -182,7 +196,16 @@ const CheckContainer = styled.div`
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`

View File

@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
// Types
import {
ChainItemType,
QueryError,
RegisteredAddresses,
TwitterProfileType,
} from "../../types";
@ -32,11 +33,17 @@ import { ChainIdHelper } from "@keplr-wallet/cosmos";
import AllChainsIcon from "../../public/images/svg/all-chains-icon.svg";
import { AllChainsItem } from "../../components/chain-list/all-chains-item";
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 {
fetchTwitterInfo,
queryAddressesFromTwitterName,
queryOwnerOfTwitterName,
queryRegisteredTwitterId,
verifyTwitterAccount,
} from "../../queries";
@ -46,6 +53,8 @@ import {
TWITTER_LOGIN_ERROR,
} from "../../constants/error-message";
import { makeClaimMessage, makeSetRecordMessage } from "../../messages";
import Axios, { AxiosError } from "axios";
import { BackButton } from "../../components/back-button";
export default function VerificationPage() {
const router = useRouter();
@ -69,6 +78,9 @@ export default function VerificationPage() {
const [searchValue, setSearchValue] = useState("");
const [isOwner, setIsOwner] = useState(false);
const [isAgree, setIsAgree] = useState(false);
useEffect(() => {
const init = async () => {
if (window.location.search) {
@ -78,7 +90,7 @@ export default function VerificationPage() {
);
// Initialize Wallet
await initWallet();
const keplrWallet = await initWallet();
// Fetch Twitter Profile
const twitterInfo = await fetchTwitterInfo(state, code);
@ -94,10 +106,19 @@ export default function VerificationPage() {
});
if ("data" in registeredQueryResponse) {
const ownerOfQueryResponse = await queryOwnerOfTwitterName(
registeredQueryResponse.data.name,
);
const addressesQueryResponse = await queryAddressesFromTwitterName(
registeredQueryResponse.data.name,
);
if (keplrWallet) {
const key = await keplrWallet.getKey(MainChainId);
setIsOwner(ownerOfQueryResponse.data.owner === key.bech32Address);
}
setRegisteredChainList(addressesQueryResponse.data.addresses);
}
} catch (error) {
@ -153,6 +174,8 @@ export default function VerificationPage() {
await fetchChainList(keplrWallet);
setWallet(keplrWallet);
return keplrWallet;
} else {
ErrorHandler(KEPLR_NOT_FOUND_ERROR);
}
@ -227,84 +250,99 @@ export default function VerificationPage() {
};
const onClickRegistration = async () => {
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,
try {
const { state, code } = checkTwitterAuthQueryParameter(
window.location.search,
);
const twitterInfo = await fetchTwitterInfo(state, code);
const registerMsg = makeClaimMessage(
key.bech32Address,
twitterInfo.username,
icnsVerificationList,
);
const adr36Infos = await checkAdr36();
const addressMsgs = adr36Infos.map((adr36Info) => {
return makeSetRecordMessage(
if (wallet && adr36Infos) {
const key = await wallet.getKey(MainChainId);
const icnsVerificationList = await verifyTwitterAccount(
key.bech32Address,
twitterInfo.accessToken,
);
const registerMsg = makeClaimMessage(
key.bech32Address,
twitterInfo.username,
adr36Info,
icnsVerificationList,
localStorage.getItem(REFERRAL_KEY) ?? undefined,
);
});
const aminoMsgs = twitterAuthInfo?.isRegistered
? []
: [registerMsg.amino];
const protoMsgs = twitterAuthInfo?.isRegistered
? []
: [registerMsg.proto];
const addressMsgs = adr36Infos.map((adr36Info) => {
return makeSetRecordMessage(
key.bech32Address,
twitterInfo.username,
adr36Info,
);
});
for (const addressMsg of addressMsgs) {
aminoMsgs.push(addressMsg.amino);
protoMsgs.push(addressMsg.proto);
const aminoMsgs = twitterAuthInfo?.isRegistered
? []
: [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 (
<Container>
<Logo />
@ -314,6 +352,7 @@ export default function VerificationPage() {
<SkeletonChainList />
) : (
<ContentContainer>
<BackButton />
<TwitterProfile twitterProfileInformation={twitterAuthInfo} />
<ChainListTitleContainer>
@ -351,9 +390,21 @@ export default function VerificationPage() {
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
disabled={checkedItems.size < 1}
disabled={isRegisterButtonDisable}
onClick={onClickRegistration}
>
Register
@ -375,6 +426,10 @@ const MainContainer = styled.div`
display: flex;
justify-content: center;
height: 100vh;
padding: 2.7rem 0;
color: white;
`;
@ -384,15 +439,14 @@ export const ContentContainer = styled.div`
align-items: center;
width: 40rem;
margin-top: 5rem;
`;
export const ButtonContainer = styled.div`
export const ButtonContainer = styled.div<{ disabled?: boolean }>`
width: 12rem;
height: 4rem;
margin-top: 2rem;
background-color: ${(props) =>
props.disabled ? color.orange["300"] : color.orange["100"]};
`;
export const ChainListTitleContainer = styled.div`
@ -414,3 +468,26 @@ const ChainListTitle = styled.div`
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 {
NAME_NFT_ADDRESS,
REGISTRAR_ADDRESS,
RESOLVER_ADDRESS,
REST_URL,
@ -8,6 +9,7 @@ import { Buffer } from "buffer/";
import {
AddressesQueryResponse,
NameByTwitterIdQueryResponse,
OwnerOfQueryResponse,
QueryError,
} from "../types";
@ -20,11 +22,14 @@ export const queryRegisteredTwitterId = async (
const msg = {
name_by_twitter_id: { twitter_id: twitterId },
};
return request<NameByTwitterIdQueryResponse>(
getCosmwasmQueryUrl(
REGISTRAR_ADDRESS,
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",
500: "#424242",
600: "#333333",
700: "#2B2B2B",
800: "#242424",
700: "#222222",
800: "#1D1D1D",
900: "#181818",
};
const black = "#121212";

View File

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

View File

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

View File

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