Skip to content

Commit

Permalink
Merge pull request #23 from DavidTParks/web3-wallet-connection
Browse files Browse the repository at this point in the history
Web3 wallet connection
  • Loading branch information
DavidTParks committed Feb 1, 2023
2 parents 799abbe + e5ad865 commit 0c3fdfa
Show file tree
Hide file tree
Showing 26 changed files with 19,017 additions and 11,387 deletions.
6 changes: 5 additions & 1 deletion .env.example
Expand Up @@ -11,4 +11,8 @@ POSTMARK_MARKETING_API_SECRET=""
PROTECTED_API_ROUTE_KEY=""
NEXT_PUBLIC_AXIOM_INGEST_ENDPOINT=""
AXIOM_TOKEN=""
AXIOM_DATASET="vercel"
AXIOM_DATASET="vercel"
NEXT_PUBLIC_SUPABASE_URL=""
NEXT_PUBLIC_SUPABASE_KEY=""
NEXT_PUBLIC_INFURA_KEY=""
NEXT_PUBLIC_ENABLE_TESTNETS="true"
13 changes: 13 additions & 0 deletions app/(dashboard)/dashboard/settings/wallet/loading.tsx
@@ -0,0 +1,13 @@
import { DashboardHeader } from "@/components/dashboard/header"
import { DashboardShell } from "@/components/dashboard/shell"
import { Card } from "@/ui/card"

export default function DashboardSettingsLoading() {
return (
<DashboardShell>
<div className="grid gap-10">
<Card.Skeleton />
</div>
</DashboardShell>
)
}
125 changes: 125 additions & 0 deletions app/(dashboard)/dashboard/settings/wallet/page.tsx
@@ -0,0 +1,125 @@
import { DashboardShell } from "@/components/dashboard/shell"
import { AddWalletForm } from "@/components/dashboard/settings/wallet/add-wallet-form"
import { notFound } from "next/navigation"
import { getCurrentUser } from "@/lib/session"
import { db } from "@/lib/db"
import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"
import { WalletOperations } from "@/components/dashboard/settings/wallet/wallet-operations"

