Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 14 additions & 32 deletions examples/nextjs-core/components/profile-resolver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ interface ProfileResult {
textRecords: TextRecord[];
}

let justaNameInstance: Awaited<ReturnType<typeof JustaName.init>> | null = null;
let justaNameInstance: ReturnType<typeof JustaName.init> | null = null;

async function getJustaName() {
function getJustaName() {
if (!justaNameInstance) {
justaNameInstance = await JustaName.init({
justaNameInstance = JustaName.init({
networks: [
{
chainId: 84532,
providerUrl: `https://base-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`,
chainId: 1,
providerUrl: `https://eth-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`,
},
],
});
Expand All @@ -49,24 +49,20 @@ export function ProfileResolver() {
setProfile(null);

try {
const justaName = await getJustaName();
const justaName = getJustaName();

if (isAddress(trimmed)) {
const result = await justaName.subnames.reverseResolve({
address: trimmed as `0x${string}`,
chainId: 84532,
chainId: 1,
});

if (!result) {
setError("No ENS name found for this address.");
return;
}

const resultAny = result as unknown as Record<string, string>;
const name =
typeof result === "string"
? result
: resultAny.name ?? resultAny.ens ?? String(result);
const name = result;

const records = await justaName.subnames.getRecords({ ens: name });

Expand Down Expand Up @@ -186,29 +182,15 @@ export function ProfileResolver() {

function extractTextRecords(records: unknown): TextRecord[] {
if (!records || typeof records !== "object") return [];

const rec = records as Record<string, unknown>;

if (Array.isArray(rec.texts)) {
return (rec.texts as Array<Record<string, string>>)
if (
rec.records &&
typeof rec.records === "object" &&
Array.isArray((rec.records as Record<string, unknown>).texts)
) {
return ((rec.records as Record<string, unknown>).texts as Array<Record<string, string>>)
.filter((t) => t.key && t.value)
.map((t) => ({ key: t.key, value: t.value }));
}

if (rec.textRecords && typeof rec.textRecords === "object") {
return Object.entries(rec.textRecords as Record<string, string>)
.filter(([, v]) => v)
.map(([k, v]) => ({ key: k, value: v }));
}

if (rec.records && typeof rec.records === "object") {
const inner = rec.records as Record<string, unknown>;
if (inner.texts && typeof inner.texts === "object") {
return Object.entries(inner.texts as Record<string, string>)
.filter(([, v]) => v)
.map(([k, v]) => ({ key: k, value: v }));
}
}

return [];
}
17 changes: 15 additions & 2 deletions examples/nextjs-core/components/sign-in-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,31 @@ export function SignInButton() {
setError(null);
setIsConnecting(true);

// Clear any existing JAW session so wallet_connect goes through the
// unauthenticated path and shows the popup with the SIWE request.
// If skipped, a cached session returns cached capabilities (no SIWE data).
try { await jaw.provider.request({ method: "wallet_disconnect" }); } catch { /* ignore */ }

let nonce: string;
try {
const nonceRes = await fetch("/api/siwe/nonce");
const { nonce } = await nonceRes.json();
const data = await nonceRes.json();
nonce = data.nonce;
} catch {
setError("Failed to fetch nonce from server.");
setIsConnecting(false);
return;
}

try {
// wallet_connect with SIWE capability — must use wallet_connect, not eth_requestAccounts
const result = await jaw.provider.request({
method: "wallet_connect",
params: [{
capabilities: {
signInWithEthereum: {
nonce,
chainId: "0xaa36a7",
chainId: "0x14a34",
domain: window.location.host,
uri: window.location.origin,
statement: "Sign in to JAW SIWE Demo",
Expand Down
42 changes: 14 additions & 28 deletions examples/nextjs-wagmi/components/profile-resolver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ interface ProfileResult {
textRecords: TextRecord[];
}

let justaNameInstance: Awaited<ReturnType<typeof JustaName.init>> | null = null;
let justaNameInstance: ReturnType<typeof JustaName.init> | null = null;

async function getJustaName() {
function getJustaName() {
if (!justaNameInstance) {
justaNameInstance = await JustaName.init({
justaNameInstance = JustaName.init({
networks: [
{
chainId: 84532,
providerUrl: `https://base-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`,
chainId: 1,
providerUrl: `https://eth-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`,
},
],
});
Expand All @@ -49,20 +49,20 @@ export function ProfileResolver() {
setProfile(null);

try {
const justaName = await getJustaName();
const justaName = getJustaName();

if (isAddress(trimmed)) {
const result = await justaName.subnames.reverseResolve({
address: trimmed,
chainId: 84532,
chainId: 1,
});

if (!result) {
setError('No ENS name found for this address.');
return;
}

const name = typeof result === 'string' ? result : result.name ?? result.ens ?? String(result);
const name = result;

const records = await justaName.subnames.getRecords({
ens: name,
Expand Down Expand Up @@ -186,29 +186,15 @@ export function ProfileResolver() {

function extractTextRecords(records: unknown): TextRecord[] {
if (!records || typeof records !== 'object') return [];

const rec = records as Record<string, unknown>;

if (Array.isArray(rec.texts)) {
return (rec.texts as Array<Record<string, string>>)
if (
rec.records &&
typeof rec.records === 'object' &&
Array.isArray((rec.records as Record<string, unknown>).texts)
) {
return ((rec.records as Record<string, unknown>).texts as Array<Record<string, string>>)
.filter((t) => t.key && t.value)
.map((t) => ({ key: t.key, value: t.value }));
}

if (rec.textRecords && typeof rec.textRecords === 'object') {
return Object.entries(rec.textRecords as Record<string, string>)
.filter(([, v]) => v)
.map(([k, v]) => ({ key: k, value: v }));
}

if (rec.records && typeof rec.records === 'object') {
const inner = rec.records as Record<string, unknown>;
if (inner.texts && typeof inner.texts === 'object') {
return Object.entries(inner.texts as Record<string, string>)
.filter(([, v]) => v)
.map(([k, v]) => ({ key: k, value: v }));
}
}

return [];
}
124 changes: 66 additions & 58 deletions examples/nextjs-wagmi/components/sign-in-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,74 +6,82 @@ import { useConnect, useDisconnect } from "@jaw.id/wagmi";
import { config } from "@/lib/config";

export function SignInButton() {
const { address, isConnected } = useAccount();
const { isConnected } = useAccount();
const { mutate: connect, isPending: isConnecting } = useConnect();
const { mutate: disconnect, isPending: isDisconnecting } = useDisconnect();
const { mutate: disconnect, mutateAsync: disconnectAsync, isPending: isDisconnecting } = useDisconnect();
const [verifiedAddress, setVerifiedAddress] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

const handleSignIn = useCallback(() => {
const handleSignIn = useCallback(async () => {
setError(null);

fetch("/api/siwe/nonce")
.then((res) => res.json())
.then(({ nonce }) => {
connect(
{
connector: config.connectors[0],
capabilities: {
signInWithEthereum: {
nonce,
chainId: "0xaa36a7",
domain: window.location.host,
uri: window.location.origin,
statement: "Sign in to JAW SIWE Demo",
expirationTime: new Date(
Date.now() + 60 * 60 * 1000
).toISOString(),
},
},
},
{
onSuccess: async (data) => {
const siweResponse =
data.accounts[0].capabilities?.signInWithEthereum;
// Clear any existing JAW session so wallet_connect goes through the
// unauthenticated path and shows the popup with the SIWE request.
// If skipped, a cached session returns cached capabilities (no SIWE data).
if (isConnected) {
try { await disconnectAsync({}); } catch { /* ignore */ }
}

let nonce: string;
try {
const res = await fetch("/api/siwe/nonce");
const data = await res.json();
nonce = data.nonce;
} catch {
setError("Failed to fetch nonce from server.");
return;
}

if (siweResponse && "message" in siweResponse) {
try {
const verifyRes = await fetch("/api/siwe/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: siweResponse.message,
signature: siweResponse.signature,
}),
});
connect(
{
connector: config.connectors[0],
capabilities: {
signInWithEthereum: {
nonce,
chainId: "0x14a34",
domain: window.location.host,
uri: window.location.origin,
statement: "Sign in to JAW SIWE Demo",
expirationTime: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
},
},
},
{
onSuccess: async (data) => {
const siweResponse =
data.accounts[0].capabilities?.signInWithEthereum;

if (!verifyRes.ok) {
setError("Server rejected the signature.");
return;
}
if (siweResponse && "message" in siweResponse) {
try {
const verifyRes = await fetch("/api/siwe/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: siweResponse.message,
signature: siweResponse.signature,
}),
});

const { address: verified } = await verifyRes.json();
setVerifiedAddress(verified);
} catch {
setError("Verification request failed.");
}
} else {
setError("SIWE response missing from wallet.");
if (!verifyRes.ok) {
setError("Server rejected the signature.");
return;
}
},
onError: (err) => {
setError(err.message || "Connection failed.");
},

const { address: verified } = await verifyRes.json();
setVerifiedAddress(verified);
} catch {
setError("Verification request failed.");
}
} else {
setError("SIWE response missing from wallet.");
}
);
})
.catch(() => {
setError("Failed to fetch nonce from server.");
});
}, [connect]);
},
onError: (err) => {
setError(err.message || "Connection failed.");
},
}
);
}, [connect, disconnectAsync, isConnected]);

const handleSignOut = useCallback(() => {
fetch("/api/siwe/logout", { method: "POST" }).then(() => {
Expand All @@ -83,7 +91,7 @@ export function SignInButton() {
});
}, [disconnect]);

if (isConnected && verifiedAddress) {
if (verifiedAddress) {
return (
<div className="flex flex-col gap-4">
<div className="rounded-lg border border-green-800/50 bg-green-950/30 px-4 py-3">
Expand Down
Loading