Skip to content

Commit

Permalink
feat(upgrade-account-modal): able to withdraw account
Browse files Browse the repository at this point in the history
  • Loading branch information
runjuu committed Feb 21, 2023
1 parent 1355035 commit 6bb38b6
Show file tree
Hide file tree
Showing 32 changed files with 1,063 additions and 92 deletions.
11 changes: 11 additions & 0 deletions packages/connect-kit/src/apis/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,14 @@ export async function updateCharactersMetadata({
body: { metadata, mode },
});
}

export function getWithdrawProof({
token,
}: {
token: string;
}): Promise<{ proof: string; nonce: number; expires: number }> {
return request(`/newbie/account/withdraw/proof`, {
method: "GET",
token,
});
}
1 change: 1 addition & 0 deletions packages/connect-kit/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from "./use-set-primary-character";
export * from "./use-transfer-csb";
export * from "./use-update-character-handle";
export * from "./use-update-character-metadata";
export * from "./use-withdraw-email-account";
63 changes: 40 additions & 23 deletions packages/connect-kit/src/hooks/use-account-balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,50 @@ export type UseAccountBalanceResult = {
isLoading: boolean;
};

const NO_BALANCE: UseAccountBalanceResult = { balance: null, isLoading: false };

export function useAccountBalance(): UseAccountBalanceResult {
const account = useAccountState((s) => s.computed.account);
const emailBalance = useEmailAccountBalance();
const walletBalance = useWalletAccountBalance();

switch (account?.type) {
case "email":
return emailBalance;
case "wallet":
return walletBalance;
default:
return NO_BALANCE;
}
}

export function useEmailAccountBalance(): UseAccountBalanceResult {
const email = useAccountState((s) => s.email);

return React.useMemo((): UseAccountBalanceResult => {
if (!email) return { balance: null, isLoading: false };

const decimals = 18;
const value = BigNumber.from(email.csb);

return {
balance: {
decimals,
formatted: utils.formatUnits(value, decimals),
symbol: "CSB",
value,
},
isLoading: false,
};
}, [email]);
}

export function useWalletAccountBalance(): UseAccountBalanceResult {
const wallet = useAccountState((s) => s.wallet);

const { data: balance, isLoading } = useBalance({
address: account?.address as `0x${string}` | undefined,
address: wallet?.address as `0x${string}` | undefined,
});

return React.useMemo((): UseAccountBalanceResult => {
switch (account?.type) {
case "email": {
const decimals = 18;
const value = BigNumber.from(account.csb);

return {
balance: {
decimals,
formatted: utils.formatUnits(value, decimals),
symbol: "CSB",
value,
},
isLoading: false,
};
}
case "wallet":
return { balance, isLoading };
}

return { balance: null, isLoading: true };
}, [account, balance, isLoading]);
return React.useMemo(() => ({ balance, isLoading }), [balance, isLoading]);
}
47 changes: 47 additions & 0 deletions packages/connect-kit/src/hooks/use-withdraw-email-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useAccountState } from "@crossbell/connect-kit";
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
import { useContract } from "@crossbell/contract";
import { indexer } from "@crossbell/indexer";
import { isAddressEqual } from "@crossbell/util-ethers";

import { getWithdrawProof } from "../apis";
import { asyncRetry } from "../utils";

export function useWithdrawEmailAccount(options?: UseMutationOptions) {
const account = useAccountState();
const contract = useContract();

const mutation = useMutation(async () => {
const { wallet, email } = account;

if (!wallet || !email) return;

const { nonce, expires, proof } = await getWithdrawProof({
token: email.token,
});

await contract.withdrawCharacterFromNewbieVilla(
wallet.address,
email.characterId,
nonce,
expires,
proof
);

const character = await asyncRetry(async (RETRY) => {
const character = await indexer.getCharacter(email.characterId);

return isAddressEqual(character?.owner, wallet.address)
? character!
: RETRY;
});

await account.refreshWallet();

account.switchCharacter(character);

await account.disconnectEmail();
}, options);

return { ...mutation, account };
}
28 changes: 20 additions & 8 deletions packages/connect-kit/src/modals/upgrade-account-modal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { DynamicContainer, DynamicContainerContent } from "@crossbell/ui";

import { BaseModal } from "../../components";
import { BaseModal, Congrats } from "../../components";

