Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wallet wizard poc (wip) #1530

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
52 changes: 52 additions & 0 deletions components/wallet-fields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ClientInput, PasswordInput, Checkbox, Select } from '@/components/form'
import Info from '@/components/info'
import { useIsClient } from '@/components/use-client'
import Text from '@/components/text'

export default function WalletFields ({ wallet: { config, fields, isConfigured } }) {
const isClient = useIsClient()

return fields
.map(({ name, label = '', type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => {
const rawProps = {
...props,
name,
initialValue: config?.[name],
readOnly: isClient && isConfigured && editable === false && !!config?.[name],
groupClassName: props.hidden ? 'd-none' : undefined,
label: label
? (
<div className='d-flex align-items-center'>
{label}
{/* help can be a string or object to customize the label */}
{help && (
<Info label={help.label}>
<Text>{help.text || help}</Text>
</Info>
)}
{optional && (
<small className='text-muted ms-2'>
{typeof optional === 'boolean' ? 'optional' : <Text>{optional}</Text>}
</small>
)}
</div>
)
: undefined,
required: !optional,
autoFocus: i === 0
}
if (type === 'text') {
return <ClientInput key={i} {...rawProps} />
}
if (type === 'password') {
return <PasswordInput key={i} {...rawProps} newPass />
}
if (type === 'checkbox') {
return <Checkbox key={i} {...rawProps} />
}
if (type === 'select') {
return <Select key={i} {...rawProps} />
}
return null
})
}
3 changes: 2 additions & 1 deletion pages/settings/wallets/[wallet].js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import { Form, ClientInput, PasswordInput, CheckboxGroup, Checkbox } from '@/components/form'
import { Form, CheckboxGroup, Checkbox } from '@/components/form'
import { CenterLayout } from '@/components/layout'
import { WalletSecurityBanner } from '@/components/banners'
import { WalletLogs } from '@/components/wallet-logger'
import { useToast } from '@/components/toast'
import { useRouter } from 'next/router'
import WalletFields from '@/components/wallet-fields'
import { useWallet } from '@/wallets/index'
import Info from '@/components/info'
import Text from '@/components/text'
Expand Down
172 changes: 172 additions & 0 deletions pages/settings/wallets/wizards/[wizard].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import Layout from '@/components/layout'
import { useToast } from '@/components/toast'
import { useRouter } from 'next/router'
import Text from '@/components/text'
import WalletFields from '@/components/wallet-fields'
import wizards from '@/wallets/wizards'
import { useState, useEffect, useRef } from 'react'
import { Form, SubmitButton } from '@/components/form'
import styles from '@/components/comment.module.css'
import { Button } from 'react-bootstrap'
import { useWallets } from 'wallets'

export const getServerSideProps = getGetServerSideProps({ query: null })

const StepComponent = ({ index, title, name, completed }) => {
return (
<div className='fw-bold d-flex align-items-center ml-8'>
<div
className={`rounded-circle text-black ${completed ? 'bg-secondary' : 'bg-light'}
align-items-center justify-content-center d-flex
`} style={{ width: '1.4rem', height: '1.4rem' }}
>
<span>{index + 1}</span>
</div>
<div className='ps-2 pe-4'>{title ?? name ?? 'Step ' + (index + 1)}</div>
</div>
)
}

export default function WalletWizard () {
const router = useRouter()
const { wizard: name } = router.query
const wizard = wizards[name]
const stepsData = useRef({})

const { wallets } = useWallets()
wallets.connect = async (name, values, label) => {
values.enabled = true
console.log(values)
const wallet = wallets.find(w => w.name === name)
if (!wallet) {
throw new Error(`Wallet ${name} not found`)
}
await wallet.save(values)
}

const [currentStep, setCurrentStep] = useState(null)
const [currentStepData, setCurrentStepData] = useState({})
const [currentStepIndex, setCurrentStepIndex] = useState(0)
const [completedSteps, setCompletedSteps] = useState([])

const [isLastStep, setIsLastStep] = useState(false)
const stepTitle = currentStep?.title ?? currentStep?.name ?? 'Step ' + (currentStepIndex + 1)

const setStep = async (index) => {
let step = wizard.getStep ? await wizard.getStep(index, stepsData.current, wallets) : wizard.steps[index]
if (typeof step === 'function') {
step = await step(stepsData.current, wallets)
}
setCurrentStep(step)
let currentStepData = stepsData.current[step.name]
if (!currentStepData) {
currentStepData = {}
stepsData.current[step.name] = currentStepData
}
setCurrentStepData(currentStepData)
return step
}

const nextStep = async () => {
if (isLastStep) throw new Error('Cannot go forward from last step')
const index = currentStepIndex + 1
setCompletedSteps([...completedSteps, stepTitle])
setCurrentStepIndex(index)
const step = await setStep(index)
if (step.last != null) {
setIsLastStep(step.last)
} else if (wizard.isLastStep) {
setIsLastStep(await wizard.isLastStep(index, stepsData.current, wallets))
} else {
setIsLastStep(wizard.steps && index === wizard.steps.length - 1)
}
}

const prevStep = async () => {
if (currentStepIndex === 0) throw new Error('Cannot go back from first step')
setIsLastStep(false)
setCompletedSteps(completedSteps.slice(0, -1))
const index = currentStepIndex - 1
setCurrentStepIndex(index)
await setStep(index)
}

useEffect(() => {
setCompletedSteps([])
setIsLastStep(false)
setCurrentStepIndex(0)
setStep(0)
}, [])

const validateProps = currentStep && currentStep.fieldValidation
? (typeof currentStep.fieldValidation === 'function'
? { validate: currentStep.fieldValidation }
: { schema: currentStep.fieldValidation })
: {}

if (!currentStep) return <></>
return (
<Layout>
<div className='py-5 w-100'>

<h2 className='mb-2 text-center'>configure {wizard.title}</h2>

<h6 className='text-muted text-center mb-4'><Text>{wizard.description}</Text></h6>
<div className='pt-4 d-flex align-items-center text-muted small'>
{completedSteps.map((step, i) => (
<StepComponent key={i} index={i} name={step} completed />
)
)}

<StepComponent index={currentStepIndex} name={stepTitle} />
</div>
<Form
className='mt-4'
initial={currentStepData}
// {...validateProps}
onSubmit={async (values) => {
Object.assign(currentStepData, values)
nextStep()
console.log(stepsData.current)
window.scrollTo({ top: 0 })
}}
>
<div className={styles.text}>
<Text>{currentStep.description}</Text>
</div>
<div className='mt-4 mb-4'>
<WalletFields wallet={{
config: currentStepData,
fields: currentStep.fields.map(f => {
f.key = f.name ?? f.title
return f
})
}}
/>
</div>
<div className='mt-4 d-flex align-items-center ms-auto justify-content-end'>

{
!isLastStep && currentStepIndex > 0 && (
<Button className='me-4 text-muted nav-link fw-bold' variant='link' onClick={prevStep}>back</Button>

)
}
{
!isLastStep
? (

<SubmitButton variant='primary' className='mt-1 px-4'>next</SubmitButton>
)
: (
<Button className='btn btn-primary' onClick={(() => router.back())}>Ok</Button>
)

}
</div>
</Form>
</div>
</Layout>
)
}
112 changes: 112 additions & 0 deletions wallets/wizards/coinos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { nwcSchema, lnAddrSchema } from '@/lib/validate'

export const title = 'coinos'
export const authors = ['supratic']
export const icon = 'https://coinos.io/icons/logo.svg'
export const description = '[Coinos](https://coinos.io/) wallet'

const step1 = async (stepsData, wallets) => {
return {
name: 'SignUp',
title: 'Sign Up',
description: `
Create a Coinos account on https://Coinos.io/register, fyi... it's KYC free,
so no question asked on signup apart username and password.
You can have it fully anonymous, clicking on the 🎲 and it will generate a random username for you.
Make sure your password is secure.
`,
fields: []
}
}

const step2 = async (stepsData, wallets) => {
return {
name: 'NWC',
title: 'Get NWC',
description: `
Once you have your [email protected] account...
![image](https://imgprxy.stacker.news/R1tVUU8XIXA5s0rqoL6d3x4syJDnq4r_BXgdqvDGfyM/rs:fit:1920:1080/aHR0cHM6Ly9tLnN0YWNrZXIubmV3cy81MzMxMQ)
... expand the details.
![image](https://imgprxy.stacker.news/G9oqobqhSGEt37-sq7LStJqtJ880QSEYCEX8aD-sdig/rs:fit:1920:1080/aHR0cHM6Ly9tLnN0YWNrZXIubmV3cy81MzMxMw)
Then copy the Nostr Wallet Connect url and paste it below
`,
fields: [
{
name: 'nwcAddr',
label: 'Input the NWC url',
type: 'password',
placeholder: 'nostr+walletconnect://...',
autoComplete: 'off'
}
],
fieldValidation: nwcSchema
}
}

const step3 = async (stepsData, wallets) => {
return {
name: 'lnAddress',
title: 'Get LN Address',
description: `
Once you have your [email protected] account...
![image](https://imgprxy.stacker.news/R1tVUU8XIXA5s0rqoL6d3x4syJDnq4r_BXgdqvDGfyM/rs:fit:1920:1080/aHR0cHM6Ly9tLnN0YWNrZXIubmV3cy81MzMxMQ)
... expand the details.
![image](hhttps://imgprxy.stacker.news/jnrYBUi4dmYkO4SpDuJcc6Ww9D7GfIHTN_o8nuKvCn0/rs:fit:1600:900/aHR0cHM6Ly9tLnN0YWNrZXIubmV3cy81MzMxMg)
Then copy that lightning address and paste it below.
`,
fields: [
{
name: 'lnAddr',
label: 'Input the Lightning Address',
type: 'text',
placeholder: '[email protected]',
autoComplete: 'off'
}
],
fieldValidation: lnAddrSchema
}
}

const step4 = async (stepsData, wallets) => {
try {
console.log(stepsData)
const nwcUrl = stepsData.NWC.nwcAddr
const lnAddress = stepsData.lnAddress.lnAddr

await wallets.connect(
'nwc', // connector
{ // fields
nwcUrl
},
'coinos' // label
)
await wallets.connect(
'lightning-address', // connector
{ // fields
address: lnAddress
},
'coinos' // label
)
return {
name: 'final',
title: 'Connected!',
description: `
You have successfully connected your Coinos wallet!
`,
fields: []
}
} catch (e) {
return {
name: 'error',
title: 'Error',
description: `
There was an error connecting your Coinos wallet. Please try again.

Error: ${e.message}
`,
fields: []
}
}
}

export const steps = [step1, step2, step3, step4]
3 changes: 3 additions & 0 deletions wallets/wizards/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as coinos from './coinos'
import * as lnbits from './lnbits'
export default { coinos, lnbits }
Loading
Loading