Compare commits

...
Author SHA1 Message Date
Thunnini 53ecf825a0 Minor fix 2022-12-18 21:30:37 +09:00
Thunnini 5d48012133 Add DisabledChainItemType type and prevent registration from ethermint chain with ledger 2022-12-18 21:01:34 +09:00
Thunnini 3de363af0f Merge branch 'main' into Thunnini/ledger-ethereum-warning
# Conflicts:
#	.pnp.cjs
#	package.json
#	yarn.lock
2022-12-18 19:42:16 +09:00
delivan eead90d4b9 Change empty chain image to icns logo 2022-12-18 18:45:49 +09:00
delivan a61856557c Add loading state on primary button 2022-12-18 18:38:56 +09:00
delivan 7e72ab82b6 Update primary button style 2022-12-18 17:59:29 +09:00
HeesungB 68d8ba31aa Add final check modal 2022-12-18 01:51:29 +09:00
delivan e0151666b2 Merge branch 'main' of https://github.com/interchain-name/icns-frontend into main 2022-12-17 23:46:02 +09:00
delivan b41ef751fb Track registration flow 2022-12-17 23:45:50 +09:00
delivan e884fc1d86 Add Amplitude 2022-12-17 22:40:07 +09:00
delivan 1abc475354 Add Sentry 2022-12-17 20:42:56 +09:00
Thunnini d488d31345 Change typing of interface Wallet 2022-12-17 20:24:17 +09:00
Thunnini 80907781ad Bump @keplr-wallet/* 2022-12-17 20:22:03 +09:00
98 changed files with 2178 additions and 203 deletions
+3 -1
View File
@@ -49,4 +49,6 @@ build
.sentryclirc
# Intelij files
.idea
.idea
# Sentry
.sentryclirc
Generated
+776 -33
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import Image, { ImageProps } from "next/image";
import KeplrIcon from "../../public/images/svg/keplr-icon.svg";
import ICNSLogo from "../../public/images/icns-logo-120x120.png";
import styled from "styled-components";
export const ChainImage = ({ src, ...props }: ImageProps) => {
@@ -14,7 +14,7 @@ export const ChainImage = ({ src, ...props }: ImageProps) => {
src={srcState}
alt="chain image"
sizes="3rem"
onError={() => setSrcState(KeplrIcon)}
onError={() => setSrcState(ICNSLogo)}
/>
</ImageWrapper>
);
+34 -6
View File
@@ -1,4 +1,4 @@
import { ChainItemType } from "../../types";
import { ChainItemType, DisabledChainItemType } from "../../types";
import { FunctionComponent, useEffect, useState } from "react";
import color from "../../styles/color";
@@ -8,15 +8,31 @@ import { ChainImage } from "./chain-image";
import { Checkbox } from "../checkbox";
interface Props {
chainItem: ChainItemType;
chainItem: ChainItemType | DisabledChainItemType;
checkedItemHandler: (chainItem: ChainItemType, isChecked: boolean) => void;
checkedItems: Set<unknown>;
disabled?: boolean;
}
export const ChainItem: FunctionComponent<Props> = (props) => {
const { chainItem, checkedItemHandler, checkedItems, disabled } = props;
const [checked, setChecked] = useState(!!disabled);
const { chainItem, checkedItemHandler, checkedItems } = props;
const disabled = "disabled" in chainItem && chainItem.disabled;
// XXX: Currently, this component can't handle `checked` state well,
// If chain is disabled, it should be disabled in general.
// However, if it is disabled due to the limitation of ethermint and ledger,
// it should be not checked.
// To solve this problem, for now, just use dumb way.
// If chain is disabled with explicit reason, it should be unchecked.
const [checked, setChecked] = useState(!!disabled && !chainItem.reason);
useEffect(() => {
if (disabled) {
if (chainItem.reason) {
setChecked(false);
} else {
setChecked(true);
}
}
}, [chainItem, disabled]);
const checkHandler = () => {
if (!disabled) {
@@ -46,7 +62,12 @@ export const ChainItem: FunctionComponent<Props> = (props) => {
/>
<ChainInfoContainer>
<ChainName>{`.${chainItem.prefix}`}</ChainName>
<WalletAddress>{chainItem.address}</WalletAddress>
{chainItem.address ? (
<WalletAddress>{chainItem.address}</WalletAddress>
) : null}
{disabled && chainItem.reason ? (
<DisabledReason>{chainItem.reason.message}</DisabledReason>
) : null}
</ChainInfoContainer>
<Flex1 />
@@ -117,3 +138,10 @@ export const WalletAddress = styled.div`
color: ${color.grey["400"]};
`;
export const DisabledReason = styled.div`
color: ${color.grey["200"]};
font-weight: 500;
font-size: 14px;
line-height: 17px;
`;
+4 -5
View File
@@ -1,12 +1,12 @@
import { Dispatch, FunctionComponent, SetStateAction, useEffect } from "react";
import { ChainItemType } from "../../types";
import { ChainItemType, DisabledChainItemType } from "../../types";
import color from "../../styles/color";
import styled from "styled-components";
import { ChainItem } from "./chain-item";
interface Props {
chainList: ChainItemType[];
disabledChainList: ChainItemType[];
disabledChainList: DisabledChainItemType[];
checkedItems: Set<unknown>;
setCheckedItems: Dispatch<SetStateAction<Set<unknown>>>;
}
@@ -30,7 +30,7 @@ export const ChainList: FunctionComponent<Props> = (props) => {
<ChainContainer color={color.grey["900"]}>
{chainList.map((chainItem) => (
<ChainItem
key={chainItem.address}
key={chainItem.chainId}
chainItem={chainItem}
checkedItemHandler={checkedItemHandler}
checkedItems={checkedItems}
@@ -38,11 +38,10 @@ export const ChainList: FunctionComponent<Props> = (props) => {
))}
{disabledChainList.map((chainItem) => (
<ChainItem
key={chainItem.address}
key={chainItem.chainId}
chainItem={chainItem}
checkedItemHandler={checkedItemHandler}
checkedItems={checkedItems}
disabled={true}
/>
))}
</ChainContainer>
+4 -1
View File
@@ -1,5 +1,6 @@
import { captureException } from "@sentry/nextjs";
import Link from "next/link";
import React, { Component, ErrorInfo, ReactNode } from "react";
import { Component, ErrorInfo, ReactNode } from "react";
interface Props {
children?: ReactNode;
@@ -21,6 +22,8 @@ class ErrorBoundary extends Component<Props, State> {
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
captureException(error);
}
public render() {
+196
View File
@@ -0,0 +1,196 @@
import { FunctionComponent } from "react";
import color from "../../styles/color";
import ReactModal from "react-modal";
import styled from "styled-components";
import TwitterIcon from "../../public/images/svg/twitter-modal-icon.svg";
import Image from "next/image";
import { PrimaryButton } from "../primary-button";
import { SecondaryButton } from "../secondary-button";
import { MINIMUM_OSMO_FEE } from "../../constants/wallet";
import { useRouter } from "next/router";
interface Props {
twitterUserName: string | undefined;
walletInfo:
| { name: string; pubKey: Uint8Array; bech32Address: string }
| undefined;
isModalOpen: boolean;
onCloseModal: () => void;
onClickRegisterButton: () => Promise<void>;
}
export const FinalCheckModal: FunctionComponent<Props> = (props) => {
const {
twitterUserName,
walletInfo,
isModalOpen,
onCloseModal,
onClickRegisterButton,
} = props;
const router = useRouter();
return (
<ReactModal
isOpen={isModalOpen}
onRequestClose={onCloseModal}
ariaHideApp={false}
style={{
overlay: { background: "#121212cc" },
content: {
top: "50%",
left: "50%",
right: "auto",
bottom: "auto",
padding: 0,
marginRight: "-50%",
transform: "translate(-50%, -50%)",
background: color.grey["800"],
border: 0,
},
}}
>
<ModalContainer>
<ModalTitle>Final Checks</ModalTitle>
<MainText>You are claiming the ICNS name</MainText>
<ICNSNameContainer>
<BoldText>{twitterUserName}</BoldText>
<TwitterImageContainer>
<Image
src={TwitterIcon}
fill={true}
sizes="2rem"
alt="twitter icon"
/>
</TwitterImageContainer>
</ICNSNameContainer>
<MainText>on</MainText>
<BoldText>{walletInfo?.name}</BoldText>
<MainText>({walletInfo?.bech32Address})</MainText>
<Divider />
<SubText>
ICNS name can only be claimed once per Twitter account.
<br />
ICNS name cant be transferred at this time.
<br />
Please make sure youve selected the right account on your wallet.
</SubText>
<SubText>
<SubBoldText>{MINIMUM_OSMO_FEE}</SubBoldText> will be spent as a
spam-prevention fee.
</SubText>
<ButtonContainer>
<SecondaryButton
onClick={async () => {
await router.push("/");
}}
>
Use a different account
</SecondaryButton>
<RegisterButton>
<PrimaryButton onClick={onClickRegisterButton}>
Register
</PrimaryButton>
</RegisterButton>
</ButtonContainer>
</ModalContainer>
</ReactModal>
);
};
const ModalContainer = styled.div`
display: flex;
flex-direction: column;
gap: 0.625rem;
width: 50rem;
padding: 1.75rem 2rem;
`;
const ModalTitle = styled.div`
font-family: "Inter", serif;
font-style: normal;
font-weight: 700;
font-size: 1.5rem;
line-height: 1.8rem;
margin-bottom: 1rem;
color: ${color.white};
`;
const ICNSNameContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
`;
const TwitterImageContainer = styled.div`
width: 2rem;
height: 2rem;
margin-top: 0.4rem;
position: relative;
`;
const MainText = styled.div`
font-family: "Inter", serif;
font-style: normal;
font-weight: 600;
font-size: 1rem;
line-height: 1.2rem;
color: ${color.white};
`;
const SubText = styled.div`
font-family: "Inter", serif;
font-style: normal;
font-weight: 500;
font-size: 1rem;
line-height: 1.5rem;
color: ${color.grey["300"]};
`;
const SubBoldText = styled.span`
color: ${color.grey["100"]};
`;
const BoldText = styled.div`
font-family: "Inter", serif;
font-style: normal;
font-weight: 600;
font-size: 2rem;
line-height: 2.5rem;
color: ${color.orange["50"]};
`;
const Divider = styled.div`
width: 100%;
margin: 1.625rem 0;
border: 0.5px solid ${color.grey["500"]};
`;
const ButtonContainer = styled.div`
display: flex;
flex-direction: row;
height: 3.5rem;
margin-top: 2.5rem;
padding: 0 4.25rem;
gap: 3.5rem;
`;
const RegisterButton = styled.div`
width: 10rem;
`;
+110
View File
@@ -0,0 +1,110 @@
import { ButtonHTMLAttributes, FunctionComponent } from "react";
import styled, { keyframes } from "styled-components";
import color from "../../styles/color";
interface PrimaryButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
}
export const PrimaryButton: FunctionComponent<PrimaryButtonProps> = ({
children,
isLoading,
...props
}) => {
return (
<StyledPrimaryButton {...props}>
{isLoading ? (
<SpinnerWrapper>
<Spinner />
<Spinner />
<Spinner />
<Spinner />
</SpinnerWrapper>
) : (
<span>{children}</span>
)}
</StyledPrimaryButton>
);
};
const StyledPrimaryButton = styled.button`
display: flex;
align-items center;
justify-content: center;
width: 100%;
height: 100%;
border: none;
padding: 11px 30px;
font-family: "Inter", serif;
font-style: normal;
font-weight: 600;
font-size: 1.25rem;
line-height: 1.25rem;
letter-spacing: 0.07em;
text-transform: uppercase;
background-color: ${color.orange["100"]};
cursor: pointer;
&:hover {
transition-duration: 0.5s;
background-color: ${color.orange["200"]};
span {
opacity: 0.5;
}
}
&:disabled {
background-color: ${color.orange["300"]};
span {
opacity: 0.5;
}
}
span {
transition-duration: 0.5s;
color: ${color.orange["50"]};
}
`;
const SpinnerWrapper = styled.div`
display: flex;
position: relative;
width: 20px;
height: 20px;
`;
const spinAnimation = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const Spinner = styled.div<{ animationDelay?: string }>`
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
animation: ${spinAnimation} 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
${({ animationDelay }) =>
animationDelay ? `animation-delay: ${animationDelay};` : ""}
border-radius: 100%;
border-style: solid;
border-width: 3px;
border-color: white transparent transparent transparent;
`;
@@ -1,7 +1,7 @@
import styled from "styled-components";
import color from "../../styles/color";
export const PrimaryButton = styled.button`
export const SecondaryButton = styled.button`
width: 100%;
height: 100%;
@@ -17,19 +17,8 @@ export const PrimaryButton = styled.button`
letter-spacing: 0.07em;
text-transform: uppercase;
color: ${color.orange["50"]};
background-color: ${color.orange["100"]};
color: ${color.white};
background-color: ${color.grey["300"]};
cursor: pointer;
&:hover {
transition-duration: 0.5s;
background-color: ${color.orange["200"]};
}
&:disabled {
opacity: 0.5;
background-color: ${color.orange["300"]};
}
`;
@@ -107,8 +107,6 @@ const SkeletonButton = styled.div`
width: 12rem;
height: 4rem;
padding-top: 1.5rem;
background-color: ${color.grey["800"]};
`;
+14 -2
View File
@@ -1,5 +1,10 @@
/** @type {import('next').NextConfig} */
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { withSentryConfig } = require("@sentry/nextjs");
const nextConfig = {
sentry: {
hideSourceMaps: true,
},
reactStrictMode: false,
swcMinify: true,
compiler: {
@@ -23,4 +28,11 @@ const nextConfig = {
},
};
module.exports = nextConfig;
const sentryWebpackPluginOptions = {
silent: true,
};
module.exports =
process.env.NEXT_IS_ENABLE_USER_TRACKING === "true"
? withSentryConfig(nextConfig, sentryWebpackPluginOptions)
: nextConfig;
+6 -4
View File
@@ -10,10 +10,12 @@
"lint": "next lint"
},
"dependencies": {
"@keplr-wallet/common": "^0.11.23",
"@keplr-wallet/cosmos": "^0.11.23",
"@keplr-wallet/proto-types": "^0.11.23",
"@keplr-wallet/types": "^0.11.23",
"@amplitude/analytics-browser": "^1.6.6",
"@keplr-wallet/common": "^0.11.25",
"@keplr-wallet/cosmos": "^0.11.25",
"@keplr-wallet/proto-types": "^0.11.25",
"@keplr-wallet/types": "^0.11.25",
"@sentry/nextjs": "^7.27.0",
"axios": "^0.27.2",
"buffer": "^6.0.3",
"crypto": "^1.0.1",
+25 -1
View File
@@ -1,7 +1,8 @@
import * as amplitude from "@amplitude/analytics-browser";
import type { AppProps } from "next/app";
import Head from "next/head";
import { useRouter } from "next/router";
import React, { useMemo } from "react";
import React, { useEffect, useMemo } from "react";
import { DefaultTheme, ThemeProvider } from "styled-components";
import ErrorBoundary from "../components/error-boundary";
@@ -17,6 +18,10 @@ const defaultPageTheme: DefaultTheme = {
bgGridColor: "rgba(51, 51, 51, 0.3)",
};
if (process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY !== undefined) {
amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY);
}
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
@@ -26,6 +31,25 @@ export default function App({ Component, pageProps }: AppProps) {
const origin = typeof window !== "undefined" ? window.location.origin : "";
useEffect(() => {
const handleRouteChangeComplete = (url: string) => {
const pathname = url.split("?")[0];
amplitude.track("view page", {
pathname,
});
};
handleRouteChangeComplete(router.pathname);
router.events.on("routeChangeStart", handleRouteChangeComplete);
// If the component is unmounted, unsubscribe
// from the event with the `off` method:
return () => {
router.events.off("routeChangeComplete", handleRouteChangeComplete);
};
}, []);
return (
<ThemeProvider theme={pageTheme}>
<Head>
+66
View File
@@ -0,0 +1,66 @@
import { flush } from "@sentry/nextjs";
import { NextPageContext } from "next";
import NextErrorComponent, { ErrorProps as NextErrorProps } from "next/error";
type ErrorProps = NextErrorProps & {
hasGetInitialPropsRun: boolean;
err: any;
};
const MyError = ({ statusCode, hasGetInitialPropsRun, err }: ErrorProps) => {
if (!hasGetInitialPropsRun && err) {
// getInitialProps is not called in case of
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
// err via _app.js so it can be captured
}
return <NextErrorComponent statusCode={statusCode} />;
};
MyError.getInitialProps = async (context: NextPageContext) => {
const errorInitialProps = (await NextErrorComponent.getInitialProps(
context,
)) as ErrorProps;
const { res, err } = context;
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
// getInitialProps has run
errorInitialProps.hasGetInitialPropsRun = true;
// Returning early because we don't want to log 404 errors to
if (res?.statusCode === 404) {
return errorInitialProps;
}
// Running on the server, the response object (`res`) is available.
//
// Next.js will pass an err on the server if a page's data fetching methods
// threw or returned a Promise that rejected
//
// Running on the client (browser), Next.js will provide an err if:
//
// - a page's `getInitialProps` threw or returned a Promise that rejected
// - an exception was thrown somewhere in the React lifecycle (render,
// componentDidMount, etc) that was caught by Next.js's React Error
// Boundary. Read more about what types of exceptions are caught by Error
// Boundaries: https://reactjs.org/docs/error-boundaries.html
if (err) {
// Flushing before returning is necessary if deploying to Vercel, see
// https://vercel.com/docs/platform/limits#streaming-responses
await flush(2000);
return errorInitialProps;
}
// If this point is reached, getInitialProps was called without any
// information about what the error might be. This is unexpected and may
// indicate a bug introduced in Next.js, so record it in Sentry
await flush(2000);
return errorInitialProps;
};
export default MyError;
+5
View File
@@ -1,3 +1,4 @@
import * as amplitude from "@amplitude/analytics-browser";
import Image from "next/image";
import styled from "styled-components";
@@ -44,6 +45,8 @@ export default function CompletePage() {
);
if (!result.code || result.code === 0) {
amplitude.track("complete registration");
const addresses = await queryAddressesFromTwitterName(twitterUserName);
setRegisteredAddressed(addresses.data.addresses);
setIsSuccess(true);
@@ -54,6 +57,8 @@ export default function CompletePage() {
};
const onClickShareButton = () => {
amplitude.track("click share button");
const { twitterUsername } = router.query;
const shareMessage = `👨‍🚀 To Interchain... And Beyond!%0a%0aHey frens, I just minted my name for the interchain on @icns_xyz: ${twitterUsername}%0a%0aClaim yours now ▶`;
+4
View File
@@ -1,3 +1,5 @@
import * as amplitude from "@amplitude/analytics-browser";
// NextJs
import Image from "next/image";
@@ -24,6 +26,8 @@ export default function Home() {
const [isModalOpen, setModalOpen] = useState(false);
const onClickConnectWalletButton = async () => {
amplitude.track("click connect wallet button");
setModalOpen(true);
};
+147 -77
View File
@@ -1,9 +1,12 @@
import * as amplitude from "@amplitude/analytics-browser";
// React
import { useEffect, useState } from "react";
// Types
import {
ChainItemType,
DisabledChainItemType,
QueryError,
RegisteredAddresses,
TwitterProfileType,
@@ -54,19 +57,31 @@ import {
import { makeClaimMessage, makeSetRecordMessage } from "../../messages";
import Axios from "axios";
import { BackButton } from "../../components/back-button";
import { FinalCheckModal } from "../../components/final-check-modal";
export default function VerificationPage() {
const router = useRouter();
const [twitterAuthInfo, setTwitterAuthInfo] = useState<TwitterProfileType>();
const [isLoading, setIsLoading] = useState(true);
const [isLoadingInit, setIsLoadingInit] = useState(true);
const [isLoadingRegistration, setIsLoadingRegistration] = useState(false);
const [wallet, setWallet] = useState<KeplrWallet>();
const [walletKey, setWalletKey] = useState<{
name: string;
pubKey: Uint8Array;
bech32Address: string;
isLedgerNano?: boolean;
}>();
const [chainList, setChainList] = useState<ChainItemType[]>([]);
const [disabledChainList, setDisabledChainList] = useState<ChainItemType[]>(
[],
);
const [chainList, setChainList] = useState<
(ChainItemType & {
isEthermintLike?: boolean;
})[]
>([]);
const [disabledChainList, setDisabledChainList] = useState<
DisabledChainItemType[]
>([]);
const [registeredChainList, setRegisteredChainList] = useState<
RegisteredAddresses[]
>([]);
@@ -75,7 +90,9 @@ export default function VerificationPage() {
const [searchValue, setSearchValue] = useState("");
const [isOwner, setIsOwner] = useState(false);
const [isAgree, setIsAgree] = useState(false);
// const [isAgree, setIsAgree] = useState(false);
const [isModalOpen, setModalOpen] = useState(false);
useEffect(() => {
init();
@@ -90,23 +107,51 @@ export default function VerificationPage() {
}, [wallet]);
useEffect(() => {
const disabledChainList = chainList.filter((chain) => {
for (const registeredChain of registeredChainList) {
if (
chain.prefix === registeredChain.bech32_prefix &&
chain.address === registeredChain.address
) {
const disabledChainList = chainList
.filter((chain) => {
if (!chain.address) {
// Address can be "" if `getKey` failed.
return true;
}
}
return false;
for (const registeredChain of registeredChainList) {
if (
chain.prefix === registeredChain.bech32_prefix &&
chain.address === registeredChain.address
) {
return true;
}
}
return false;
})
.map<DisabledChainItemType>((chain) => {
if (walletKey) {
if (walletKey.isLedgerNano && chain.isEthermintLike) {
return {
...chain,
disabled: true,
reason: new Error(
"Support for Ethereum address on Ledger is coming soon.",
),
};
}
}
return {
...chain,
disabled: true,
};
});
const filteredChainList = chainList.filter((chain) => {
return (
disabledChainList.find(
(disabled) => disabled.chainId === chain.chainId,
) == null
);
});
const filteredChainList = chainList.filter(
(chain) => !disabledChainList.includes(chain),
);
setChainList(filteredChainList);
setDisabledChainList(disabledChainList);
@@ -145,15 +190,15 @@ export default function VerificationPage() {
registeredQueryResponse.data.name,
);
const addressesQueryResponse = await queryAddressesFromTwitterName(
registeredQueryResponse.data.name,
);
if (keplrWallet) {
const key = await keplrWallet.getKey(MainChainId);
setIsOwner(ownerOfQueryResponse.data.owner === key.bech32Address);
}
const addressesQueryResponse = await queryAddressesFromTwitterName(
registeredQueryResponse.data.name,
);
setRegisteredChainList(addressesQueryResponse.data.addresses);
}
} catch (error) {
@@ -163,7 +208,7 @@ export default function VerificationPage() {
console.error(error);
} finally {
setIsLoading(false);
setIsLoadingInit(false);
}
}
};
@@ -173,9 +218,11 @@ export default function VerificationPage() {
if (keplr) {
const keplrWallet = new KeplrWallet(keplr);
const key = await keplrWallet.getKey(MainChainId);
await fetchChainList(keplrWallet);
setWallet(keplrWallet);
setWalletKey(key);
return keplrWallet;
} else {
@@ -187,7 +234,7 @@ export default function VerificationPage() {
const chainIds = (await wallet.getChainInfosWithoutEndpoints()).map(
(c) => c.chainId,
);
const chainKeys = await Promise.all(
const chainKeys = await Promise.allSettled(
chainIds.map((chainId) => wallet.getKey(chainId)),
);
@@ -200,14 +247,20 @@ export default function VerificationPage() {
chainImageUrl: `https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/${
ChainIdHelper.parse(chainInfo.chainId).identifier
}/chain.png`,
isEthermintLike: chainInfo.isEthermintLike,
};
},
);
const chainArray = [];
for (let i = 0; i < chainKeys.length; i++) {
const chainKey = chainKeys[i];
if (chainKey.status !== "fulfilled") {
console.log("Failed to get key from wallet", chainKey);
}
chainArray.push({
address: chainKeys[i].bech32Address,
address:
chainKey.status === "fulfilled" ? chainKey.value.bech32Address : "",
...chainInfos[i],
});
}
@@ -251,8 +304,20 @@ export default function VerificationPage() {
}
};
const onClickRegistration = async () => {
const onClickRegistration = () => {
amplitude.track("click register button");
if (isOwner) {
handleRegistration();
} else {
setModalOpen(true);
}
};
const handleRegistration = async () => {
try {
setIsLoadingRegistration(true);
const { state, code } = checkTwitterAuthQueryParameter(
window.location.search,
);
@@ -260,16 +325,14 @@ export default function VerificationPage() {
const adr36Infos = await checkAdr36();
if (wallet && adr36Infos) {
const key = await wallet.getKey(MainChainId);
if (wallet && walletKey && adr36Infos) {
const icnsVerificationList = await verifyTwitterAccount(
key.bech32Address,
walletKey.bech32Address,
twitterInfo.accessToken,
);
const registerMsg = makeClaimMessage(
key.bech32Address,
walletKey.bech32Address,
twitterInfo.username,
icnsVerificationList,
localStorage.getItem(REFERRAL_KEY) ?? undefined,
@@ -277,7 +340,7 @@ export default function VerificationPage() {
const addressMsgs = adr36Infos.map((adr36Info) => {
return makeSetRecordMessage(
key.bech32Address,
walletKey.bech32Address,
twitterInfo.username,
adr36Info,
);
@@ -302,7 +365,7 @@ export default function VerificationPage() {
const simulated = await simulateMsgs(
chainInfo,
key.bech32Address,
walletKey.bech32Address,
{
proto: protoMsgs,
},
@@ -314,7 +377,7 @@ export default function VerificationPage() {
const txHash = await sendMsgs(
wallet,
chainInfo,
key.bech32Address,
walletKey.bech32Address,
{
amino: aminoMsgs,
proto: protoMsgs,
@@ -337,17 +400,15 @@ export default function VerificationPage() {
if (Axios.isAxiosError(error)) {
console.error((error?.response?.data as QueryError).message);
}
} finally {
setIsLoadingRegistration(false);
}
};
const isRegisterButtonDisable = (() => {
const hasCheckedItem = checkedItems.size > 0;
if (isOwner) {
return !hasCheckedItem;
} else {
return !(isAgree && hasCheckedItem);
}
return !hasCheckedItem;
})();
return (
@@ -355,7 +416,7 @@ export default function VerificationPage() {
<Logo />
<MainContainer>
{isLoading ? (
{isLoadingInit ? (
<SkeletonChainList />
) : (
<ContentContainer>
@@ -395,22 +456,21 @@ export default function VerificationPage() {
setCheckedItems={setCheckedItems}
/>
{!isOwner && (
<AgreeContainer
onClick={() => {
setIsAgree(!isAgree);
}}
>
<AgreeCheckBox type="checkbox" checked={isAgree} readOnly />I
check that Osmo is required for this transaction
</AgreeContainer>
)}
{/*<AgreeContainer*/}
{/* onClick={() => {*/}
{/* setIsAgree(!isAgree);*/}
{/* }}*/}
{/*>*/}
{/* <AgreeCheckBox type="checkbox" checked={isAgree} readOnly />I*/}
{/* check that Osmo is required for this transaction*/}
{/*</AgreeContainer>*/}
{chainList.length > 0 && (
<ButtonContainer disabled={isRegisterButtonDisable}>
<PrimaryButton
disabled={isRegisterButtonDisable}
onClick={onClickRegistration}
isLoading={isLoadingRegistration}
>
Register
</PrimaryButton>
@@ -419,6 +479,14 @@ export default function VerificationPage() {
</ContentContainer>
)}
</MainContainer>
<FinalCheckModal
twitterUserName={twitterAuthInfo?.username}
walletInfo={walletKey}
isModalOpen={isModalOpen}
onCloseModal={() => setModalOpen(false)}
onClickRegisterButton={handleRegistration}
/>
</Container>
);
}
@@ -458,6 +526,8 @@ export const ButtonContainer = styled.div<{ disabled?: boolean }>`
width: 11rem;
height: 3.5rem;
margin-top: 1.5rem;
background-color: ${(props) =>
props.disabled ? color.orange["300"] : color.orange["100"]};
`;
@@ -482,30 +552,30 @@ 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;
user-select: none;
color: ${color.grey["400"]};
padding: 2rem 0;
cursor: pointer;
`;
const AgreeCheckBox = styled.input.attrs({ type: "checkbox" })`
width: 1.2rem;
height: 1.2rem;
accent-color: ${color.orange["200"]};
`;
// 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;
// user-select: none;
//
// color: ${color.grey["400"]};
//
// padding: 2rem 0;
//
// cursor: pointer;
// `;
//
// const AgreeCheckBox = styled.input.attrs({ type: "checkbox" })`
// width: 1.2rem;
// height: 1.2rem;
//
// accent-color: ${color.orange["200"]};
// `;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28 8.786C27.118 9.17667 26.1694 9.43733 25.1687 9.56067C26.186 8.95667 26.9687 7.99333 27.336 6.85533C26.384 7.41333 25.3294 7.82333 24.2074 8.03933C23.3114 7.08933 22.0334 6.5 20.6174 6.5C17.8974 6.5 15.6927 8.68533 15.6927 11.38C15.6927 11.7613 15.7374 12.1327 15.8214 12.4933C11.7294 12.288 8.10005 10.3427 5.67205 7.39067C5.24538 8.112 5.00538 8.95667 5.00538 9.848C5.00538 11.542 5.87271 13.0333 7.19538 13.912C6.38805 13.8873 5.62805 13.6627 4.96271 13.3027C4.96271 13.3173 4.96271 13.3393 4.96271 13.36C4.96271 15.7273 6.66071 17.6987 8.91138 18.1473C8.50005 18.26 8.06538 18.3227 7.61738 18.3227C7.29938 18.3227 6.98938 18.2867 6.68938 18.2327C7.31605 20.1673 9.13405 21.5813 11.288 21.6233C9.60271 22.93 7.48005 23.7127 5.17205 23.7127C4.77338 23.7127 4.38338 23.69 3.99805 23.6433C6.17871 25.024 8.76805 25.8333 11.5474 25.8333C20.604 25.8333 25.5587 18.396 25.5587 11.944C25.5587 11.7327 25.552 11.522 25.542 11.314C26.5087 10.6313 27.342 9.77 28 8.786Z" fill="#03A9F4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+28
View File
@@ -0,0 +1,28 @@
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
const IS_ENABLE_USER_TRACKING = process.env.NEXT_PUBLIC_IS_ENABLE_USER_TRACKING;
Sentry.init({
enabled: IS_ENABLE_USER_TRACKING === "true",
dsn:
SENTRY_DSN ||
"https://78c91641e90f4f7cad28f50aaec9fb95@o4504343701946368.ingest.sentry.io/4504343708827648",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
denyUrls: [
// deny all chrome extension
"chrome-extension://",
// deny all firefox extension
"moz-extension://",
],
});
+3
View File
@@ -0,0 +1,3 @@
defaults.url=https://sentry.io/
defaults.org=interchain-name-service
defaults.project=icns-frontend
+23
View File
@@ -0,0 +1,23 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const IS_ENABLE_USER_TRACKING =
process.env.NEXT_PUBLIC_IS_ENABLE_USER_TRACKING ||
process.env.NEXT_IS_ENABLE_USER_TRACKING;
Sentry.init({
enabled: IS_ENABLE_USER_TRACKING === "true",
dsn:
SENTRY_DSN ||
"https://78c91641e90f4f7cad28f50aaec9fb95@o4504343701946368.ingest.sentry.io/4504343708827648",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
+7
View File
@@ -5,3 +5,10 @@ export interface ChainItemType {
chainImageUrl: string;
address: string;
}
export interface DisabledChainItemType extends ChainItemType {
disabled: true;
// Show reason why this chain is disabled to user if needed.
reason?: Error;
}
+19 -6
View File
@@ -45,18 +45,32 @@ export class KeplrWallet implements Wallet {
}
getChainInfosWithoutEndpoints(): Promise<
Omit<ChainInfo, "rest" | "rpc" | "nodeProvider">[]
(Pick<ChainInfo, "chainId" | "chainName" | "bech32Config"> & {
readonly isEthermintLike?: boolean;
})[]
> {
// TODO: Update @keplr-wallet/types
return (this.keplr as any).getChainInfosWithoutEndpoints();
return this.keplr.getChainInfosWithoutEndpoints().then((chainInfos) => {
return chainInfos.map((chainInfo) => {
return {
...chainInfo,
isEthermintLike: chainInfo.features?.includes("eth-address-gen"),
};
});
});
}
getKey(chainId: string): Promise<{
readonly name: string;
readonly pubKey: Uint8Array;
readonly bech32Address: string;
readonly isLedgerNano?: boolean;
}> {
return this.keplr.getKey(chainId);
return this.keplr.getKey(chainId).then((key) => {
return {
...key,
isLedgerNano: key.isNanoLedger,
};
});
}
init(chainIds: string[]): Promise<void> {
@@ -88,8 +102,7 @@ export class KeplrWallet implements Wallet {
signature: Uint8Array;
}[]
> {
// TODO: Update @keplr-wallet/types
return (this.keplr as any).signICNSAdr36(
return this.keplr.signICNSAdr36(
chainId,
contractAddress,
owner,
+4 -1
View File
@@ -4,13 +4,16 @@ export interface Wallet {
init(chainIds: string[]): Promise<void>;
getChainInfosWithoutEndpoints(): Promise<
Omit<ChainInfo, "rest" | "rpc" | "nodeProvider">[]
(Pick<ChainInfo, "chainId" | "chainName" | "bech32Config"> & {
readonly isEthermintLike?: boolean;
})[]
>;
getKey(chainId: string): Promise<{
readonly name: string;
readonly pubKey: Uint8Array;
readonly bech32Address: string;
readonly isLedgerNano?: boolean;
}>;
signAmino(
chainId: string,
+692 -48
View File
File diff suppressed because it is too large Load Diff