import { SceneKind } from "./types";
import {
Expand All @@ -10,9 +10,13 @@ import {
StoresProvider,
} from "./stores";

import { Header } from "./components/header";
import { ConnectKindDifferences } from "./scenes/connect-kind-differences";
import { SelectOptions } from "./scenes/select-options";
import { UpgradeToWallet } from "./scenes/upgrade-to-wallet";
import { SelectWalletToConnect } from "./scenes/select-wallet-to-connect";
import { ConnectWallet } from "./scenes/connect-wallet";
import { ConfirmUpgrade } from "./scenes/confirm-upgrade";
import { GetAWallet } from "../../scenes";

export { useUpgradeAccountModal };

Expand All @@ -32,18 +36,26 @@ export function UpgradeAccountModal() {
}

function Main() {
const currentScene = useScenesStore(({ computed }) => computed.currentScene);
const scene = useScenesStore(({ computed }) => computed.currentScene);

return (
<DynamicContainerContent id={currentScene.kind}>
{((): JSX.Element => {
switch (currentScene.kind) {
<DynamicContainerContent id={scene.kind}>
{((): React.ReactNode => {
switch (scene.kind) {
case SceneKind.selectOptions:
return <SelectOptions />;
case SceneKind.connectKindDifferences:
return <ConnectKindDifferences />;
case SceneKind.upgradeToWallet:
return <UpgradeToWallet />;
case SceneKind.selectWalletToConnect:
return <SelectWalletToConnect />;
case SceneKind.connectWallet:
return <ConnectWallet {...scene} />;
case SceneKind.congrats:
return <Congrats {...scene} />;
case SceneKind.getAWallet:
return <GetAWallet Header={Header} />;
case SceneKind.confirmUpgrade:
return <ConfirmUpgrade {...scene} />;
}
})()}
</DynamicContainerContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@media (min-width: 768px) {
.container {
width: 360px;
}
}

.main {
padding: 0 24px 24px;
}

.tips {
font-size: 14px;
color: #999;
text-align: center;
margin: 10px 0 20px;
}

.walletIcon {
width: 24px;
height: 24px;
}

.address {
padding: 10px 16px;
font-size: 14px;
background: rgba(28, 27, 31, 0.04);
color: rgba(28, 27, 31, 0.38);
border-radius: 12px;
}

.actions {
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 48px;
}

.actions button:first-of-type {
background: #f6f7f9;
color: #000;
}

.actions button {
flex: 1;
height: 48px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react";

import { useAccountState } from "../../../../hooks";
import { Field, WalletIcon, ActionBtn } from "../../../../components";

import { useUpgradeAccountModal } from "../../stores";
import { Header } from "../../components/header";
import styles from "./confirm.module.css";

export type ConfirmProps = {
onConfirm: () => void;
};

export function Confirm({ onConfirm }: ConfirmProps) {
const address = useAccountState((s) => s.wallet?.address);
const { hide } = useUpgradeAccountModal();

return (
<div className={styles.container}>
<Header title="Upgrade Account" />

<div className={styles.main}>
<p className={styles.tips}>
Please note that once you upgrade, you'll no longer be able to access
your email account.
<br />
Are you sure you want to continue?
</p>

<Field
title="Confirm Wallet Address"
icon={<WalletIcon className={styles.walletIcon} />}
>
<p className={styles.address}>{address}</p>
</Field>

<div className={styles.actions}>
<ActionBtn onClick={hide} size="md">
Remain
</ActionBtn>
<ActionBtn color="green" onClick={onConfirm} size="md">
Upgrade Now
</ActionBtn>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from "react";
import { utils } from "ethers";
import { LoadingOverlay, useUrlComposer } from "@crossbell/ui";
import { useRefCallback } from "@crossbell/util-hooks";

import { WalletClaimCSB } from "../../../../scenes";
import {
useAccountState,
useWalletAccountBalance,
useWithdrawEmailAccount,
} from "../../../../hooks";
import { Header } from "../../components/header";
import { useScenesStore, useUpgradeAccountModal } from "../../stores";
import { SceneKind } from "../../types";
import { Confirm } from "./confirm";

export type ConfirmUpgradeProps = {
scene: "claim-csb" | "confirm";
};

export function ConfirmUpgrade({ scene }: ConfirmUpgradeProps) {
const [goTo, updateLast] = useScenesStore((s) => [s.goTo, s.updateLast]);
const { hide: hideModal } = useUpgradeAccountModal();
const urlComposer = useUrlComposer();
const { balance } = useWalletAccountBalance();

const {
account,
mutate: withdraw,
isLoading,
} = useWithdrawEmailAccount({
onSuccess() {
const character = useAccountState.getState().computed.account?.character;

goTo({
kind: SceneKind.congrats,
title: "Congrats!",
desc: "Now you can return into the feed and enjoy Crossbell.",
tips: "Welcome to new Crossbell",
timeout: "15s",
btnText: character ? "Check Character" : "Close",
onClose: hideModal,
onClickBtn: () => {
if (character) {
window.open(urlComposer.characterUrl(character), "_blank");
}
hideModal();
},
});
},
});

const handleConfirm = useRefCallback(() => {
if (!account.wallet || !account.email || isLoading) return;

const hasEnoughCSB = !!balance?.value.gte(utils.parseEther("0.001"));

if (hasEnoughCSB) {
withdraw();
} else {
updateLast({ kind: SceneKind.confirmUpgrade, scene: "claim-csb" });
}
});

return (
<>
{scene === "confirm" && <Confirm onConfirm={handleConfirm} />}

{scene === "claim-csb" && (
<WalletClaimCSB
Header={Header}
onSuccess={withdraw}
claimBtnText="Finish"
/>
)}

<LoadingOverlay visible={isLoading}>Upgrading...</LoadingOverlay>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";
import { useRefCallback } from "@crossbell/util-hooks";

import {
ConnectWallet as Main,
ConnectWalletProps as Props,
} from "../../../../scenes";
import { Header } from "../../components/header";
import { useScenesStore } from "../../stores";
import { SceneKind } from "../../types";

export type ConnectWalletProps = Omit<Props, "Header" | "onConnect">;

export function ConnectWallet({ wallet }: ConnectWalletProps) {
const updateLast = useScenesStore((s) => s.updateLast);

const handleConnect = useRefCallback(() => {
updateLast({ kind: SceneKind.confirmUpgrade, scene: "confirm" });
});

return <Main onConnect={handleConnect} wallet={wallet} Header={Header} />;
}

0 comments on commit 6bb38b6

Please sign in to comment.