pstream is dead; long live pstream
taciturnaxolotl.github.io/pstream-ng/
1import { useState } from "react";
2import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
3import { useTranslation } from "react-i18next";
4import { useNavigate } from "react-router-dom";
5
6import { MetaResponse } from "@/backend/accounts/meta";
7import { Button } from "@/components/buttons/Button";
8import { BackendSelector } from "@/components/form/BackendSelector";
9import {
10 LargeCard,
11 LargeCardButtons,
12 LargeCardText,
13} from "@/components/layout/LargeCard";
14import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
15import {
16 AccountCreatePart,
17 AccountProfile,
18} from "@/pages/parts/auth/AccountCreatePart";
19import { PassphraseGeneratePart } from "@/pages/parts/auth/PassphraseGeneratePart";
20import { TrustBackendPart } from "@/pages/parts/auth/TrustBackendPart";
21import { VerifyPassphrase } from "@/pages/parts/auth/VerifyPassphrasePart";
22import { PageTitle } from "@/pages/parts/util/PageTitle";
23import { conf } from "@/setup/config";
24import { useAuthStore } from "@/stores/auth";
25
26function CaptchaProvider(props: {
27 siteKey: string | null;
28 children: JSX.Element;
29}) {
30 if (!props.siteKey) return props.children;
31 return (
32 <GoogleReCaptchaProvider reCaptchaKey={props.siteKey}>
33 {props.children}
34 </GoogleReCaptchaProvider>
35 );
36}
37
38export function RegisterPage() {
39 const navigate = useNavigate();
40 const { t } = useTranslation();
41 const setBackendUrl = useAuthStore((s) => s.setBackendUrl);
42 const currentBackendUrl = useAuthStore((s) => s.backendUrl);
43 const config = conf();
44 const availableBackends =
45 config.BACKEND_URLS.length > 0
46 ? config.BACKEND_URLS
47 : config.BACKEND_URL
48 ? [config.BACKEND_URL]
49 : [];
50
51 // If there's only one backend and user hasn't selected a custom one, auto-select it
52 const defaultBackend =
53 currentBackendUrl ??
54 (availableBackends.length === 1 ? availableBackends[0] : null);
55
56 const [step, setStep] = useState(
57 availableBackends.length > 1 || !defaultBackend ? -1 : 0,
58 );
59 const [mnemonic, setMnemonic] = useState<null | string>(null);
60 const [credentialId, setCredentialId] = useState<null | string>(null);
61 const [authMethod, setAuthMethod] = useState<"mnemonic" | "passkey">(
62 "mnemonic",
63 );
64 const [account, setAccount] = useState<null | AccountProfile>(null);
65 const [siteKey, setSiteKey] = useState<string | null>(null);
66 const [selectedBackendUrl, setSelectedBackendUrl] = useState<string | null>(
67 currentBackendUrl ?? defaultBackend ?? null,
68 );
69
70 const handleBackendSelect = (url: string | null) => {
71 setSelectedBackendUrl(url);
72 if (url) {
73 setBackendUrl(url);
74 }
75 };
76
77 return (
78 <CaptchaProvider siteKey={siteKey}>
79 <SubPageLayout>
80 <PageTitle subpage k="global.pages.register" />
81 {step === -1 && (availableBackends.length > 1 || !defaultBackend) ? (
82 <LargeCard>
83 <LargeCardText title={t("auth.backendSelection.title")}>
84 {t("auth.backendSelection.description")}
85 </LargeCardText>
86 <BackendSelector
87 selectedUrl={selectedBackendUrl}
88 onSelect={handleBackendSelect}
89 availableUrls={availableBackends}
90 showCustom
91 />
92 <LargeCardButtons>
93 <span className="text-type-danger font-medium text-center">
94 {t("settings.connections.server.notice")}
95 </span>
96 <Button
97 theme="purple"
98 onClick={() => {
99 if (selectedBackendUrl) {
100 setStep(0);
101 }
102 }}
103 disabled={!selectedBackendUrl}
104 >
105 {t("auth.register.information.next")}
106 </Button>
107 </LargeCardButtons>
108 </LargeCard>
109 ) : null}
110 {step === 0 ? (
111 <TrustBackendPart
112 backendUrl={selectedBackendUrl}
113 onNext={(meta: MetaResponse) => {
114 setSiteKey(
115 meta.hasCaptcha && meta.captchaClientKey
116 ? meta.captchaClientKey
117 : null,
118 );
119 setStep(1);
120 }}
121 />
122 ) : null}
123 {step === 1 ? (
124 <PassphraseGeneratePart
125 onNext={(m) => {
126 setMnemonic(m);
127 setAuthMethod("mnemonic");
128 setStep(2);
129 }}
130 onPasskeyNext={(credId) => {
131 setCredentialId(credId);
132 setAuthMethod("passkey");
133 setStep(2);
134 }}
135 />
136 ) : null}
137 {step === 2 ? (
138 <AccountCreatePart
139 onNext={(a) => {
140 setAccount(a);
141 setStep(3);
142 }}
143 />
144 ) : null}
145 {step === 3 ? (
146 <VerifyPassphrase
147 hasCaptcha={!!siteKey}
148 mnemonic={mnemonic}
149 credentialId={credentialId}
150 authMethod={authMethod}
151 userData={account}
152 backendUrl={selectedBackendUrl}
153 onNext={() => {
154 navigate("/");
155 }}
156 />
157 ) : null}
158 </SubPageLayout>
159 </CaptchaProvider>
160 );
161}