[WIP] Add checkbox handler, chain image error handler, keplr init logic

This commit is contained in:
HeesungB 2022-12-12 22:45:24 +09:00
parent 8319467125
commit 921ec5d7e6
10 changed files with 221 additions and 54 deletions

View File

@ -0,0 +1,10 @@
import { useState } from "react";
import Image, { ImageProps } from "next/image";
import KeplrIcon from "../../public/images/svg/keplr-icon.svg";
export const ChainImage = (props: ImageProps) => {
const [src, setSrc] = useState(props.src);
return <Image {...props} src={src} onError={() => setSrc(KeplrIcon)} />;
};

View File

@ -1,39 +1,49 @@
import { AccountInfo } from "../../types"; import { ChainItemType } from "../../types";
import { FunctionComponent } from "react"; import { ChangeEvent, FunctionComponent, useState } from "react";
import { import {
ChainImageContainer, ChainImageContainer,
ChainInfoContainer, ChainInfoContainer,
ChainItemContainer, ChainItemContainer,
} from "./chain-list"; } from "./chain-list";
import Image from "next/image";
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 { ChainImage } from "./chain-image";
interface Props { interface Props {
chainInfo: AccountInfo; chainItem: ChainItemType;
checkedItemHandler: (chainItem: ChainItemType, isChecked: boolean) => void;
} }
export const ChainItem: FunctionComponent<Props> = (props) => { export const ChainItem: FunctionComponent<Props> = (props) => {
const { chainInfo } = props; const { chainItem, checkedItemHandler } = props;
const [checked, setChecked] = useState(false);
const checkHandler = (event: ChangeEvent<HTMLInputElement>) => {
setChecked(!checked);
checkedItemHandler(chainItem, event.target.checked);
};
return ( return (
<ChainItemContainer key={chainInfo.prefix} isLoading={false}> <ChainItemContainer key={chainItem.prefix} isLoading={false}>
<ChainImageContainer width="3rem" height="3rem"> <ChainImageContainer width="3rem" height="3rem">
<Image <ChainImage
src={chainInfo.chainImageUrl} src={chainItem.chainImageUrl}
fill={true} fill={true}
alt={`${chainInfo.prefix} chain image`} alt={`${chainItem.prefix} chain image`}
/> />
</ChainImageContainer> </ChainImageContainer>
<ChainInfoContainer> <ChainInfoContainer>
<ChainName>{`.${chainInfo.prefix}`}</ChainName> <ChainName>{`.${chainItem.prefix}`}</ChainName>
<WalletAddress>{chainInfo.address}</WalletAddress> <WalletAddress>{chainItem.address}</WalletAddress>
</ChainInfoContainer> </ChainInfoContainer>
<Flex1 /> <Flex1 />
<ChainCheckBox /> <ChainCheckBox
checked={checked}
onChange={(event) => checkHandler(event)}
/>
</ChainItemContainer> </ChainItemContainer>
); );
}; };

View File

