This repository has been archived by the owner on Jan 8, 2025. It is now read-only.
generated from scaffold-eth/scaffold-eth-2
-
Notifications
You must be signed in to change notification settings - Fork 34
add a password manager app demo #8
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
"use client"; | ||
|
||
import { useEffect, useState } from "react"; | ||
import type * as NillionTypes from "@nillion/nillion-client-js-browser/nillion_client_js_browser.d.ts"; | ||
import type { NextPage } from "next"; | ||
import { useAccount } from "wagmi"; | ||
import { CopyString } from "~~/components/nillion/CopyString"; | ||
import Dropdown from "~~/components/nillion/Dropdown"; | ||
import { NillionOnboarding } from "~~/components/nillion/NillionOnboarding"; | ||
import RetrieveSecretCommand from "~~/components/nillion/RetrieveSecretCommand"; | ||
import SecretForm from "~~/components/nillion/SecretForm"; | ||
import { Address } from "~~/components/scaffold-eth"; | ||
import { getUserKeyFromSnap } from "~~/utils/nillion/getUserKeyFromSnap"; | ||
import { retrieveSecretBlob } from "~~/utils/nillion/retrieveSecretBlob"; | ||
import { storeSecretsBlob } from "~~/utils/nillion/storeSecretsBlob"; | ||
|
||
interface StoredSecrets { | ||
[secretName: string]: string; // store_id | ||
} | ||
const Home: NextPage = () => { | ||
const { address: connectedAddress } = useAccount(); | ||
const [connectedToSnap, setConnectedToSnap] = useState<boolean>(false); | ||
const [userKey, setUserKey] = useState<string | null>(null); | ||
const [userId, setUserId] = useState<string | null>(null); | ||
const [nillion, setNillion] = useState<any>(null); | ||
const [nillionClient, setNillionClient] = useState<any>(null); | ||
const [selectedSecretName, setSelectedSecretName] = useState<string>(""); | ||
const [selectedStoreId, setSelectedStoreId] = useState<string | null>(null); | ||
const [latestSecretName, setLatestSecretName] = useState<string | null>(null); | ||
const [storedSecrets, setStoredSecrets] = useState<StoredSecrets>({}); | ||
const [retrievedValue, setRetrievedValue] = useState<string | null>(null); | ||
|
||
async function handleConnectToSnap() { | ||
const snapResponse = await getUserKeyFromSnap(); | ||
setUserKey(snapResponse?.user_key || null); | ||
setConnectedToSnap(snapResponse?.connectedToSnap || false); | ||
} | ||
|
||
useEffect(() => { | ||
if (userKey) { | ||
const getNillionClientLibrary = async () => { | ||
const nillionClientUtil = await import("~~/utils/nillion/nillionClient"); | ||
const libraries = await nillionClientUtil.getNillionClient(userKey); | ||
setNillion(libraries.nillion); | ||
setNillionClient(libraries.nillionClient); | ||
return libraries.nillionClient; | ||
}; | ||
|
||
getNillionClientLibrary().then(nillionClient => { | ||
const user_id = nillionClient.user_id; | ||
setUserId(user_id); | ||
}); | ||
} | ||
}, [userKey]); | ||
|
||
async function handleSecretFormSubmit( | ||
secretName: string, | ||
secretValue: string, | ||
permissionedUserIdForRetrieveSecret: string | null, | ||
permissionedUserIdForUpdateSecret: string | null, | ||
permissionedUserIdForDeleteSecret: string | null, | ||
) { | ||
await storeSecretsBlob( | ||
nillion, | ||
nillionClient, | ||
[{ name: secretName, value: secretValue }], | ||
permissionedUserIdForRetrieveSecret ? [permissionedUserIdForRetrieveSecret] : [], | ||
permissionedUserIdForUpdateSecret ? [permissionedUserIdForUpdateSecret] : [], | ||
permissionedUserIdForDeleteSecret ? [permissionedUserIdForDeleteSecret] : [], | ||
).then((store_id: string) => { | ||
setStoredSecrets(prevSecrets => ({ | ||
...prevSecrets, | ||
[secretName]: store_id, | ||
})); | ||
|
||
setLatestSecretName(secretName); | ||
}); | ||
} | ||
|
||
async function handleRetrieveSecretBlob(store_id: string, secret_name: string) { | ||
await retrieveSecretBlob(nillionClient, store_id, secret_name).then(setRetrievedValue); | ||
} | ||
|
||
const handleSecretDropdownSelection = (secretName: string) => { | ||
setSelectedSecretName(secretName); | ||
setSelectedStoreId(storedSecrets[secretName]); | ||
}; | ||
|
||
const resetNillion = () => { | ||
setConnectedToSnap(false); | ||
setUserKey(null); | ||
setUserId(null); | ||
setNillion(null); | ||
setNillionClient(null); | ||
}; | ||
|
||
const resetForm = () => { | ||
setLatestSecretName(null); | ||
setRetrievedValue(null); | ||
}; | ||
|
||
useEffect(() => { | ||
if (!connectedAddress) { | ||
resetNillion(); | ||
} | ||
}, [connectedAddress]); | ||
|
||
return ( | ||
<> | ||
<div className="flex items-center flex-col pt-10"> | ||
<div className="px-5 flex flex-col"> | ||
<h1 className="text-xl"> | ||
<span className="block text-4xl font-bold text-center">Nillion Password Manager</span> | ||
|
||
{!connectedAddress && <p>Connect your MetaMask Flask wallet</p>} | ||
{connectedAddress && connectedToSnap && !userKey && ( | ||
<a target="_blank" href="https://nillion-snap-site.vercel.app/" rel="noopener noreferrer"> | ||
<button className="btn btn-sm btn-primary mt-4"> | ||
No Nillion User Key - Generate and store user key here | ||
</button> | ||
</a> | ||
)} | ||
</h1> | ||
|
||
{connectedAddress && ( | ||
<div className="flex justify-center items-center space-x-2"> | ||
<p className="my-2 font-medium">Connected Wallet Address:</p> | ||
<Address address={connectedAddress} /> | ||
</div> | ||
)} | ||
|
||
{connectedAddress && !connectedToSnap && ( | ||
<button className="btn btn-sm btn-primary mt-4" onClick={handleConnectToSnap}> | ||
Connect to Snap with your Nillion User Key | ||
</button> | ||
)} | ||
|
||
{connectedToSnap && ( | ||
<div> | ||
{userKey && ( | ||
<div> | ||
<div className="flex justify-center items-center space-x-2"> | ||
<p className="my-2 font-medium"> | ||
🤫 Nillion User Key from{" "} | ||
<a target="_blank" href="https://nillion-snap-site.vercel.app/" rel="noopener noreferrer"> | ||
MetaMask Flask | ||
</a> | ||
: | ||
</p> | ||
|
||
<CopyString str={userKey} /> | ||
</div> | ||
|
||
{userId && ( | ||
<div className="flex justify-center items-center space-x-2"> | ||
<p className="my-2 font-medium">Connected as Nillion User ID:</p> | ||
<CopyString str={userId} /> | ||
</div> | ||
)} | ||
</div> | ||
)} | ||
</div> | ||
)} | ||
</div> | ||
|
||
<div className="flex-grow bg-base-300 w-full mt-16 px-8 py-12"> | ||
<div className="flex justify-center items-center gap-12 flex-col sm:flex-row"> | ||
{!connectedToSnap ? ( | ||
<NillionOnboarding /> | ||
) : ( | ||
<div className="flex flex-row justify-between"> | ||
{/* Store secret blob */} | ||
<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center w-full rounded-3xl my-2 justify-between mx-5"> | ||
<h1 className="text-xl">Store a new password</h1> | ||
<div className="flex flex-row w-full justify-between items-center my-10 mx-10"> | ||
<div className="flex-1 px-2"> | ||
{latestSecretName ? ( | ||
<> | ||
<RetrieveSecretCommand | ||
secretType="SecretBlob" | ||
userKey={userKey} | ||
storeId={storedSecrets[latestSecretName]} | ||
secretName={latestSecretName || ""} | ||
/> | ||
<button className="btn btn-sm btn-primary mt-4" onClick={resetForm}> | ||
Add another password | ||
</button> | ||
</> | ||
) : ( | ||
<SecretForm | ||
secretName={""} | ||
onSubmit={handleSecretFormSubmit} | ||
secretType="text" | ||
isLoading={false} | ||
// use customSecretName boolean prop to signal that the form should set the secret name | ||
customSecretName | ||
hidePermissions | ||
itemName="password" | ||
/> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
|
||
{/* Retrieve secret blob */} | ||
|
||
<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center w-full rounded-3xl my-2 justify-between mx-5"> | ||
<h1 className="text-xl">Retrieve passwords from Nillion</h1> | ||
<div className="flex flex-row w-full justify-between items-center my-10 mx-10"> | ||
<div className="flex-1 px-2 flex-col"> | ||
<div> | ||
<Dropdown | ||
options={Object.keys(storedSecrets).map(s => ({ value: s, label: s }))} | ||
onDropdownUpdate={selectedName => handleSecretDropdownSelection(selectedName)} | ||
itemName="a password" | ||
disabled={Object.keys(storedSecrets).length === 0} | ||
/> | ||
</div> | ||
|
||
<button | ||
className="btn btn-sm btn-primary mt-4" | ||
onClick={() => handleRetrieveSecretBlob(selectedStoreId || "", selectedSecretName)} | ||
disabled={!selectedStoreId} | ||
> | ||
Retrieve and decode {selectedSecretName} | ||
</button> | ||
|
||
{retrievedValue && <p>✅ Retrieved value: {retrievedValue}</p>} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
}; | ||
|
||
export default Home; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import React, { useState } from "react"; | ||
|
||
interface DropdownOption { | ||
value: string; | ||
label: string; | ||
} | ||
|
||
interface DropdownProps { | ||
options: DropdownOption[]; | ||
onDropdownUpdate: (value: string) => void; | ||
disabled?: boolean; | ||
itemName?: string; | ||
} | ||
|
||
const Dropdown: React.FC<DropdownProps> = ({ options, onDropdownUpdate, itemName, disabled = false }) => { | ||
const [selectedOption, setSelectedOption] = useState<string | null>(null); | ||
|
||
const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => { | ||
const selectedValue = e.target.value; | ||
setSelectedOption(selectedValue); | ||
onDropdownUpdate(selectedValue); | ||
}; | ||
|
||
return ( | ||
<select value={selectedOption || ""} onChange={e => handleSelect(e)} disabled={disabled}> | ||
<option value="">Select {itemName} name</option> | ||
{options.map((option, index) => ( | ||
<option key={index} value={option.value}> | ||
{option.label} | ||
</option> | ||
))} | ||
</select> | ||
); | ||
}; | ||
|
||
export default Dropdown; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do TS types for our wasm types not work here? e.g. I imagine you should be able to use
NillionClient
rather thanany
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
caught between a rock and a hard place here
I can't get the nillion types to replace
any
withoutcausing client errors: if the
import * as nillion from "@nillion/nillion-client-js-browser/nillion_client_js_browser.js";
happens without being async imported, the client complains and errors outcreating a circular dependency: nillion js lib is imported in the client util https://github.com/NillionNetwork/scaffold-nillion/blob/main/packages/nextjs/utils/nillion/nillionClient.ts#L2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm this isn't great. Those type definitions are being generated so we should make sure they're usable and find a way to use them. I think it's okay to keep it this now until we figure that out tho. cc @wwwehr @navasvarela you may know how to make this work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could the types potentially be a separate import from the js client library?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@oceans404 There is a TS type descriptor file generated as part of the client's npm,
nillion_client_js_browser.d.ts
. This file contains all the type definitions ONLY. I'm not a TS programmer myself but I'd imagine you can import this file?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@navasvarela thanks Juan, this worked
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@navasvarela I noticed there's no nillion type here - just NillionClient