Next.jsとMetaMaskで実現するDApp向けユーザー設定ストレージの構築
DAOやDAppといったWeb3アプリケーションは、年々人気が高まっています。Web3の理念において、これらのプラットフォームは、ユーザーのIDを他者から、ときには本人自身からさえ隠したまま、よりパーソナルでカスタマイズされた体験を提供することが求められます。
本記事では、そうしたケースにおいてユーザー体験をいかに向上させるかを探っていきます。
ウォレットによる認証・ID確認
多くのアプリケーションでは、ETHなどの暗号資産を管理するためにMetaMaskのようなブロックチェーンウォレットが利用されています。こうしたウォレットを使うと、ユーザーはさまざまなチェーン上でアカウントを作成できます。各アカウントは秘密鍵によって生成されますが、同時にそのアカウントを表す公開アドレスも持っています。
公開アドレスは誰でも閲覧できますが、それに関連するトランザクションを操作できるのは所有者だけです。そのため、ウォレット接続はアプリケーションにとって堅牢な認証メカニズムとして機能します。
このプロジェクトについて
本プロジェクトでは、Next.jsを使ってユーザー設定(プリファレンス)ストレージシステムを実装します。ユーザーがインターフェースをカスタマイズしておくと、サイトに再訪した際には、意図した通りのUIが表示される仕組みです。
さらに発展的なアイデアとしては、企業のサブプラットフォームやクロスプラットフォーム間でUIを統一することも挙げられます。こうすれば、あるサイトで設定したプリファレンスをサポートする別のサイトへ移動した際にも、一貫した体験を得られます。
また、カスタマイズ用パラメータの数は自由に変更できるため、思い描く構造に合わせて柔軟に作り変えることも可能です。
サンプルプロジェクトでは、次の2つのパラメータのみを扱います。
- ウェブサイトのテーマ
- ユーザーの挨拶名(表示名)
はじめに
Upstash Redis環境変数の設定
- Upstashで無料のRedisデータベースを作成します。
.env.local.exampleファイルを.env.localとしてコピーします(Gitには無視されます)。UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENは、Upstashコンソールのデータベース詳細ページで確認できます。
依存パッケージのインストール
以下のコマンドで必要な依存パッケージをインストールします。
npm i @upstash/redis @metamask/detect-provider @mui/material @emotion/react @emotion/styled
api/store.jsファイルの作成
Redis SDKを設定し、ハンドラ関数をエクスポートします。
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()
export default async function handler(req, res) {
const accountID = await JSON.parse(req.body).accountID
const body = req.body
const setResult = await redis.set(accountID, body);
res.status(200).json({ result: setResult })
}
api/[accountAddress].jsファイルの作成
同様にRedis SDKを設定し、ハンドラをエクスポートします。
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()
export default async function handler(req, res) {
const accountID = req.query.accountAddress
const getResult = await redis.get(accountID)
res.status(200).json({ result: getResult })
}
index.jsファイルの設定
プロジェクト用の変数を定義する
const [accountAddress, setAccountAddress] = useState(null)
const [themePreference, setThemePreference] = useState("light")
const [greetingMessage, setGreetingMessage] = useState("Anonymous Person")
const [userSettings, setUserSettings] = useState(null)
// 接続済みかどうかを確認。接続済みならデータを取得する
useEffect(() => {
checkConnection()
getPreferences()
}, [accountAddress])
MetaMaskへの接続
async function connect() {
const provider = await detectEthereumProvider()
console.log("provider:", provider)
if (provider) {
console.log('Ethereum successfully detected!')
provider.request({ method: "eth_requestAccounts" }).then((accounts) => {
if (!accountAddress) {
setAccountAddress(accounts[0])
}
}).catch((err) => console.log(err))
console.log("window.ethereum:", window.ethereum)
getPreferences()
} else {
alert('Please install MetaMask!')
}
}
リロード後や時間経過後の接続状態チェック
async function checkConnection() {
const provider = await detectEthereumProvider()
if (provider) {
provider
.request({ method: 'eth_accounts' })
.then(accounts => {
setAccountAddress(accounts[0])
})
.catch(console.log)
} else {
console.log("Not connected, window.ethereum not found")
}
}
プリファレンスの保存
async function setPreferences(themePreference, greetingMessage) {
if (accountAddress) {
const res = await fetch(`/api/store`, {
method: "POST",
body: JSON.stringify({
accountID: accountAddress,
themePreference: themePreference,
greetingMessage: greetingMessage,
})
})
const data = await res.json()
}
else {
alert("No account address detected")
}
}
プリファレンスの取得
async function getPreferences(e) {
if (accountAddress) {
console.log("Fetching user preferences...")
const res = await fetch(`/api/${accountAddress}`, { method: "GET" })
const data = await res.json()
setUserSettings(data.result)
if (data.result) {
setThemePreference(data.result.themePreference)
setGreetingMessage(data.result.greetingMessage)
}
}
else {
console.log("No account connected yet!")
}
}
変数の状態を変更する
以下の関数を入力フィールドやボタンなどにバインドします。
async function handleDarkMode(e) {
console.log("themePreference:", themePreference)
const newPreference = themePreference == "light" ? "dark" : "light"
setThemePreference(newPreference)
await setPreferences(newPreference, greetingMessage)
await getPreferences()
}
async function takeGreetingMessage(e) {
// Enterキーで送信
if (e.keyCode == 13) {
const message = e.target.value
setGreetingMessage(message)
console.log(message)
await setPreferences(themePreference, message)
await getPreferences()
e.target.value = ""
}
}
上記の関数を使ったシンプルなUIを作成する
関連するMaterial-UIの依存関係をインポートしていることを確認してください。
return (
<div className={styles.container}>
<h2>Web3 Preferences Holder</h2>
<Button variant="contained" onClick={connect}>Connect Metamask</Button>
<p>
Lets you keep user preferences on cross-websites
</p>
<br />
<p>For example, take a greeter message from user.</p>
<TextField label="Call me..." variant="outlined" size="small" onKeyDown={takeGreetingMessage} />
<br />
<br />
<Button onClick={handleDarkMode} variant="contained" size="small" style={{ backgroundColor: "#3D3B3B" }} > Switch Dark Mode </Button>
<p>Sample Component/Page:</p>
<Showcase userSettings={userSettings} />
</div>
)
componentsディレクトリにsampleComponent.jsxを作成
これはWebインターフェースやアプリケーション全体を表すメインコンポーネントです。システムの仕組みと目的を視覚化する役割を担います。ここではパラメータをお好みの方法で解析し、サンプルインターフェースを自由に構築してください。
今回の例では、プロジェクトは次のように構成されています。
import React from "react";
import { useState} from 'react'
import { ThemeProvider, createTheme } from '@mui/material/styles'
import { orange, grey } from '@mui/material/colors'
const lightTheme = createTheme({
palette: {
primary: {
main: grey[400],
},
}
})
const darkTheme = createTheme({
palette: {
primary: {
main: orange[400],
},
}
})
export default function Showcase(parameters) {
const userSettings = parameters.userSettings
const [theme, setTheme] = useState("light")
const [greetingMessage, setGreetingMessage] = useState("Anonymous Person")
const items = []
if (userSettings) {
const obj = userSettings[0]
for (const key in userSettings) {
items.push(<li key={key}> {key}: {userSettings[key]} </li>)
}
if (userSettings["themePreference"] != theme) {
setTheme(userSettings["themePreference"] == "light" ? "light" : "dark")
}
if (!greetingMessage || userSettings["greetingMessage"] != greetingMessage) {
if (userSettings["greetingMessage"]) {
setGreetingMessage(userSettings["greetingMessage"])
}
else {
setGreetingMessage("not the same message")
}
}
}
return (
<div>
<div style={{
padding: 10,
margin: 10,
backgroundColor: theme == "light" ? "grey" : "orange",
border: "solid",
borderWidth: "30px",
borderColor: theme == "light" ? "#B2B2B2" : "black"
}}>
<ThemeProvider
theme={theme == "light" ? lightTheme : darkTheme}
>
<h2>Hi, {greetingMessage}!</h2>
<p>User and their preferences:</p>
{items}
</ThemeProvider>
</div>
</div>
);
}
ワークフロー
まだMetaMask経由で接続していないユーザー(デフォルトテンプレート)