@ -1,19 +1,38 @@
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import { AccountInfo, WidthHeightProps } from "../../types"; import { ChainItemType, WidthHeightProps } from "../../types";
import color from "../../styles/color"; import color from "../../styles/color";
import styled from "styled-components"; import styled from "styled-components";
import { ChainItem } from "./chain-item"; import { ChainItem } from "./chain-item";
interface Props { interface Props {
chainList: AccountInfo[]; chainList: ChainItemType[];
checkedItems: any;
setCheckedItems: any;
} }
export const ChainList: FunctionComponent<Props> = (props) => { export const ChainList: FunctionComponent<Props> = (props) => {
const { chainList } = props; const { chainList, checkedItems, setCheckedItems } = props;
const checkedItemHandler = (chainItem: ChainItemType, isChecked: boolean) => {
const tempSet = new Set(checkedItems);
if (isChecked) {
tempSet.add(chainItem);
} else if (!isChecked && checkedItems.has(chainItem)) {
tempSet.delete(chainItem);
}
setCheckedItems(tempSet);
};
return ( return (
<ChainContainer color={color.grey["800"]}> <ChainContainer color={color.grey["800"]}>
{chainList.map((chainInfo) => ( {chainList.map((chainItem) => (
<ChainItem key={chainInfo.prefix} chainInfo={chainInfo} /> <ChainItem
key={chainItem.address}
chainItem={chainItem}
checkedItemHandler={checkedItemHandler}
/>
))} ))}
</ChainContainer> </ChainContainer>
); );

View File

@ -4,8 +4,9 @@ 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 { WalletType } from "../../constants/wallet"; import { SELECTED_WALLET_KEY, WalletType } from "../../constants/wallet";
import { TwitterAuthUrlResponse } from "../../types"; import { getKeplrFromWindow, KeplrWallet } from "../../wallets";
import { loginWithTwitter } from "../../constants/twitter";
interface Props { interface Props {
wallet: WalletType; wallet: WalletType;
@ -15,11 +16,34 @@ export const WalletItem: FunctionComponent<Props> = (props: Props) => {
const { wallet } = props; const { wallet } = props;
const onClickWalletItem = async () => { const onClickWalletItem = async () => {
const { authUrl }: TwitterAuthUrlResponse = await ( try {
await fetch("/api/twitter-auth-url") if (wallet.name === "Keplr") {
).json(); await connectKeplr();
localStorage.setItem(SELECTED_WALLET_KEY, wallet.name);
}
window.location.href = authUrl; await loginWithTwitter();
} catch (e) {
console.log(e);
}
};
const connectKeplr = async () => {
const keplr = await getKeplrFromWindow();
if (keplr === undefined) {
window.location.href =
"https://chrome.google.com/webstore/detail/keplr/dmkamcknogkgcdfhhbddcghachkejeap";
}
if (keplr) {
const wallet = new KeplrWallet(keplr);
const chainIds = (await wallet.getChainInfosWithoutEndpoints()).map(
(c) => c.chainId,
);
await wallet.init(chainIds);
}
}; };
return ( return (

View File

@ -1,3 +1,5 @@
import { TwitterAuthUrlResponse } from "../types";
export const twitterOAuthBaseUrl = "https://twitter.com/i/oauth2/authorize"; export const twitterOAuthBaseUrl = "https://twitter.com/i/oauth2/authorize";
export const twitterOAuthScopes = [ export const twitterOAuthScopes = [
@ -7,3 +9,11 @@ export const twitterOAuthScopes = [
]; ];
export const twitterApiBaseUrl = "https://api.twitter.com/2"; export const twitterApiBaseUrl = "https://api.twitter.com/2";
export const loginWithTwitter = async () => {
const { authUrl }: TwitterAuthUrlResponse = await (
await fetch("/api/twitter-auth-url")
).json();
window.location.href = authUrl;
};

View File

@ -3,8 +3,12 @@ import { StaticImageData } from "next/image";
import KeplrIcon from "../public/images/svg/keplr-icon.svg"; import KeplrIcon from "../public/images/svg/keplr-icon.svg";
import CosmostationIcon from "../public/images/svg/cosmostation-icon.svg"; import CosmostationIcon from "../public/images/svg/cosmostation-icon.svg";
export const SELECTED_WALLET_KEY = "SELECTED_WALLET_KEY";
export const MainChainId = "osmo-test-4";
export type WalletName = "Keplr" | "Cosmostation";
export interface WalletType { export interface WalletType {
name: string; name: WalletName;
image: StaticImageData; image: StaticImageData;
isReady: boolean; isReady: boolean;
} }

View File

@ -14,7 +14,8 @@ import MainTitle from "../public/images/svg/main-title.svg";
import MainLogo from "../public/images/svg/main-logo.svg"; import MainLogo from "../public/images/svg/main-logo.svg";
import CheckIcon from "../public/images/svg/check-icon.svg"; import CheckIcon from "../public/images/svg/check-icon.svg";
import { Logo } from "../components/logo"; import { Logo } from "../components/logo";
import { useState } from "react"; import { useEffect, useState } from "react";
import { SELECTED_WALLET_KEY } from "../constants/wallet";
export default function Home() { export default function Home() {
const [isModalOpen, setModalOpen] = useState(false); const [isModalOpen, setModalOpen] = useState(false);
@ -23,6 +24,10 @@ export default function Home() {
setModalOpen(true); setModalOpen(true);
}; };
useEffect(() => {
localStorage.removeItem(SELECTED_WALLET_KEY);
}, []);
return ( return (
<Container> <Container>
<Logo /> <Logo />

View File

@ -2,7 +2,11 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
// Types // Types
import { IcnsVerificationResponse, TwitterAuthInfoResponse } from "../../types"; import {
ChainItemType,
IcnsVerificationResponse,
TwitterAuthInfoResponse,
} from "../../types";
import { request } from "../../utils/url"; import { request } from "../../utils/url";
// Styles // Styles
@ -14,44 +18,33 @@ import { Logo } from "../../components/logo";
import { SkeletonChainList } from "../../components/skeleton"; import { SkeletonChainList } from "../../components/skeleton";
import { PrimaryButton } from "../../components/primary-button"; import { PrimaryButton } from "../../components/primary-button";
import { AccountInfos } from "../../config";
import { TwitterProfile } from "../../components/twitter-profile"; import { TwitterProfile } from "../../components/twitter-profile";
import { ChainList } from "../../components/chain-list"; import { ChainList } from "../../components/chain-list";
import { useRouter } from "next/router";
import { MainChainId } from "../../constants/wallet";
import { getKeplrFromWindow, KeplrWallet } from "../../wallets";
import { ChainIdHelper } from "@keplr-wallet/cosmos";
export default function VerificationPage() { export default function VerificationPage() {
const router = useRouter();
const [twitterAuthInfo, setTwitterAuthInfo] = const [twitterAuthInfo, setTwitterAuthInfo] =
useState<TwitterAuthInfoResponse | null>(); useState<TwitterAuthInfoResponse | null>();
const [chainList, setChainList] = useState<ChainItemType[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [checkedItems, setCheckedItems] = useState(new Set());
useEffect(() => { useEffect(() => {
const handleVerification = async () => { const handleVerification = async () => {
if (window.location.search) { if (window.location.search) {
const [, state, code] = if (window.location.search.match("error")) {
window.location.search.match( await router.push("/");
/^(?=.*state=([^&]+)|)(?=.*code=([^&]+)|).+$/, }
) || [];
const newTwitterAuthInfo = await request<TwitterAuthInfoResponse>( await fetchTwitterInfo();
`/api/twitter-auth-info?state=${state}&code=${code}`,
);
setTwitterAuthInfo(newTwitterAuthInfo); await fetchChainList();
const icnsVerificationList = (
await request<IcnsVerificationResponse>("/api/icns-verification", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
claimer: "osmo1y5mm5nj5m8ttddt5ccspek6xgyyavehrkak7gq",
authToken: newTwitterAuthInfo.accessToken,
}),
})
).verificationList;
console.log(icnsVerificationList);
setIsLoading(false); setIsLoading(false);
} }
@ -60,6 +53,87 @@ export default function VerificationPage() {
handleVerification(); handleVerification();
}, []); }, []);
const fetchTwitterInfo = async () => {
const [, state, code] =
window.location.search.match(
/^(?=.*state=([^&]+)|)(?=.*code=([^&]+)|).+$/,
) || [];
const newTwitterAuthInfo = await request<TwitterAuthInfoResponse>(
`/api/twitter-auth-info?state=${state}&code=${code}`,
);
setTwitterAuthInfo(newTwitterAuthInfo);
};
const fetchChainList = async () => {
const keplr = await getKeplrFromWindow();
if (keplr) {
const wallet = new KeplrWallet(keplr);
const chainIds = (await wallet.getChainInfosWithoutEndpoints()).map(
(c) => c.chainId,
);
const chainKeys = await Promise.all(
chainIds.map((chainId) => wallet.getKey(chainId)),
);
const chainInfos = (await wallet.getChainInfosWithoutEndpoints()).map(
(chainInfo) => {
return {
prefix: chainInfo.bech32Config.bech32PrefixAccAddr,
chainImageUrl: `https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/${
ChainIdHelper.parse(chainInfo.chainId).identifier
}/chain.png`,
};
},
);
const chainArray: ChainItemType[] = [];
for (let i = 0; i < chainKeys.length; i++) {
chainArray.push({
address: chainKeys[i].bech32Address,
...chainInfos[i],
});
}
// remove duplicated item
// const filteredChainList = chainArray.filter((chain, index, self) => {
// return index === self.findIndex((t) => chain.prefix === t.prefix);
// });
setChainList(chainArray);
}
};
const verifyTwitterAccount = async () => {
const keplr = await getKeplrFromWindow();
if (twitterAuthInfo && keplr) {
const wallet = new KeplrWallet(keplr);
const key = await wallet.getKey(MainChainId);
const icnsVerificationList = (
await request<IcnsVerificationResponse>("/api/icns-verification", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
claimer: key.bech32Address,
authToken: twitterAuthInfo.accessToken,
}),
})
).verificationList;
console.log(icnsVerificationList);
}
};
const onClickRegistration = async () => {
await verifyTwitterAccount();
};
return ( return (
<Container> <Container>
<Logo /> <Logo />
@ -76,10 +150,19 @@ export default function VerificationPage() {
<SearchContainer>Search</SearchContainer> <SearchContainer>Search</SearchContainer>
</ChainListTitleContainer> </ChainListTitleContainer>
<ChainList chainList={AccountInfos} /> <ChainList
chainList={chainList}
checkedItems={checkedItems}
setCheckedItems={setCheckedItems}
/>
<ButtonContainer> <ButtonContainer>
<PrimaryButton>Register</PrimaryButton> <PrimaryButton
disabled={checkedItems.size < 1}
onClick={onClickRegistration}
>
Register
</PrimaryButton>
</ButtonContainer> </ButtonContainer>
</ContentContainer> </ContentContainer>
)} )}

View File

@ -1,4 +1,4 @@
export interface AccountInfo { export interface ChainItemType {
prefix: string; prefix: string;
chainImageUrl: string; chainImageUrl: string;
address: string; address: string;

4
window.d.ts vendored
View File

@ -2,5 +2,7 @@ import { Window as KeplrWindow } from "@keplr-wallet/types";
declare global { declare global {
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Window extends KeplrWindow {} interface Window extends KeplrWindow {
cosmostation: any;
}
} }