Skip to content

Commit

Permalink
feat(billing): implement wallet type switch
Browse files Browse the repository at this point in the history
refs #247
  • Loading branch information
ygrishajev committed Aug 28, 2024
1 parent bd4c06b commit 65864fc
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 132 deletions.
96 changes: 54 additions & 42 deletions apps/deploy-web/src/components/layout/WalletStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,32 @@ import {
TooltipContent,
TooltipTrigger
} from "@akashnetwork/ui/components";
import { Bank, HandCard, LogOut, MoreHoriz, Wallet } from "iconoir-react";
import { Bank, CoinsSwap, HandCard, LogOut, MoreHoriz, Wallet } from "iconoir-react";
import Link from "next/link";
import { useRouter } from "next/navigation";

import { LoginRequiredLink } from "@src/components/user/LoginRequiredLink";
import { ConnectManagedWalletButton } from "@src/components/wallet/ConnectManagedWalletButton";
import { envConfig } from "@src/config/env.config";
import { useWallet } from "@src/context/WalletProvider";
import { useLoginRequiredEventHandler } from "@src/hooks/useLoginRequiredEventHandler";
import { useTotalWalletBalance } from "@src/hooks/useWalletBalance";
import { udenomToDenom } from "@src/utils/mathHelpers";
import { UrlService } from "@src/utils/urlUtils";
import { FormattedDecimal } from "../shared/FormattedDecimal";
import { ConnectWalletButton } from "../wallet/ConnectWalletButton";

const goToCheckout = () => {
window.location.href = "/api/proxy/v1/checkout";
};

const withBilling = envConfig.NEXT_PUBLIC_BILLING_ENABLED;