初めてMetaMask経由で接続したユーザー(デフォルトテンプレート)

- ユーザーは、プラットフォームで挨拶されるときに表示したい名前を入力します。
- ライトテーマとダークテーマのどちらかを選択します。
ユーザープリファレンスの設定完了。以降、プリファレンスインターフェースをサポートする任意のプラットフォームで同じUIを表示可能(ユーザーカスタマイズ済みテンプレート)

コンポーネントは、ユーザーが設定したプリファレンスをテキスト形式で表示します。解析方法は自由に設計できます。
このシンプルな例からわかるように、ユーザーに表示するあらゆるコンポーネントをパラメータ化できます。デザインの改善やパラメータの追加によってプロジェクトを拡張することも可能ですが、根本的なロジックは常に同じです。
まとめ
プロジェクトのデモを確認するには、こちらをご覧ください。
完成したプロジェクトは、GitHubリポジトリでご確認いただけます。
リポジトリにはVercelへのクイックデプロイボタンが用意されており、ワンクリックでデプロイすると、Upstash Redisとの統合が自動的に作成されます。
皆さんのアイデアやフィードバックをお待ちしています。TwitterまたはDiscordからお気軽にお問い合わせください。
-
Upstash Redisで実現するNetlify Graphのグローバルキャッシュ
はじめに先日、Netlifyは「Netlify Graph」という新機能を発表しました。同僚が以前から指摘していた不足していたピースに対して、Netlifyがソリューションへ向けて良い一歩を踏み出した形です。Netlify Graphは、開発者がWebアプリ向けのGraphQL API呼び出しを構築するのを支援する機能です。Netlifyダッシュボード上でGraphQLリクエストを準備すれば、ワンクリックでクライアントコードをプロジェクトに注入できます。Netlify Functionsとサードパーティサービスの課題Netlify Functionsをサードパーティサービスと組み合わせて使う場
-
Next.jsアプリに最適なデータベースはどれ?主要6サービスを徹底比較
Next.jsは、サーバーサイドレンダリング(SSR)機能により、開発者がフルスタックアプリケーションを構築できる強力なフレームワークです。さらにVercelやNetlifyといったプラットフォームを使えば、サーバーレス関数でバックエンドAPIも手軽に実装できます。では次に考えるべきは「Next.jsアプリに最適なデータベースは何か」という問題です。 この記事では、Next.jsコミュニティで人気の高いデータベースサービスを取り上げ、それぞれがサーバーレスモデルにどれだけ適合しているかを解説します。評価ポイントは主に以下の2つです。 サーバーレス対応の料金体系:従量課金制か?データベースを使