Skip to content

Commit

Permalink
refactor: onboarding (#105)
Browse files Browse the repository at this point in the history
* refactor: onboarding

* fix: remove welcome screen, allow copying recovery kit

* fix: misc teaks to onboarding

* updated: copy on the invite onboarding

* updated: new device setup copy

* fix: typo in graphene type resolvers

* fix: allow copying recovery kit on invite screen to allow account creation

* feat: misc copy changes

---------

Co-authored-by: Nimish <[email protected]>
  • Loading branch information
rohan-chaturvedi and nimish-ks authored Nov 9, 2023
1 parent ec7f04d commit 5cbc42d
Show file tree
Hide file tree
Showing 19 changed files with 517 additions and 428 deletions.
2 changes: 1 addition & 1 deletion backend/backend/graphene/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def resolve_recovery(self, info):
user=info.context.user, organisation=self, deleted_at=None)
return org_member.wrapped_recovery

def resolve_idenity_key(self, info):
def resolve_identity_key(self, info):
org_member = OrganisationMember.objects.get(
user=info.context.user, organisation=self, deleted_at=None)
return org_member.identity_key
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/[team]/newdevice/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function NewDevice({ params }: { params: { team: string } }) {
icon: <MdOutlineKey />,
title: 'Sudo password',
description:
"Please set up a strong 'sudo' password to continue. This will be used to to perform administrative tasks and to encrypt keys locally on this device.",
"Please set up a strong 'sudo' password to continue. This will be used to encrypt keys and perform administrative tasks.",
},
])

Expand Down Expand Up @@ -74,7 +74,7 @@ export default function NewDevice({ params }: { params: { team: string } }) {
icon: <MdOutlineKey />,
title: 'Sudo password',
description:
"Please set up a strong 'sudo' password to continue. This will be used to to perform administrative tasks and to encrypt keys locally on this device.",
"Please set up a strong 'sudo' password to continue. This will be used to encrypt keys and perform administrative tasks.",
},
])

Expand Down
46 changes: 39 additions & 7 deletions frontend/app/[team]/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { Alert } from '@/components/common/Alert'
import { Avatar } from '@/components/common/Avatar'
import { Button } from '@/components/common/Button'
import { ModeToggle } from '@/components/common/ModeToggle'
import { AccountSeedGen } from '@/components/onboarding/AccountSeedGen'
import { AccountRecovery } from '@/components/onboarding/AccountRecovery'
import { RoleLabel } from '@/components/users/RoleLabel'
import { organisationContext } from '@/contexts/organisationContext'
import { cryptoUtils } from '@/utils/auth'
import { copyRecoveryKit, generateRecoveryPdf } from '@/utils/recovery'
import { Dialog, Transition } from '@headlessui/react'
import { useSession } from 'next-auth/react'
import { Fragment, useContext, useState } from 'react'
import { FaEye, FaEyeSlash, FaMoon, FaSun, FaTimes } from 'react-icons/fa'
import { toast } from 'react-toastify'

