feat: 10x token (#354)

* feat: 10x token

* feat: power withdraw modal

* feat: update token text
This commit is contained in:
DIYgod 2024-09-13 12:40:59 +08:00 committed by GitHub
parent 77419ac1b9
commit 9088aea651
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 229 additions and 65 deletions

View File

@ -4362,7 +4362,7 @@ declare const _routes: hono_hono_base.HonoBase<
input: {
json: {
entryId: string
amount: "1000000000000000000" | "2000000000000000000"
amount: string
}
}
output: {
@ -4465,7 +4465,7 @@ declare const _routes: hono_hono_base.HonoBase<
status: 200
}
}
"/wallets/transactions/withdrawable": {
"/wallets/transactions/feed-withdrawable": {
$get: {
input: {
query: {
@ -4484,7 +4484,7 @@ declare const _routes: hono_hono_base.HonoBase<
status: 200
}
}
"/wallets/transactions/withdraw": {
"/wallets/transactions/feed-withdraw": {
$post: {
input: {
json: {
@ -4501,6 +4501,24 @@ declare const _routes: hono_hono_base.HonoBase<
status: 200
}
}
"/wallets/transactions/withdraw": {
$post: {
input: {
json: {
address: string
amount: string
}
}
output: {
code: 0
data: {
transactionHash: string
}
}
outputFormat: "json" | "text"
status: 200
}
}
"/wallets/transactions/claim-check": {
$get: {
input: {}
@ -4544,6 +4562,16 @@ declare const _routes: hono_hono_base.HonoBase<
status: 200
}
}
"/wallets/refresh": {
$post: {
input: {}
output: {
code: 0
}
outputFormat: "json" | "text"
status: 200
}
}
} & {
"/subscriptions": {
$get: {

View File

@ -9,3 +9,6 @@ export const QUERY_PERSIST_KEY = getStorageNS("REACT_QUERY_OFFLINE_CACHE")
export const ROUTE_FEED_PENDING = "all"
export const ROUTE_ENTRY_PENDING = "pending"
export const ROUTE_FEED_IN_FOLDER = "folder-"
export const DAILY_CLAIM_AMOUNT = "20"
export const INVITATION_PRICE = "100"

View File

@ -1,4 +1,3 @@
import * as HoverCard from "@radix-ui/react-hover-card"
import { useUISettingKey } from "@renderer/atoms/settings/ui"
import { ScrollArea } from "@renderer/components/ui/scroll-area"

View File

@ -267,7 +267,7 @@ const SearchResultCount: FC<{
const searchInstance = React.useContext(SearchCmdKContext)
const hasKeyword = useSearchStore((s) => !!s.keyword)
const searchType = useSearchStore((s) => s.searchType)
// eslint-disable-next-line react-compiler/react-compiler
const recordCountPromise = useMemo(async () => {
let count = 0
const counts = await searchInstance?.then((s) => s.counts)

View File

@ -19,6 +19,7 @@ import {
TooltipPortal,
TooltipTrigger,
} from "@renderer/components/ui/tooltip"
import { INVITATION_PRICE } from "@renderer/constants"
import { useAuthQuery } from "@renderer/hooks/common"
import { apiClient } from "@renderer/lib/api-fetch"
import { toastFetchError } from "@renderer/lib/error-parser"
@ -42,8 +43,8 @@ export const SettingInvitations = () => {
use.
</p>
<p className="flex items-center">
<span>You can spend 10 </span>
<i className="i-mgc-power ml-1 mr-0.5 text-base text-accent" />
<span>You can spend {INVITATION_PRICE} </span>
<i className="i-mgc-power mx-0.5 text-base text-accent" />
<span> Power to generate an invitation code for your friends.</span>
</p>
</div>
@ -153,7 +154,7 @@ const ConfirmModalContent = ({ dismiss }: { dismiss: () => void }) => {
return (
<>
<div className="flex items-center">
<span>Generating an invitation code will cost you 10 </span>
<span>Generating an invitation code will cost you {INVITATION_PRICE} </span>
<i className="i-mgc-power mx-1 text-base text-accent" />
<span>Power. Do you want to continue?</span>
</div>

View File

@ -1,6 +1,7 @@
import { TooltipTrigger } from "@radix-ui/react-tooltip"
import { Button } from "@renderer/components/ui/button"
import { Tooltip, TooltipContent } from "@renderer/components/ui/tooltip"
import { DAILY_CLAIM_AMOUNT } from "@renderer/constants"
import { useClaimCheck, useClaimWalletDailyRewardMutation } from "@renderer/queries/wallet"
export const ClaimDailyReward = () => {
@ -22,7 +23,9 @@ export const ClaimDailyReward = () => {
</Button>
</TooltipTrigger>
<TooltipContent>
{canClaim ? "Claim your 2 Daily Power now!" : `You have already claimed today.`}
{canClaim
? `Claim your ${DAILY_CLAIM_AMOUNT} Daily Power now!`
: `You have already claimed today.`}
</TooltipContent>
</Tooltip>
)

View File

@ -1,4 +1,5 @@
import { useWhoami } from "@renderer/atoms/user"
import { Button } from "@renderer/components/ui/button"
import { CopyButton } from "@renderer/components/ui/code-highlighter"
import { LoadingWithIcon } from "@renderer/components/ui/loading"
import {
@ -7,9 +8,13 @@ import {
TooltipPortal,
TooltipTrigger,
} from "@renderer/components/ui/tooltip"
import { DAILY_CLAIM_AMOUNT } from "@renderer/constants"
import { apiClient } from "@renderer/lib/api-fetch"
import { cn } from "@renderer/lib/utils"
import { SettingSectionTitle } from "@renderer/modules/settings/section"
import { Balance } from "@renderer/modules/wallet/balance"
import { useWallet } from "@renderer/queries/wallet"
import { useWallet, wallet as walletActions } from "@renderer/queries/wallet"
import { useMutation } from "@tanstack/react-query"
import { ClaimDailyReward } from "./claim-daily-reward"
import { CreateWallet } from "./create-wallet"
@ -20,6 +25,15 @@ export const MyWalletSection = () => {
const wallet = useWallet({ userId: user?.id })
const myWallet = wallet.data?.[0]
const refreshMutation = useMutation({
mutationFn: async () => {
await apiClient.wallets.refresh.$post()
},
onSuccess: () => {
walletActions.get().invalidate()
},
})
if (wallet.isPending) {
return (
<div className="center absolute inset-0 flex">
@ -51,7 +65,10 @@ export const MyWalletSection = () => {
</a>
.
</p>
<p>You can claim 2 free Power daily, which can be used to tip RSS entries on Follow.</p>
<p>
You can claim {DAILY_CLAIM_AMOUNT} free Power daily, which can be used to tip RSS
entries on Follow.
</p>
</div>
<SettingSectionTitle margin="compact" title="Your Address" />
<div className="flex items-center gap-2 text-sm">
@ -66,9 +83,23 @@ export const MyWalletSection = () => {
</div>
<SettingSectionTitle title="Your Balance" margin="compact" />
<div className="mb-2 flex items-end justify-between">
<Balance className="text-xl font-bold text-accent">
{BigInt(myWallet.dailyPowerToken || 0n) + BigInt(myWallet.cashablePowerToken || 0n)}
</Balance>
<div className="flex items-center gap-1">
<Balance className="text-xl font-bold text-accent">
{BigInt(myWallet.dailyPowerToken || 0n) + BigInt(myWallet.cashablePowerToken || 0n)}
</Balance>
<Button
variant="ghost"
onClick={() => refreshMutation.mutate()}
disabled={refreshMutation.isPending}
>
<i
className={cn(
"i-mgc-refresh-2-cute-re",
refreshMutation.isPending && "animate-spin",
)}
/>
</Button>
</div>
<div className="flex gap-2">
<WithdrawButton />
<ClaimDailyReward />
@ -77,25 +108,8 @@ export const MyWalletSection = () => {
<Tooltip>
<TooltipTrigger className="block">
<div className="flex flex-row items-center gap-x-2 text-xs text-zinc-600 dark:text-neutral-400">
<span className="flex w-[120px] items-center gap-1 text-left">
Daily Power
<i className="i-mingcute-question-line" />
</span>
<Balance>{myWallet.dailyPowerToken}</Balance>
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent align="start" className="z-[999]">
<p>1. Daily Power can only be used for tipping others.</p>
<p>2. You can claim 2 free Daily Power daily.</p>
</TooltipContent>
</TooltipPortal>
</Tooltip>
<Tooltip>
<TooltipTrigger className="block">
<div className="flex flex-row items-center gap-x-2 text-xs text-zinc-600 dark:text-neutral-400">
<span className="flex w-[120px] items-center gap-1 text-left">
Cashable Power <i className="i-mingcute-question-line" />
<span className="flex items-center gap-1 text-left">
Withdrawable <i className="i-mingcute-question-line" />
</span>
<Balance>{myWallet.cashablePowerToken}</Balance>
@ -103,9 +117,9 @@ export const MyWalletSection = () => {
</TooltipTrigger>
<TooltipPortal>
<TooltipContent align="start" className="z-[999]">
<p>1. Cashable Power can be withdrawn to your wallet for trading.</p>
<p>
2. Cashable Power is the Power you have recharged and the tips you have received.
Withdrawable Power includes both the tips you've received and the Power you've
recharged.
</p>
</TooltipContent>
</TooltipPortal>

View File

@ -1,14 +1,137 @@
import { TooltipTrigger } from "@radix-ui/react-tooltip"
import { zodResolver } from "@hookform/resolvers/zod"
import { useWhoami } from "@renderer/atoms/user"
import { Button } from "@renderer/components/ui/button"
import { Tooltip, TooltipContent } from "@renderer/components/ui/tooltip"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@renderer/components/ui/form"
import { Input } from "@renderer/components/ui/input"
import { useModalStack } from "@renderer/components/ui/modal"
import { apiClient } from "@renderer/lib/api-fetch"
import { cn } from "@renderer/lib/utils"
import { Balance } from "@renderer/modules/wallet/balance"
import { useWallet, wallet as walletActions } from "@renderer/queries/wallet"
import { useMutation } from "@tanstack/react-query"
import { from, toNumber } from "dnum"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
export const WithdrawButton = () => (
<Tooltip>
<TooltipTrigger>
<Button variant="outline" disabled={true}>
Withdraw
</Button>
</TooltipTrigger>
<TooltipContent>Comming soon...</TooltipContent>
</Tooltip>
)
export const WithdrawButton = () => {
const { present } = useModalStack()
const onClick = () => {
present({
title: "Withdraw Power",
content: ({ dismiss }) => <WithdrawModalContent dismiss={dismiss} />,
})
}
return (
<Button variant="outline" onClick={onClick}>
Withdraw
</Button>
)
}
const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => {
const user = useWhoami()
const wallet = useWallet({ userId: user?.id })
const cashablePowerTokenBigInt = [BigInt(wallet.data?.[0].cashablePowerToken || 0n), 18] as const
const cashablePowerTokenNumber = toNumber(cashablePowerTokenBigInt)
const formSchema = z.object({
address: z.string().startsWith("0x").length(42),
amount: z.number().min(0).max(cashablePowerTokenNumber),
})
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})
const mutation = useMutation({
mutationFn: async ({ address, amount }: { address: string; amount: number }) => {
const amountBigInt = from(amount, 18)[0]
await apiClient.wallets.transactions.withdraw.$post({
json: {
address,
amount: amountBigInt.toString(),
},
})
},
})
const onSubmit = (values: z.infer<typeof formSchema>) => {
mutation.mutate(values)
}
useEffect(() => {
if (mutation.isError) {
toast.error(`Withdrawal failed: ${mutation.error?.message}`)
}
}, [mutation.isError])
useEffect(() => {
if (mutation.isSuccess) {
toast.success("Withdrawal successful!")
walletActions.get().invalidate()
walletActions.transactions.get().invalidate()
dismiss()
}
}, [mutation.isSuccess])
return (
<>
<div className={cn(!cashablePowerTokenNumber && "text-orange-700", "mb-4 text-sm")}>
You have{" "}
<Balance className="inline-block">{wallet.data?.[0].cashablePowerToken || "0"}</Balance>{" "}
withdrawable Power in your wallet.
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-96 space-y-4">
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Your Ethereum Address</FormLabel>
<FormControl>
<Input autoFocus {...field} placeholder="0x..." />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<Input
autoFocus
{...field}
type="number"
onChange={(value) => field.onChange(value.target.valueAsNumber)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="center flex">
<Button disabled={!form.formState.isValid} type="submit" isLoading={mutation.isPending}>
Submit
</Button>
</div>
</form>
</Form>
</>
)
}

View File

@ -22,8 +22,8 @@ export const Balance = ({
const formattedFull = format(n, { digits: 18, trailingZeros: true })
const Content = (
<div className={cn("font-mono tabular-nums", className)}>
{formatted} {withSuffix && <span>POWER</span>}
<div className={cn("tabular-nums", className)}>
{formatted} {withSuffix && <span>Power</span>}
</div>
)
@ -33,8 +33,8 @@ export const Balance = ({
<Tooltip>
<TooltipTrigger asChild>{Content}</TooltipTrigger>
<TooltipContent>
<div className="font-mono text-sm">
<span className="font-bold tabular-nums">{formattedFull}</span> <span>POWER</span>
<div className="text-sm">
<span className="font-bold tabular-nums">{formattedFull}</span> <span>Power</span>
</div>
</TooltipContent>
</Tooltip>

View File

@ -16,7 +16,7 @@ import { useFeedClaimModal } from "../claim"
import { useSettingModal } from "../settings/modal/hooks-hack"
import { Balance } from "./balance"
const DEFAULT_RECOMMENDED_TIP = 1
const DEFAULT_RECOMMENDED_TIP = 10
const useMyWallet = () => {
const user = useWhoami()
@ -56,7 +56,7 @@ const TipModalContent_: FC<{
const tipMutation = useWalletTipMutation()
const [amount, setAmount] = useState<1 | 2>(DEFAULT_RECOMMENDED_TIP)
const [amount, setAmount] = useState<number>(DEFAULT_RECOMMENDED_TIP)
const amountBigInt = from(amount, 18)[0]
@ -146,13 +146,10 @@ const TipModalContent_: FC<{
<span>Amount</span>
</div>
<RadioGroup
value={amount.toString()}
onValueChange={(value) => setAmount(Number(value) as 1 | 2)}
>
<RadioGroup value={amount.toString()} onValueChange={(value) => setAmount(Number(value))}>
<div className="grid grid-cols-2 gap-2">
<RadioCard wrapperClassName="justify-center" label="1 Power" value="1" />
<RadioCard wrapperClassName="justify-center" label="2 Power" value="2" />
<RadioCard wrapperClassName="justify-center" label="10 Power" value="10" />
<RadioCard wrapperClassName="justify-center" label="20 Power" value="20" />
</div>
</RadioGroup>

View File

@ -83,7 +83,7 @@ export const I18nProvider: FC<PropsWithChildren> = ({ children }) => {
const [currentI18NInstance, update] = useAtom(i18nAtom)
if (import.meta.env.DEV)
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/rules-of-hooks
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(
() =>
EventBus.subscribe("I18N_UPDATE", () => {

View File

@ -1,9 +1,5 @@
import type { EntryModel } from "@renderer/models"
import {
EntryService,
FeedService,
SubscriptionService,
} from "@renderer/services"
import { EntryService, FeedService, SubscriptionService } from "@renderer/services"
import type { IFuseOptions } from "fuse.js"
import Fuse from "fuse.js"