export default async function NotificationsPage() {
const user = await getCurrentUser()

if (!user) {
return notFound()
}

const dbUser = await db.user.findUnique({
where: {
id: user.id,
},
include: {
wallets: true,
},
})

if (!dbUser) return null

return (
<DashboardShell>
<div>
<AddWalletForm
user={{
id: dbUser.id,
}}
/>
<div className="mt-12 sm:flex sm:items-center">
<div className="sm:flex-auto">
<h3 className="text-xl font-semibold text-brandtext-500">
Wallets
</h3>
<p className="mt-2 text-sm text-brandtext-600">
You can add up to 10 wallets to your account. Set a
default wallet to be used to receive ERC-20 utility
token rewards for platform activity.
</p>
{dbUser.wallets?.length ? (
<div className="mt-4 flex flex-col">
<div className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className=" min-w-full py-2 align-middle md:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-raised ring-opacity-5 md:rounded-lg">
<table className="min-w-full divide-y divide-raised-border">
<thead className="bg-raised">
<tr>
<th
scope="col"
className="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-brandtext-500 sm:pl-6"
>
Default
</th>
<th
scope="col"
className="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-brandtext-500"
>
Wallet
</th>
<th
scope="col"
className="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-brandtext-500"
></th>
</tr>
</thead>
<tbody className="divide-y divide-raised-border bg-raised">
{dbUser?.wallets?.map(
(wallet) => (
<tr key={wallet.id}>
<td className="inline-flex items-center gap-2 whitespace-nowrap py-3 pl-4 pr-3 text-sm text-brandtext-600 sm:pl-6">
{wallet.default
? "True"
: "False"}
</td>
<td className="whitespace-nowrap px-2 py-3 text-sm font-medium text-brandtext-600">
{
wallet.address
}
</td>
<td>
<WalletOperations
user={{
id: user.id,
}}
userWallet={{
id: wallet.id,
default:
wallet.default,
}}
/>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
) : (
<>
<EmptyPlaceholder className="mt-4 min-h-[200px]">
<EmptyPlaceholder.Icon name="eth" />
<EmptyPlaceholder.Title>
No wallets added
</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description className="mb-0">
There are no wallets associated with
this account.
</EmptyPlaceholder.Description>
</EmptyPlaceholder>
</>
)}
</div>
</div>
</div>
</DashboardShell>
)
}
13 changes: 9 additions & 4 deletions app/layout.tsx
Expand Up @@ -7,6 +7,9 @@ import { SiteFooter } from "@/components/site-footer"
import { AnalyticsWrapper } from "@/components/analytics"
import { Providers } from "./providers"
export { reportWebVitals } from "next-axiom"
import { Web3Providers } from "./web3Providers"
import "@rainbow-me/rainbowkit/styles.css"
import Script from "next/script"

const satoshi = localFont({
src: "./Satoshi-Variable.woff2",
Expand Down Expand Up @@ -42,10 +45,12 @@ export default function RootLayout({
*/}
<head />
<body className="min-h-screen">
{children}
<Toaster position="bottom-right" />
<SiteFooter />
<AnalyticsWrapper />
<Web3Providers>
{children}
<Toaster position="bottom-right" />
<SiteFooter />
<AnalyticsWrapper />
</Web3Providers>
</body>
</html>
</Providers>
Expand Down
14 changes: 14 additions & 0 deletions app/web3Providers.tsx
@@ -0,0 +1,14 @@
"use client"

import { chains, wagmiClient } from "@/lib/wagmiClient"
import { RainbowKitProvider } from "@rainbow-me/rainbowkit"
import { PropsWithChildren } from "react"
import { WagmiConfig } from "wagmi"

export function Web3Providers({ children }: PropsWithChildren) {
return (
<WagmiConfig client={wagmiClient}>
<RainbowKitProvider chains={chains}>{children}</RainbowKitProvider>
</WagmiConfig>
)
}
186 changes: 186 additions & 0 deletions components/dashboard/settings/wallet/add-wallet-form.tsx
@@ -0,0 +1,186 @@
"use client"

import * as React from "react"
import { SiweMessage } from "siwe"
import { trpc } from "@/client/trpcClient"
import { Icons } from "@/components/icons"
import { cn } from "@/lib/utils"
import { userNotificationsSchema } from "@/lib/validations/user"
import { Button } from "@/ui/button"
import { Card } from "@/ui/card"
import { Checkbox } from "@/ui/checkbox"
import { toast } from "@/ui/toast"
import { zodResolver } from "@hookform/resolvers/zod"
import { User } from "@prisma/client"
import { useRouter } from "next/navigation"
import { FormProvider, useForm } from "react-hook-form"
import { z } from "zod"
import { ConnectWallet } from "./conect-wallet"
import { useAccount } from "wagmi"
import { Chip } from "@/ui/chip"
import { useFetchNonce } from "@/hooks/use-fetch-nonce"
import { useNetwork } from "wagmi"
import { useSignMessage } from "wagmi"

type FormData = z.infer<typeof userNotificationsSchema>

interface AddWalletFormProps extends React.HTMLAttributes<HTMLFormElement> {
user: Pick<User, "id">
}

export function AddWalletForm({
className,
user,
...props
}: AddWalletFormProps) {
const { isConnected, address } = useAccount()
const { chain } = useNetwork()
const { signMessageAsync } = useSignMessage()

const saveNotificationPreferences =
trpc.user.saveNotificationPreferences.useMutation()

const router = useRouter()

const methods = useForm<FormData>({
resolver: zodResolver(userNotificationsSchema),
})

const [state, setState] = React.useState<{
loading?: boolean
nonce?: string
}>({})

const fetchNonce = async () => {
try {
const nonceRes = await fetch("/api/nonce")
const nonce = await nonceRes.text()
setState((x) => ({ ...x, nonce }))
} catch (error) {
setState((x) => ({ ...x, error: error as Error }))
}
}

// Pre-fetch random nonce when button is rendered
// to ensure deep linking works for WalletConnect
// users on iOS when signing the SIWE message
React.useEffect(() => {
fetchNonce()
}, [])

async function onSubmit(data: FormData) {
try {
await saveNotificationPreferences.mutateAsync({
submissionAccepted: data.submissionAccepted,
newSubmission: data.newSubmission,
})
toast({
title: "Notification preferences updated",
message: "",
type: "success",
})
router.refresh()
} catch (e) {
return toast({
title: "Something went wrong.",
message: "Please refresh the page and try again.",
type: "error",
})
}
}

const signIn = async () => {
try {
const chainId = chain?.id

if (!address || !chainId) return

// Create SIWE message with pre-fetched nonce and sign with wallet
const message = new SiweMessage({
domain: window.location.host,
address,
statement: "Sign in with Ethereum to the app.",
uri: window.location.origin,
version: "1",
chainId,
nonce: state.nonce,
})

const signature = await signMessageAsync({
message: message.prepareMessage(),
})

// Verify signature
const verifyRes = await fetch(`/api/verify?userId=${user.id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message, signature }),
})
if (!verifyRes.ok) throw new Error("Error verifying message")

router.refresh()
return toast({
title: "Wallet linked",
message:
"You can now interact with all the blockchain elements on our platform!",
type: "success",
})
} catch (error) {
return toast({
title: "Something went wrong.",
message: "Please refresh the page and try again.",
type: "error",
})
}
}

return (
<FormProvider {...methods}>
<form
className={cn(className)}
onSubmit={methods.handleSubmit(onSubmit)}
{...props}
>
<Card>
<Card.Header>
<Card.Title className="flex items-center gap-2">
<Icons.eth />
Crypto Wallet{" "}
{isConnected ? (
<Chip size="small" intent="green">
Connected
</Chip>
) : (
<Chip size="small" intent="rose">
Not connected
</Chip>
)}
</Card.Title>
<Card.Description>
Connect your wallet to your Vamp account. This lets
you participate in our on-chain, subsidized
blockchain game on the Polygon network.
</Card.Description>
</Card.Header>
<Card.Content className="flex flex-col items-start gap-8 pb-4">
{isConnected ? (
<ConnectWallet />
) : (
"Connect your wallet"
)}
</Card.Content>
<Card.Footer className="flex flex-col items-start space-y-2 md:flex-row md:justify-between md:space-x-0">
{!isConnected && <ConnectWallet />}
{isConnected && (
<Button onClick={signIn}>
Sign message to link Wallet
</Button>
)}
</Card.Footer>
</Card>
</form>
</FormProvider>
)
}

1 comment on commit 0c3fdfa

@vercel
Copy link

@vercel vercel bot commented on 0c3fdfa Feb 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.