feat(pwa): add base
All checks were successful
/ build-web (push) Successful in 5m35s
/ build-desktop (linux) (push) Successful in 9m41s
/ build-mobile (push) Successful in 20m12s
/ release (push) Successful in 1m51s
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
All checks were successful
/ build-web (push) Successful in 5m35s
/ build-desktop (linux) (push) Successful in 9m41s
/ build-mobile (push) Successful in 20m12s
/ release (push) Successful in 1m51s
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
This commit is contained in:
parent
6e977bca7e
commit
7b36218ffa
40 changed files with 4014 additions and 922 deletions
|
|
@ -21,17 +21,37 @@ export {
|
|||
type OnboardingStepControls,
|
||||
} from "@methanium/ui";
|
||||
|
||||
|
||||
|
||||
interface GateState {
|
||||
docs: z.infer<typeof legalDocsSchema>;
|
||||
acceptedPP: boolean;
|
||||
acceptedTOS: boolean;
|
||||
changedPP: boolean;
|
||||
changedTOS: boolean;
|
||||
includeLegal: boolean;
|
||||
includeOnboarding: boolean;
|
||||
includeTauriPermissions: boolean;
|
||||
}
|
||||
|
||||
async function fetchLegalDocumentHash(document: string) {
|
||||
const response = await fetch(
|
||||
`https://legal.methanium.net/tensamin/${document}/raw`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Legal document request failed: ${response.status}`);
|
||||
}
|
||||
if (!response.headers.get("content-type")?.startsWith("text/plain")) {
|
||||
throw new Error("Legal document request returned an invalid content type");
|
||||
}
|
||||
|
||||
const hash = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
await response.arrayBuffer(),
|
||||
);
|
||||
return Array.from(new Uint8Array(hash), (byte) =>
|
||||
byte.toString(16).padStart(2, "0"),
|
||||
).join("");
|
||||
}
|
||||
|
||||
export default function OnboardingGate({ children }: { children: ReactNode }) {
|
||||
const { load, save } = useStorage();
|
||||
const [state, setState] = useState<GateState>();
|
||||
|
|
@ -51,25 +71,9 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await fetch("https://legal.tensamin.net/api/current");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Legal documents request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const current: unknown = await response.json();
|
||||
if (!active) return;
|
||||
|
||||
const parsed = legalDocsSchema.safeParse(current);
|
||||
if (!parsed.success) {
|
||||
setError("Failed to load legal documents");
|
||||
setErrorDescription(
|
||||
"The legal documents data received from the server is invalid. Please try again later.",
|
||||
);
|
||||
log(0, "Legal", "red", "Invalid legal documents data", parsed.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const [
|
||||
ppHash,
|
||||
tosHash,
|
||||
localDocs,
|
||||
acceptedPP,
|
||||
acceptedTOS,
|
||||
|
|
@ -77,6 +81,8 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
onboardingStarted,
|
||||
tauriPermissionsDone,
|
||||
] = await Promise.all([
|
||||
fetchLegalDocumentHash("privacy-policy"),
|
||||
fetchLegalDocumentHash("terms-of-service"),
|
||||
load("legal_docs"),
|
||||
load("accepted_privacy_policy"),
|
||||
load("accepted_terms_of_service"),
|
||||
|
|
@ -87,10 +93,16 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
|
||||
if (!active) return;
|
||||
|
||||
const docs = legalDocsSchema.parse({
|
||||
pp: { hash: ppHash },
|
||||
tos: { hash: tosHash },
|
||||
});
|
||||
const changedPP = acceptedPP && localDocs.pp.hash !== docs.pp.hash;
|
||||
const changedTOS = acceptedTOS && localDocs.tos.hash !== docs.tos.hash;
|
||||
const currentAcceptedPP =
|
||||
acceptedPP && localDocs.pp.hash === parsed.data.pp.hash;
|
||||
acceptedPP && localDocs.pp.hash === docs.pp.hash;
|
||||
const currentAcceptedTOS =
|
||||
acceptedTOS && localDocs.tos.hash === parsed.data.tos.hash;
|
||||
acceptedTOS && localDocs.tos.hash === docs.tos.hash;
|
||||
const existingUser = acceptedPP && acceptedTOS;
|
||||
const includeOnboarding =
|
||||
!onboardingDone && (!existingUser || onboardingStarted);
|
||||
|
|
@ -104,9 +116,11 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
setState({
|
||||
docs: parsed.data,
|
||||
docs,
|
||||
acceptedPP: currentAcceptedPP,
|
||||
acceptedTOS: currentAcceptedTOS,
|
||||
changedPP,
|
||||
changedTOS,
|
||||
includeLegal: !currentAcceptedPP || !currentAcceptedTOS,
|
||||
includeOnboarding,
|
||||
includeTauriPermissions:
|
||||
|
|
@ -168,16 +182,28 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
|
||||
const steps: OnboardingStep[] = [];
|
||||
if (state.includeLegal) {
|
||||
const changedDocuments = [
|
||||
state.changedPP && "Privacy Policy",
|
||||
state.changedTOS && "Terms of Service",
|
||||
].filter(Boolean);
|
||||
|
||||
steps.push({
|
||||
id: "legal",
|
||||
title: "Privacy Policy & ToS",
|
||||
description: `${state.docs.pp.version} / ${state.docs.tos.version}`,
|
||||
title:
|
||||
changedDocuments.length > 0
|
||||
? "Legal documents changed"
|
||||
: "Privacy Policy & ToS",
|
||||
description:
|
||||
changedDocuments.length > 0
|
||||
? changedDocuments.join(" & ")
|
||||
: "Review and accept our legal documents",
|
||||
defaultCanContinue: false,
|
||||
content: (
|
||||
<LegalPage
|
||||
docs={state.docs}
|
||||
initiallyAcceptedPP={state.acceptedPP}
|
||||
initiallyAcceptedTOS={state.acceptedTOS}
|
||||
changedPP={state.changedPP}
|
||||
changedTOS={state.changedTOS}
|
||||
onAccept={acceptLegal}
|
||||
/>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,21 +1,17 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import { Checkbox, Label, Link } from "@methanium/ui";
|
||||
import { legalDocsSchema } from "@tensamin/shared/features/legal/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { useOnboardingStep } from "@methanium/ui";
|
||||
|
||||
|
||||
import { Checkbox, Label, Link, useOnboardingStep } from "@methanium/ui";
|
||||
|
||||
export default function LegalPage({
|
||||
docs,
|
||||
initiallyAcceptedPP,
|
||||
initiallyAcceptedTOS,
|
||||
changedPP,
|
||||
changedTOS,
|
||||
onAccept,
|
||||
}: {
|
||||
docs: z.infer<typeof legalDocsSchema>;
|
||||
initiallyAcceptedPP: boolean;
|
||||
initiallyAcceptedTOS: boolean;
|
||||
changedPP: boolean;
|
||||
changedTOS: boolean;
|
||||
onAccept: () => Promise<void>;
|
||||
}) {
|
||||
const [acceptedPP, setAcceptedPP] = useState(initiallyAcceptedPP);
|
||||
|
|
@ -34,34 +30,61 @@ export default function LegalPage({
|
|||
return (
|
||||
<div className="mx-auto flex min-h-[calc(100dvh-17rem)] w-full max-w-5xl flex-col gap-10 p-2 py-20 md:min-h-[calc(100dvh-20.5rem)] md:p-24">
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<BigCheckbox
|
||||
id="acceptPP"
|
||||
checked={acceptedPP}
|
||||
onChange={setAcceptedPP}
|
||||
label="I agree to the Privacy Policy"
|
||||
/>
|
||||
<BigCheckbox
|
||||
id="acceptTOS"
|
||||
checked={acceptedTOS}
|
||||
onChange={setAcceptedTOS}
|
||||
label="I agree to the Terms of Service"
|
||||
/>
|
||||
<div className="w-full border-t-2" />
|
||||
<Link
|
||||
label="Privacy Policy"
|
||||
link={`https://legal.tensamin.net/pp/${docs.pp.version}`}
|
||||
/>
|
||||
<Link
|
||||
label="Terms of Service"
|
||||
link={`https://legal.tensamin.net/tos/${docs.tos.version}`}
|
||||
/>
|
||||
<div className="flex w-full max-w-xl flex-col items-start gap-8">
|
||||
{!initiallyAcceptedPP && (
|
||||
<LegalDocumentAcceptance
|
||||
id="acceptPP"
|
||||
name="Privacy Policy"
|
||||
link="https://legal.methanium.net/tensamin/privacy-policy/"
|
||||
changed={changedPP}
|
||||
checked={acceptedPP}
|
||||
onChange={setAcceptedPP}
|
||||
/>
|
||||
)}
|
||||
{!initiallyAcceptedTOS && (
|
||||
<LegalDocumentAcceptance
|
||||
id="acceptTOS"
|
||||
name="Terms of Service"
|
||||
link="https://legal.methanium.net/tensamin/terms-of-service/"
|
||||
changed={changedTOS}
|
||||
checked={acceptedTOS}
|
||||
onChange={setAcceptedTOS}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LegalDocumentAcceptance({
|
||||
id,
|
||||
name,
|
||||
link,
|
||||
changed,
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
link: string;
|
||||
changed: boolean;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-start gap-3">
|
||||
<Link label={`Read the ${name}`} link={link} />
|
||||
<BigCheckbox
|
||||
id={id}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
label={`I agree to the ${changed ? `updated ${name}` : name}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BigCheckbox({
|
||||
id,
|
||||
label,
|
||||
|
|
|
|||
Loading…
Reference in a new issue