const ViewRecoveryDialog = () => {
const { activeOrganisation } = useContext(organisationContext)
Expand Down Expand Up @@ -49,22 +51,46 @@ const ViewRecoveryDialog = () => {
setIsOpen(true)
}

const handleDownloadRecoveryKit = async () => {
toast.promise(
generateRecoveryPdf(
recovery,
session?.user?.email!,
activeOrganisation!.name,
session?.user?.name || undefined
),
{
pending: 'Generating recovery kit',
success: 'Downloaded recovery kit',
}
)
}

const handleCopyRecoveryKit = () => {
copyRecoveryKit(
recovery,
session?.user?.email!,
activeOrganisation!.name,
session?.user?.name || undefined
)
}

return (
<>
<div className="flex flex-col gap-4">
<Alert variant="info" icon={true}>
<div className="flex flex-col gap-2">
<p>Your recovery phrase is encrypted.</p>
<p>Your account keys are encrypted.</p>

<p>
Backup your account recovery phrase in a safe place if you haven&apos;t already. If
you forget your sudo password, it is the only way to restore your accout keys.
Store your account recovery kit in a safe place if you haven&apos;t already. If you
forget your sudo password, it is the only way to restore your accout keys.
</p>
</div>
</Alert>
<div>
<Button variant="primary" onClick={openModal} title="View recovery">
<FaEye /> View recovery
<FaEye /> View recovery info
</Button>
</div>
</div>
Expand Down Expand Up @@ -106,7 +132,13 @@ const ViewRecoveryDialog = () => {
</Dialog.Title>

<div className="py-4">
{recovery && <AccountSeedGen mnemonic={recovery} />}
{recovery && (
<AccountRecovery
mnemonic={recovery}
onDownload={handleDownloadRecoveryKit}
onCopy={handleCopyRecoveryKit}
/>
)}

{!recovery && (
<form onSubmit={handleDecryptRecovery}>
Expand Down Expand Up @@ -195,7 +227,7 @@ export default function Settings({ params }: { params: { team: string } }) {
</div>

<div className="flex flex-col gap-4">
<div className="text-lg font-medium">Recovery phrase</div>
<div className="text-lg font-medium">Recovery</div>
<ViewRecoveryDialog />
</div>

Expand Down
116 changes: 59 additions & 57 deletions frontend/app/invite/[invite]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
import { cryptoUtils } from '@/utils/auth'
import VerifyInvite from '@/graphql/queries/organisation/validateOrganisationInvite.gql'
import AcceptOrganisationInvite from '@/graphql/mutations/organisation/acceptInvite.gql'
import { useLazyQuery, useMutation, useQuery } from '@apollo/client'
import { useLazyQuery, useMutation } from '@apollo/client'
import { HeroPattern } from '@/components/common/HeroPattern'
import { Button } from '@/components/common/Button'
import { FaArrowRight } from 'react-icons/fa'
import Loading from '@/app/loading'
import { useEffect, useState } from 'react'
import { Step, Stepper } from '@/components/onboarding/Stepper'
import { AccountPassword } from '@/components/onboarding/AccountPassword'
import { AccountSeedChecker } from '@/components/onboarding/AccountSeedChecker'
import { AccountSeedGen } from '@/components/onboarding/AccountSeedGen'
import { MdKey, MdOutlineVerifiedUser, MdOutlinePassword } from 'react-icons/md'
import { AccountRecovery } from '@/components/onboarding/AccountRecovery'
import { MdKey, MdOutlinePassword } from 'react-icons/md'
import { toast } from 'react-toastify'
import { OrganisationMemberInviteType } from '@/apollo/graphql'
import { useSession } from 'next-auth/react'
import { setLocalKeyring } from '@/utils/localStorage'
import { Logo } from '@/components/common/Logo'
import { copyRecoveryKit, generateRecoveryPdf } from '@/utils/recovery'

const bip39 = require('bip39')

Expand Down Expand Up @@ -49,9 +49,9 @@ export default function Invite({ params }: { params: { invite: string } }) {

const [showWelcome, setShowWelcome] = useState<boolean>(true)
const [step, setStep] = useState<number>(0)
const [recoverySkipped, setRecoverySkipped] = useState<boolean>(false)

const [recoveryDownloaded, setRecoveryDownloaded] = useState<boolean>(false)
const [success, setSuccess] = useState<boolean>(false)
const [inputs, setInputs] = useState<Array<string>>([])
const [pw, setPw] = useState<string>('')
const [pw2, setPw2] = useState<string>('')
const [mnemonic, setMnemonic] = useState('')
Expand All @@ -73,26 +73,19 @@ export default function Invite({ params }: { params: { invite: string } }) {
const steps: Step[] = [
{
index: 0,
name: 'Set up recovery phrase',
icon: <MdKey />,
title: 'Recovery',
name: 'Sudo Password',
icon: <MdOutlinePassword />,
title: 'Set a sudo password',
description:
"This is your 24 word recovery phrase. You can use it log in to your Phase account if you forget the sudo password. It's used to derive your encryption keys. Only you have access to it. Please write it down or store it somewhere safe like a password manager.",
'This will be used to encrypt your account keys. You will be need to enter this password to perform administrative tasks.',
},
{
index: 1,
name: 'Verify recovery phrase',
icon: <MdOutlineVerifiedUser />,
title: 'Verify recovery phrase',
description: 'Please enter the your recovery phrase in the correct order below.',
},
{
index: 2,
name: 'Sudo password',
icon: <MdOutlinePassword />,
title: 'Set a sudo password',
name: 'Account recovery',
icon: <MdKey />,
title: 'Account Recovery',
description:
'Please set up a strong sudo password to continue. This will be used to to perform administrative tasks and to secure your account keys.',
'If you forget your sudo password, you will need to use a recovery kit to regain access to your account.',
},
]

Expand Down Expand Up @@ -160,23 +153,15 @@ export default function Invite({ params }: { params: { invite: string } }) {
})
}

const handleInputUpdate = (newValue: string, index: number) => {
if (newValue.split(' ').length === 24) {
setInputs(newValue.split(' '))
} else setInputs(inputs.map((input: string, i: number) => (index === i ? newValue : input)))
}

const validateCurrentStep = () => {
if (step === 1 && !recoverySkipped) {
if (inputs.join(' ') !== mnemonic && !recoverySkipped) {
errorToast('Incorrect account recovery key!')
return false
}
} else if (step === 2) {
if (step === 0) {
if (pw !== pw2) {
errorToast("Passwords don't match")
return false
}
} else if (step === 1 && !recoveryDownloaded) {
errorToast('Please download the your account recovery kit!')
return false
}
return true
}
Expand All @@ -198,20 +183,11 @@ export default function Invite({ params }: { params: { invite: string } }) {
if (step !== 0) setStep(step - 1)
}

const skipRecoverySteps = () => {
setRecoverySkipped(true)
setStep(2)
}

useEffect(() => {
setMnemonic(bip39.generateMnemonic(256))
const id = crypto.randomUUID()
}, [])

useEffect(() => {
setInputs([...Array(mnemonic.split(' ').length)].map(() => ''))
}, [mnemonic])

const WelcomePane = () => (
<div className="mx-auto my-auto max-w-2xl space-y-8 p-16 bg-zinc-100 dark:bg-zinc-800 text-black dark:text-white rounded-md shadow-2xl text-center">
<div className="space-y-2">
Expand Down Expand Up @@ -242,8 +218,8 @@ export default function Invite({ params }: { params: { invite: string } }) {
return (
<div className="mx-auto my-auto max-w-2xl space-y-8 p-16 bg-zinc-100 dark:bg-zinc-800 text-black dark:text-white rounded-md shadow-2xl text-center">
<div className="flex flex-col gap-y-2 items-center">
<h1 className="text-4xl text-black dark:text-white text-center font-bold">Success!</h1>
<p className="text-black/30 dark:text-white/40 text-center">Your account is setup!</p>
<h1 className="text-4xl text-black dark:text-white text-center font-bold">You're All Set</h1>
<p className="text-black/30 dark:text-white/40 text-center">Your account is ready to go!</p>
<div className="mx-auto pt-8">
<Button
variant="primary"
Expand All @@ -258,6 +234,33 @@ export default function Invite({ params }: { params: { invite: string } }) {
)
}

const handleDownloadRecoveryKit = async () => {
toast
.promise(
generateRecoveryPdf(
mnemonic,
session?.user?.email!,
invite.organisation.name,
session?.user?.name || undefined
),
{
pending: 'Generating recovery kit',
success: 'Downloaded recovery kit',
}
)
.then(() => setRecoveryDownloaded(true))
}

const handleCopyRecoveryKit = () => {
copyRecoveryKit(
mnemonic,
session?.user?.email!,
invite.organisation.name,
session?.user?.name || undefined
)
setRecoveryDownloaded(true)
}

return (
<>
<div>
Expand All @@ -280,16 +283,15 @@ export default function Invite({ params }: { params: { invite: string } }) {
<Stepper steps={steps} activeStep={step} />
</div>

{step === 0 && <AccountSeedGen mnemonic={mnemonic} />}
{step === 0 && <AccountPassword pw={pw} setPw={setPw} pw2={pw2} setPw2={setPw2} />}

{step === 1 && (
<AccountSeedChecker
<AccountRecovery
mnemonic={mnemonic}
inputs={inputs}
updateInputs={handleInputUpdate}
required={!recoverySkipped}
onDownload={handleDownloadRecoveryKit}
onCopy={handleCopyRecoveryKit}
/>
)}
{step === 2 && <AccountPassword pw={pw} setPw={setPw} pw2={pw2} setPw2={setPw2} />}

<div className="flex justify-between w-full">
<div>
Expand All @@ -300,13 +302,13 @@ export default function Invite({ params }: { params: { invite: string } }) {
)}
</div>
<div className="flex items-center gap-2">
{step !== 2 && (
<Button variant="secondary" type="button" onClick={skipRecoverySteps}>
Skip
</Button>
)}
<Button variant="primary" type="submit" isLoading={isloading}>
Next
<Button
variant="primary"
type="submit"
isLoading={isloading || loading}
disabled={step === steps.length - 1 && !recoveryDownloaded}
>
{step === steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
Expand Down
Loading

0 comments on commit 5cbc42d

Please sign in to comment.