mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-21 00:30:31 +02:00
added apis; added widgets
This commit is contained in:
parent
550baf1b66
commit
55fddaf0d9
@ -16,6 +16,7 @@
|
||||
"openpgp": "^5.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-router-dom": "^6.4.2",
|
||||
"react-use": "^17.4.0",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
@ -25,6 +26,7 @@
|
||||
"@types/openpgp": "^4.4.18",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-icons": "^3.0.0",
|
||||
"@types/react-router": "^5.1.19",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
|
9
src/apis/check-is-domain-disposable.ts
Normal file
9
src/apis/check-is-domain-disposable.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import axios from "axios"
|
||||
|
||||
export default async function checkIsDomainDisposable(
|
||||
domain: string,
|
||||
): Promise<boolean> {
|
||||
const {data} = await axios.get(`https://api.mailcheck.ai/domain/${domain}`)
|
||||
|
||||
return !data.mx || data.disposable
|
||||
}
|
8
src/apis/get-server-settings.ts
Normal file
8
src/apis/get-server-settings.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import axios from "axios"
|
||||
import {ServerSettings} from "~/types";
|
||||
|
||||
export default async function getServerSettings(): Promise<ServerSettings> {
|
||||
return (
|
||||
await axios.get(`${process.env.NEXT_PUBLIC_SERVER_BASE_URL}/settings`)
|
||||
).data
|
||||
}
|
6
src/apis/index.ts
Normal file
6
src/apis/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./check-is-domain-disposable"
|
||||
export {default as checkIsDomainDisposable} from "./check-is-domain-disposable"
|
||||
export * from "./get-server-settings"
|
||||
export {default as getServerSettings} from "./get-server-settings"
|
||||
export * from "./signup"
|
||||
export {default as signup} from "./signup"
|
1
src/apis/login.ts
Normal file
1
src/apis/login.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function login() {}
|
16
src/apis/signup.ts
Normal file
16
src/apis/signup.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import axios from "axios"
|
||||
|
||||
export interface SignupResult {
|
||||
normalized_email: string
|
||||
}
|
||||
|
||||
export default async function signup(email: string): Promise<SignupResult> {
|
||||
const {data} = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_BASE_URL}/auth/signup`,
|
||||
{
|
||||
email,
|
||||
},
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
22
src/apis/validate-token.ts
Normal file
22
src/apis/validate-token.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {AuthenticationDetails} from "~/types"
|
||||
import axios from "axios"
|
||||
|
||||
export interface ValidateTokenData {
|
||||
email: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export default async function validateToken({
|
||||
email,
|
||||
token,
|
||||
}: ValidateTokenData): Promise<AuthenticationDetails> {
|
||||
const {data} = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_BASE_URL}/auth/verify-email`,
|
||||
{
|
||||
email: email,
|
||||
token: token,
|
||||
},
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import {forwardRef, ReactElement, useEffect, useRef, useState} from "react"
|
||||
import React, {forwardRef, ReactElement, useEffect, useRef, useState} from "react"
|
||||
import {Paper} from "@mui/material"
|
||||
import whenElementHasBounds from "~/utils/when-element-has-bounds"
|
||||
import {whenElementHasBounds} from "~/utils"
|
||||
|
||||
export interface MultiStepFormProps {
|
||||
steps: (() => ReactElement)[]
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Box, Container} from "@mui/material"
|
||||
import {ReactElement} from "react"
|
||||
import React, {ReactElement} from "react"
|
||||
|
||||
export interface MultiStepFormElementProps {
|
||||
children: ReactElement
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {ReactElement} from "react"
|
||||
import UAParser from "ua-parser-js"
|
||||
import {Button} from "@mui/material"
|
||||
import APP_LINK_MAP from "utils/app-url-links"
|
||||
import {APP_LINK_MAP} from "utils"
|
||||
import {IoMdMailOpen} from "react-icons/io"
|
||||
|
||||
export interface OpenMailButtonProps {
|
||||
|
12
src/components/index.ts
Normal file
12
src/components/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * from "./MultiStepForm"
|
||||
export { default as MultiStepForm } from "./MultiStepForm"
|
||||
export * from "./MultiStepFormElement"
|
||||
export { default as MultiStepFormElement } from "./MultiStepFormElement"
|
||||
export * from "./OpenMailButton"
|
||||
export { default as OpenMailButton } from "./OpenMailButton"
|
||||
export * from "./PasswordField"
|
||||
export { default as PasswordField } from "./PasswordField"
|
||||
export * from "./SimpleForm"
|
||||
export { default as SimpleForm } from "./SimpleForm"
|
||||
export * from "./SingleElementWrapper"
|
||||
export { default as SingleElementWrapper } from "./SingleElementWrapper"
|
@ -1,15 +1,12 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
Route,
|
||||
} from "react-router-dom";
|
||||
import {createBrowserRouter, RouterProvider,} from "react-router-dom";
|
||||
import RootRoute from "~/routes/Root";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <div>Hello world!</div>,
|
||||
element: <RootRoute />,
|
||||
},
|
||||
]);
|
||||
|
||||
|
140
src/route-widgets/root/EmailForm/DetectEmailAutofillService.tsx
Normal file
140
src/route-widgets/root/EmailForm/DetectEmailAutofillService.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import {ReactElement, useCallback, useEffect, useRef, useState} from "react"
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
} from "@mui/material"
|
||||
import {MdCheck} from "react-icons/md"
|
||||
import useSessionStorage from "hooks/use-session-storage"
|
||||
|
||||
export interface DetectEmailAutofillServiceProps {
|
||||
domains: string[]
|
||||
}
|
||||
|
||||
enum AliasType {
|
||||
DuckDuckGo = "duck.com",
|
||||
SimpleLogin = "simplelogin.com",
|
||||
}
|
||||
|
||||
const TYPE_NAME_MAP: Record<AliasType, string> = {
|
||||
[AliasType.DuckDuckGo]: "DuckDuckGo's Email Tracking Protection",
|
||||
[AliasType.SimpleLogin]: "SimpleLogin",
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "has-shown-email-autofill-service"
|
||||
|
||||
export default function DetectEmailAutofillService({
|
||||
domains,
|
||||
}: DetectEmailAutofillServiceProps): ReactElement {
|
||||
const $hasDetected = useRef<boolean>(false)
|
||||
|
||||
const [type, setType] = useState<AliasType | null>(null)
|
||||
const [hasShownModal, setHasShownModal] = useSessionStorage<boolean>(
|
||||
STORAGE_KEY,
|
||||
false,
|
||||
)
|
||||
|
||||
const handleFound = useCallback(
|
||||
(type: AliasType) => {
|
||||
if (domains.includes(type)) {
|
||||
if (hasShownModal) {
|
||||
setType(type)
|
||||
|
||||
setHasShownModal(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
[domains.length, hasShownModal],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const checkDuckDuckGo = () => {
|
||||
const $element = document.querySelector("body > ddg-autofill")
|
||||
|
||||
if ($element) {
|
||||
$hasDetected.current = true
|
||||
handleFound(AliasType.DuckDuckGo)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
const checkSimpleLogin = () => {
|
||||
const $element = document.querySelector(
|
||||
"body > div.sl-button-wrapper",
|
||||
)
|
||||
|
||||
if (
|
||||
$element &&
|
||||
$element.children[0].nodeName === "DIV" &&
|
||||
$element.children[0].className === "sl-button"
|
||||
) {
|
||||
$hasDetected.current = true
|
||||
handleFound(AliasType.SimpleLogin)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkDuckDuckGo() || checkSimpleLogin()) {
|
||||
return
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if ($hasDetected.current) {
|
||||
return
|
||||
}
|
||||
|
||||
checkDuckDuckGo()
|
||||
checkSimpleLogin()
|
||||
})
|
||||
|
||||
observer.observe(document.body, {subtree: false, childList: true})
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [handleFound])
|
||||
|
||||
return (
|
||||
<Dialog open={type !== null} onClose={() => setType(null)}>
|
||||
<DialogTitle>Email relay service detected</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} justifyContent="center">
|
||||
<Grid item>
|
||||
<DialogContentText>
|
||||
We detected that you are using an email relay
|
||||
service to sign up. This KleckRelay instance does
|
||||
not support relaying to another email relay service.
|
||||
You can either choose a different instance or sign
|
||||
up with a different email address.
|
||||
</DialogContentText>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<DialogContentText>
|
||||
Detected email relay:
|
||||
</DialogContentText>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Alert severity="info">{TYPE_NAME_MAP[type!]}</Alert>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
autoFocus
|
||||
startIcon={<MdCheck />}
|
||||
onClick={() => setType(null)}
|
||||
>
|
||||
Got it
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
82
src/route-widgets/root/EmailForm/index.tsx
Normal file
82
src/route-widgets/root/EmailForm/index.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import {ReactElement} from "react"
|
||||
import {useFormik} from "formik"
|
||||
import useSchema, {Form} from "./use-schema"
|
||||
import {ServerSettings} from "apis/get-server-settings"
|
||||
import signup from "apis/signup"
|
||||
import {MdEmail} from "react-icons/md"
|
||||
import {InputAdornment, TextField} from "@mui/material"
|
||||
import DetectEmailAutofillService from "./DetectEmailAutofillService"
|
||||
import handleErrors from "utils/handle-errors"
|
||||
import {MultiStepFormElement, SimpleForm} from "components"
|
||||
|
||||
interface EmailFormProps {
|
||||
serverSettings: ServerSettings
|
||||
onSignUp: (email: string) => void
|
||||
}
|
||||
|
||||
export default function EmailForm({
|
||||
serverSettings,
|
||||
onSignUp,
|
||||
}: EmailFormProps): ReactElement {
|
||||
const schema = useSchema(serverSettings)
|
||||
const formik = useFormik<Form>({
|
||||
validationSchema: schema,
|
||||
initialValues: {
|
||||
email: "",
|
||||
},
|
||||
onSubmit: (values, {setErrors}) =>
|
||||
handleErrors(
|
||||
values.email,
|
||||
setErrors,
|
||||
)(signup).then(({normalized_email}) => onSignUp(normalized_email)),
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<MultiStepFormElement>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<SimpleForm
|
||||
title="Sign up"
|
||||
description="We only need your email and you are ready to go!"
|
||||
continueActionLabel="Sign up"
|
||||
nonFieldError={formik.errors.detail}
|
||||
isSubmitting={formik.isSubmitting}
|
||||
>
|
||||
{[
|
||||
<TextField
|
||||
key="email"
|
||||
fullWidth
|
||||
name="email"
|
||||
id="email"
|
||||
label="Email"
|
||||
inputMode="email"
|
||||
value={formik.values.email}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched.email &&
|
||||
Boolean(formik.errors.email)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.email && formik.errors.email
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<MdEmail />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
</SimpleForm>
|
||||
</form>
|
||||
</MultiStepFormElement>
|
||||
{!serverSettings.other_relays_enabled && (
|
||||
<DetectEmailAutofillService
|
||||
domains={serverSettings.other_relay_domains}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
43
src/route-widgets/root/EmailForm/use-schema.ts
Normal file
43
src/route-widgets/root/EmailForm/use-schema.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import * as yup from "yup"
|
||||
import {ServerSettings} from "apis/get-server-settings"
|
||||
import checkIsDomainDisposable from "apis/check-is-domain-disposable"
|
||||
|
||||
export interface Form {
|
||||
email: string
|
||||
detail?: string
|
||||
}
|
||||
|
||||
export default function useSchema(
|
||||
serverSettings: ServerSettings,
|
||||
): yup.BaseSchema {
|
||||
return yup.object().shape({
|
||||
email: yup
|
||||
.string()
|
||||
.email()
|
||||
.required()
|
||||
.test(
|
||||
"notDisposable",
|
||||
"Disposable email addresses are not allowed",
|
||||
async (value, context) => {
|
||||
if (serverSettings.disposable_emails_enabled) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await yup.string().email().validate(value, {
|
||||
strict: true,
|
||||
})
|
||||
const isDisposable = await checkIsDomainDisposable(
|
||||
value!.split("@")[1],
|
||||
)
|
||||
|
||||
return !isDisposable
|
||||
} catch ({message}) {
|
||||
// @ts-ignore
|
||||
context.createError({message})
|
||||
return false
|
||||
}
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
85
src/route-widgets/root/GenerateEmailReportsForm.tsx
Normal file
85
src/route-widgets/root/GenerateEmailReportsForm.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import {ReactElement} from "react"
|
||||
import {Box, Button, Grid, Typography} from "@mui/material"
|
||||
import {FaLongArrowAltRight} from "react-icons/fa"
|
||||
import {TiCancel} from "react-icons/ti"
|
||||
|
||||
export interface GenerateEmailReportsFormProps {
|
||||
onYes: () => void
|
||||
onNo: () => void
|
||||
}
|
||||
|
||||
export default function GenerateEmailReportsForm({
|
||||
onNo,
|
||||
onYes,
|
||||
}: GenerateEmailReportsFormProps): ReactElement {
|
||||
return (
|
||||
<Box width="80vw">
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
spacing={4}
|
||||
paddingX={2}
|
||||
paddingTop={4}
|
||||
paddingBottom={1}
|
||||
alignItems="end"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
spacing={4}
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Grid container spacing={2} direction="column">
|
||||
<Grid item>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h2"
|
||||
align="center"
|
||||
>
|
||||
Generate Email Reports?
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="p"
|
||||
>
|
||||
Would you like to create fully encrypted
|
||||
email reports for your mails? Only you
|
||||
will be able to access it. Not even we
|
||||
can decrypt it.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid container spacing={2} direction="row">
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<TiCancel />}
|
||||
color="secondary"
|
||||
onClick={onNo}
|
||||
>
|
||||
No
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
endIcon={<FaLongArrowAltRight />}
|
||||
color="primary"
|
||||
onClick={onYes}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
168
src/route-widgets/root/PasswordForm.tsx
Normal file
168
src/route-widgets/root/PasswordForm.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
import {ReactElement, useMemo} from "react"
|
||||
import * as yup from "yup"
|
||||
import {useFormik} from "formik"
|
||||
import {Box, Grid, InputAdornment, Typography} from "@mui/material"
|
||||
import {MdCheckCircle, MdChevronRight, MdLock} from "react-icons/md"
|
||||
import {LoadingButton} from "@mui/lab"
|
||||
import {PasswordField} from "components"
|
||||
import handleErrors from "utils/handle-errors"
|
||||
import {generateKey} from "openpgp"
|
||||
import {isDev} from "constants/values"
|
||||
import encryptString from "utils/encrypt-string"
|
||||
|
||||
export interface PasswordFormProps {
|
||||
email: string
|
||||
}
|
||||
|
||||
interface Form {
|
||||
password: string
|
||||
passwordConfirmation: string
|
||||
}
|
||||
|
||||
const schema = yup.object().shape({
|
||||
password: yup.string().required(),
|
||||
passwordConfirmation: yup
|
||||
.string()
|
||||
.required()
|
||||
.oneOf([yup.ref("password"), null], "Passwords must match"),
|
||||
})
|
||||
|
||||
export default function PasswordForm({email}: PasswordFormProps): ReactElement {
|
||||
const awaitGenerateKey = useMemo(
|
||||
() =>
|
||||
generateKey({
|
||||
type: "rsa",
|
||||
format: "armored",
|
||||
curve: "curve25519",
|
||||
userIDs: [{name: "John Smith", email: "john@example.com"}],
|
||||
passphrase: "",
|
||||
rsaBits: isDev ? 2048 : 4096,
|
||||
}),
|
||||
[],
|
||||
)
|
||||
const formik = useFormik<Form>({
|
||||
validationSchema: schema,
|
||||
initialValues: {
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
},
|
||||
onSubmit: (values, {setErrors}) =>
|
||||
handleErrors(
|
||||
values,
|
||||
setErrors,
|
||||
)(async () => {
|
||||
const keyPair = await awaitGenerateKey
|
||||
|
||||
const encryptedPrivateKey = encryptString(
|
||||
keyPair.privateKey,
|
||||
`${values.password}-${email}`,
|
||||
)
|
||||
|
||||
console.log(encryptedPrivateKey)
|
||||
}),
|
||||
})
|
||||
|
||||
return (
|
||||
<Box width="80vw">
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
paddingX={2}
|
||||
paddingY={4}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Grid container spacing={2} direction="column">
|
||||
<Grid item>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h2"
|
||||
align="center"
|
||||
>
|
||||
Set up your password
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle1" component="p">
|
||||
Please enter a safe password so that we can
|
||||
encrypt your data.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid container spacing={2} justifyContent="center">
|
||||
<Grid item>
|
||||
<PasswordField
|
||||
fullWidth
|
||||
id="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
autoComplete="new-password"
|
||||
value={formik.values.password}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched.password &&
|
||||
Boolean(formik.errors.password)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.password &&
|
||||
formik.errors.password
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<MdLock />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<PasswordField
|
||||
fullWidth
|
||||
id="passwordConfirmation"
|
||||
name="passwordConfirmation"
|
||||
label="Confirm Password"
|
||||
value={formik.values.passwordConfirmation}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched.passwordConfirmation &&
|
||||
Boolean(
|
||||
formik.errors.passwordConfirmation,
|
||||
)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.passwordConfirmation &&
|
||||
formik.errors.passwordConfirmation
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<MdCheckCircle />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={formik.isSubmitting}
|
||||
startIcon={<MdChevronRight />}
|
||||
>
|
||||
Continue
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</Box>
|
||||
)
|
||||
}
|
39
src/route-widgets/root/YouGotMail.tsx
Normal file
39
src/route-widgets/root/YouGotMail.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import {ReactElement} from "react"
|
||||
import {Grid, Typography} from "@mui/material"
|
||||
import {MultiStepFormElement, OpenMailButton} from "~/components"
|
||||
|
||||
export interface YouGotMailProps {
|
||||
domain: string
|
||||
}
|
||||
|
||||
export default function YouGotMail({domain}: YouGotMailProps): ReactElement {
|
||||
return (
|
||||
<MultiStepFormElement>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
spacing={4}
|
||||
paddingX={2}
|
||||
paddingY={4}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="h6" component="h2" align="center">
|
||||
You got mail!
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle1" component="p">
|
||||
We sent you an email with a link to confirm your email
|
||||
address. Please check your inbox and click on the link
|
||||
to continue.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<OpenMailButton domain={domain} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MultiStepFormElement>
|
||||
)
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import {ReactElement} from "react";
|
||||
import SingleElementWrapper from "~/components/SingleElementWrapper";
|
||||
import MultiStepForm from "~/components/MultiStepForm";
|
||||
import EmailForm from "~/route-widgets/root/EmailForm";
|
||||
import YouGotMail from "~/route-widgets/root/YouGotMail";
|
||||
|
||||
export default function RootRoute(): ReactElement {
|
||||
return (
|
||||
<SingleElementWrapper>
|
||||
<MultiStepForm
|
||||
steps={[
|
||||
() => (
|
||||
<EmailForm
|
||||
serverSettings={{}}
|
||||
onSignUp={() => null}
|
||||
/>
|
||||
),
|
||||
() => <YouGotMail domain={""} />,
|
||||
]}
|
||||
index={0}
|
||||
/>
|
||||
</SingleElementWrapper>
|
||||
);
|
||||
}
|
49
src/types.d.ts
vendored
Normal file
49
src/types.d.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
export enum ImageProxyFormatType {
|
||||
WEBP = "webp",
|
||||
PNG = "png",
|
||||
JPEG = "jpeg",
|
||||
}
|
||||
|
||||
export enum ProxyUserAgentType {
|
||||
APPLE_MAIL = "apple-mail",
|
||||
GOOGLE_MAIL = "google-mail",
|
||||
OUTLOOK_WINDOWS = "outlook-windows",
|
||||
OUTLOOK_MACOS = "outlook-macos",
|
||||
FIREFOX = "firefox",
|
||||
CHROME = "chrome",
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
createdAt: Date
|
||||
email: {
|
||||
address: string
|
||||
isVerified: boolean
|
||||
}
|
||||
preferences: {
|
||||
aliasRemoveTrackers: boolean
|
||||
aliasCreateMailReport: boolean
|
||||
aliasProxyImages: boolean
|
||||
aliasImageProxyFormat: ImageProxyFormatType
|
||||
aliasImageProxyUserAgent: ProxyUserAgentType
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuthenticationDetails {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
}
|
||||
|
||||
export interface ServerSettings {
|
||||
mail_domain: string
|
||||
random_email_id_min_length: number
|
||||
random_email_id_chars: string
|
||||
image_proxy_enabled: boolean
|
||||
image_proxy_life_time: number
|
||||
disposable_emails_enabled: boolean
|
||||
other_relays_enabled: boolean
|
||||
other_relay_domains: Array<string>
|
||||
email_verification_chars: string
|
||||
email_verification_length: number
|
||||
}
|
||||
|
10
src/utils/index.ts
Normal file
10
src/utils/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export * from "./app-url-links"
|
||||
export {default as APP_LINK_MAP} from "./app-url-links"
|
||||
export * from "./encrypt-string"
|
||||
export { default as encryptString } from "./encrypt-string"
|
||||
export * from "./handle-errors"
|
||||
export {default as handleErrors} from "./handle-errors"
|
||||
export * from "./parse-fastapi-error"
|
||||
export {default as parseFastapiError} from "./parse-fastapi-error"
|
||||
export * from "./when-element-has-bounds"
|
||||
export {default as whenElementHasBounds} from "./when-element-has-bounds"
|
12
yarn.lock
12
yarn.lock
@ -561,6 +561,13 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-icons@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-icons/-/react-icons-3.0.0.tgz#27ca2823a6add881d06a371bfff093afc1b9c829"
|
||||
integrity sha512-Vefs6LkLqF61vfV7AiAqls+vpR94q67gunhMueDznG+msAkrYgRxl7gYjNem/kZ+as2l2mNChmF1jRZzzQQtMg==
|
||||
dependencies:
|
||||
react-icons "*"
|
||||
|
||||
"@types/react-is@^16.7.1 || ^17.0.0":
|
||||
version "17.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
|
||||
@ -2201,6 +2208,11 @@ react-fast-compare@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-icons@*, react-icons@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.4.0.tgz#a13a8a20c254854e1ec9aecef28a95cdf24ef703"
|
||||
integrity sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==
|
||||
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
Loading…
x
Reference in New Issue
Block a user