export function WalletStatus() {
const { walletName, address, walletBalances, logout, isWalletLoaded, isWalletConnected, isManaged, isWalletLoading, isTrialing } = useWallet();
const { walletName, address, walletBalances, logout, isWalletLoaded, isWalletConnected, isManaged, isWalletLoading, isTrialing, switchWalletType } =
useWallet();
const walletBalance = useTotalWalletBalance();
const router = useRouter();
function onDisconnectClick() {
logout();
}
const whenLoggedIn = useLoginRequiredEventHandler();

const onAuthorizeSpendingClick = () => {
router.push(UrlService.settingsAuthorizations());
Expand All @@ -46,29 +51,48 @@ export function WalletStatus() {
isWalletConnected ? (
<>
<div className="flex items-center pr-2">
{!isManaged && (
<div className="pl-2 pr-2">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHoriz />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onAuthorizeSpendingClick()}>
<Bank />
&nbsp;Authorize Spending
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onDisconnectClick()}>
<LogOut />
&nbsp;Disconnect Wallet
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}

<div className="pl-2 pr-2">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHoriz />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{!isManaged && (
<>
<DropdownMenuItem onClick={() => onAuthorizeSpendingClick()}>
<Bank />
&nbsp;Authorize Spending
</DropdownMenuItem>
<DropdownMenuItem onClick={logout}>
<LogOut />
&nbsp;Disconnect Wallet
</DropdownMenuItem>
{withBilling && (
<DropdownMenuItem onClick={switchWalletType}>
<CoinsSwap />
&nbsp;Switch to USD billing
</DropdownMenuItem>
)}
</>
)}
{withBilling && isManaged && (
<>
<DropdownMenuItem onClick={whenLoggedIn(goToCheckout, "Sign In or Sign Up to top up your balance")}>
<HandCard />
&nbsp;Top up balance
</DropdownMenuItem>
<DropdownMenuItem onClick={switchWalletType}>
<CoinsSwap />
&nbsp;Switch to wallet billing
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center text-left">
<div className="flex items-center text-sm font-bold">
<Wallet className="text-xs" />
Expand Down Expand Up @@ -114,18 +138,6 @@ export function WalletStatus() {
</div>
</TooltipContent>
)}
{isManaged && (
<TooltipContent>
<LoginRequiredLink
className="flex cursor-pointer flex-row text-base"
href="/api/proxy/v1/checkout"
message="Sign In or Sign Up to top up your balance"
>
<HandCard className="text-xs" />
<span className="ml-1 text-xs">Top up balance</span>
</LoginRequiredLink>
</TooltipContent>
)}
</Tooltip>
</div>
)}
Expand All @@ -134,7 +146,7 @@ export function WalletStatus() {
</>
) : (
<div>
{envConfig.NEXT_PUBLIC_BILLING_ENABLED && <ConnectManagedWalletButton className="mb-2 mr-2 w-full md:mb-0 md:w-auto" />}
{withBilling && <ConnectManagedWalletButton className="mb-2 mr-2 w-full md:mb-0 md:w-auto" />}
<ConnectWalletButton className="w-full md:w-auto" />
</div>
)
Expand Down
48 changes: 4 additions & 44 deletions apps/deploy-web/src/components/user/LoginRequiredLink.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React, { useCallback } from "react";
import { usePopup } from "@akashnetwork/ui/context";
import React from "react";
import Link, { LinkProps } from "next/link";

import { useUser } from "@src/hooks/useUser";
import { useLoginRequiredEventHandler } from "@src/hooks/useLoginRequiredEventHandler";
import { FCWithChildren } from "@src/types/component";
import { UrlService } from "@src/utils/urlUtils";

export const LoginRequiredLink: FCWithChildren<
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> &
Expand All @@ -13,44 +11,6 @@ export const LoginRequiredLink: FCWithChildren<
message: string;
} & React.RefAttributes<HTMLAnchorElement>
> = ({ message, ...props }) => {
const { requireAction } = usePopup();
const user = useUser();
const showLoginPrompt = useCallback(
() =>
requireAction({
message,
actions: [
{
label: "Sign in",
side: "left",
size: "lg",
variant: "secondary",
onClick: () => {
window.location.href = UrlService.login();
}
},
{
label: "Sign up",
side: "right",
size: "lg",
onClick: () => {
window.location.href = UrlService.signup();
}
}
]
}),
[message, requireAction]
);

return (
<Link
{...props}
onClick={event => {
if (!user.userId) {
event.preventDefault();
showLoginPrompt();
}
}}
/>
);
const whenLoggedIn = useLoginRequiredEventHandler();
return <Link {...props} onClick={whenLoggedIn(props.onClick || (() => {}), message)} />;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import React, { ReactNode } from "react";
import { Button, ButtonProps } from "@akashnetwork/ui/components";
import { Button, ButtonProps, Spinner } from "@akashnetwork/ui/components";
import { Rocket } from "iconoir-react";

import { useWallet } from "@src/context/WalletProvider";
Expand All @@ -12,12 +12,12 @@ interface Props extends ButtonProps {
}

export const ConnectManagedWalletButton: React.FunctionComponent<Props> = ({ className = "", ...rest }) => {
const { connectManagedWallet } = useWallet();
const { connectManagedWallet, hasManagedWallet, isWalletLoading } = useWallet();

return (
<Button variant="outline" onClick={connectManagedWallet} className={cn("border-primary", className)} {...rest}>
<Rocket className="text-xs" />
<span className="m-2 whitespace-nowrap">Start Trial</span>
<Button variant="outline" onClick={connectManagedWallet} className={cn("border-primary", className)} {...rest} disabled={isWalletLoading}>
{isWalletLoading ? <Spinner size="small" className="mr-2" /> : <Rocket className="text-xs" />}
<span className="m-2 whitespace-nowrap">{hasManagedWallet ? "Switch to USD billing" : "Start Trial"}</span>
</Button>
);
};
67 changes: 54 additions & 13 deletions apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { event } from "nextjs-google-analytics";
import { SnackbarKey, useSnackbar } from "notistack";

import { LoadingState, TransactionModal } from "@src/components/layout/TransactionModal";
import { useAllowance } from "@src/hooks/useAllowance";
import { useUsdcDenom } from "@src/hooks/useDenom";
import { useManagedWallet } from "@src/hooks/useManagedWallet";
import { useUser } from "@src/hooks/useUser";
Expand All @@ -23,10 +24,9 @@ import { AnalyticsEvents } from "@src/utils/analytics";
import { STATS_APP_URL, uAktDenom } from "@src/utils/constants";
import { customRegistry } from "@src/utils/customRegistry";
import { UrlService } from "@src/utils/urlUtils";
import { LocalWalletDataType } from "@src/utils/walletUtils";
import { getSelectedStorageWallet, LocalWallet, updateStorageManagedWallet } from "@src/utils/walletUtils";
import { useSelectedChain } from "../CustomChainProvider";
import { useSettings } from "../SettingsProvider";
import { useAllowance } from "@src/hooks/useAllowance";

const ERROR_MESSAGES = {
5: "Insufficient funds",
Expand Down Expand Up @@ -58,6 +58,8 @@ type ContextType = {
isWalletLoading: boolean;
isTrialing: boolean;
creditAmount?: number;
switchWalletType: () => void;
hasManagedWallet: boolean;
};

const WalletProviderContext = React.createContext<ContextType>({} as ContextType);
Expand All @@ -69,6 +71,8 @@ const MESSAGE_STATES: Record<string, LoadingState> = {
"/akash.deployment.v1beta3.MsgUpdateDeployment": "updatingDeployment"
};

const initialWallet = getSelectedStorageWallet();

export const WalletProvider = ({ children }) => {
const [walletBalances, setWalletBalances] = useState<Balances | null>(null);
const [isWalletLoaded, setIsWalletLoaded] = useState<boolean>(true);
Expand All @@ -81,14 +85,22 @@ export const WalletProvider = ({ children }) => {
const user = useUser();
const userWallet = useSelectedChain();
const { wallet: managedWallet, isLoading, create, refetch } = useManagedWallet();
const { address: walletAddress, username, isWalletConnected } = useMemo(() => managedWallet || userWallet, [managedWallet, userWallet]);
const [selectedWalletType, selectWalletType] = useState<"managed" | "custodial">(
initialWallet?.selected && initialWallet?.isManaged ? "managed" : "custodial"
);
const {
address: walletAddress,
username,
isWalletConnected
} = useMemo(() => (selectedWalletType === "managed" && managedWallet) || userWallet, [managedWallet, userWallet, selectedWalletType]);
const { addEndpoints } = useManager();
const isManaged = !!managedWallet;
const isManaged = useMemo(() => !!managedWallet && managedWallet?.address === walletAddress, [walletAddress, managedWallet]);

const {
fee: { default: feeGranter }
} = useAllowance(walletAddress as string, isManaged);

useWhen(managedWallet, refreshBalances);
useWhen(isManaged, refreshBalances);

useEffect(() => {
if (!settings.apiEndpoint || !settings.rpcEndpoint) return;
Expand All @@ -108,6 +120,33 @@ export const WalletProvider = ({ children }) => {
})();
}, [settings?.rpcEndpoint, userWallet.isWalletConnected]);

function switchWalletType() {
if (selectedWalletType === "custodial" && !managedWallet) {
userWallet.disconnect();
}

if (selectedWalletType === "managed" && !userWallet.isWalletConnected) {
userWallet.connect();
}

if (selectedWalletType === "managed" && managedWallet) {
updateStorageManagedWallet({
...managedWallet,
selected: false
});
}

selectWalletType(prev => (prev === "custodial" ? "managed" : "custodial"));
}

function connectManagedWallet() {
if (managedWallet) {
selectWalletType("managed");
} else {
create();
}
}

async function createStargateClient() {
const selectedNetwork = networkStore.getSelectedNetwork();

Expand Down Expand Up @@ -165,12 +204,12 @@ export const WalletProvider = ({ children }) => {

async function loadWallet(): Promise<void> {
const selectedNetwork = networkStore.getSelectedNetwork();
const storageWallets = JSON.parse(localStorage.getItem(`${selectedNetwork.id}/wallets`) || "[]") as LocalWalletDataType[];
const storageWallets = JSON.parse(localStorage.getItem(`${selectedNetwork.id}/wallets`) || "[]") as LocalWallet[];

let currentWallets = storageWallets ?? [];

if (!currentWallets.some(x => x.address === walletAddress)) {
currentWallets.push({ name: username || "", address: walletAddress as string, selected: true });
currentWallets.push({ name: username || "", address: walletAddress as string, selected: true, isManaged: false });
}

currentWallets = currentWallets.map(x => ({ ...x, selected: x.address === walletAddress }));
Expand All @@ -186,7 +225,7 @@ export const WalletProvider = ({ children }) => {
let txResult: TxOutput;

try {
if (!!user?.id && managedWallet) {
if (!!user?.id && isManaged) {
const mainMessage = msgs.find(msg => msg.typeUrl in MESSAGE_STATES);

if (mainMessage) {
Expand Down Expand Up @@ -300,7 +339,7 @@ export const WalletProvider = ({ children }) => {
};

async function refreshBalances(address?: string): Promise<{ uakt: number; usdc: number }> {
if (managedWallet) {
if (isManaged && managedWallet) {
const wallet = await refetch();
const walletBalances = {
uakt: 0,
Expand Down Expand Up @@ -345,14 +384,16 @@ export const WalletProvider = ({ children }) => {
isWalletConnected: isWalletConnected,
isWalletLoaded: isWalletLoaded,
connectWallet,
connectManagedWallet: create,
connectManagedWallet,
logout,
signAndBroadcastTx,
refreshBalances,
isManaged: isManaged,
isManaged,
isWalletLoading: isLoading,
isTrialing: !!managedWallet?.isTrialing,
creditAmount: managedWallet?.creditAmount
isTrialing: isManaged && !!managedWallet?.isTrialing,
creditAmount: isManaged ? managedWallet?.creditAmount : 0,
hasManagedWallet: !!managedWallet,
switchWalletType
}}
>
{children}
Expand Down
Loading

0 comments on commit 65864fc

Please sign in to comment.