(feat): add mobile notifications
(feat): improve mobile ui & ux
This commit is contained in:
parent
4cb38264d0
commit
13fa3d8db7
29 changed files with 6638 additions and 6094 deletions
|
|
@ -12,6 +12,8 @@
|
|||
"build": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-notification": "~2",
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/storage": "workspace:*",
|
||||
"@methanium/ui": "*",
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import {
|
|||
import { useStorage } from "@tensamin/storage/context";
|
||||
import { legalDocsSchema } from "@tensamin/shared/features/legal/schema";
|
||||
import { log } from "@tensamin/shared/log";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import type { z } from "zod";
|
||||
|
||||
import LegalPage from "./pages/legal";
|
||||
import { onboardingSteps } from "./steps";
|
||||
import TauriPermissionsPage from "./pages/tauriPermissions";
|
||||
|
||||
export {
|
||||
useOnboardingStep,
|
||||
|
|
@ -27,6 +29,7 @@ interface GateState {
|
|||
acceptedTOS: boolean;
|
||||
includeLegal: boolean;
|
||||
includeOnboarding: boolean;
|
||||
includeTauriPermissions: boolean;
|
||||
}
|
||||
|
||||
export default function OnboardingGate({ children }: { children: ReactNode }) {
|
||||
|
|
@ -72,12 +75,14 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
acceptedTOS,
|
||||
onboardingDone,
|
||||
onboardingStarted,
|
||||
tauriPermissionsDone,
|
||||
] = await Promise.all([
|
||||
load("legal_docs"),
|
||||
load("accepted_privacy_policy"),
|
||||
load("accepted_terms_of_service"),
|
||||
load("onboarding_done"),
|
||||
load("onboarding_started"),
|
||||
load("tauri_permissions_done"),
|
||||
]);
|
||||
|
||||
if (!active) return;
|
||||
|
|
@ -104,6 +109,10 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
acceptedTOS: currentAcceptedTOS,
|
||||
includeLegal: !currentAcceptedPP || !currentAcceptedTOS,
|
||||
includeOnboarding,
|
||||
includeTauriPermissions:
|
||||
isTauri() &&
|
||||
/Android/.test(navigator.userAgent) &&
|
||||
!tauriPermissionsDone,
|
||||
});
|
||||
} catch (caught) {
|
||||
if (!active) return;
|
||||
|
|
@ -142,8 +151,11 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
save("onboarding_started", false),
|
||||
]);
|
||||
}
|
||||
if (state?.includeTauriPermissions) {
|
||||
await save("tauri_permissions_done", true);
|
||||
}
|
||||
setComplete(true);
|
||||
}, [save, state?.includeOnboarding]);
|
||||
}, [save, state?.includeOnboarding, state?.includeTauriPermissions]);
|
||||
|
||||
if (error && errorDescription) {
|
||||
return <ErrorScreen error={error} description={errorDescription} />;
|
||||
|
|
@ -174,6 +186,16 @@ export default function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
if (state.includeOnboarding) {
|
||||
steps.push(...onboardingSteps(onboardingThemeId, setOnboardingThemeId));
|
||||
}
|
||||
if (state.includeTauriPermissions) {
|
||||
steps.push({
|
||||
id: "tauri-permissions",
|
||||
title: "Enable notifications",
|
||||
description:
|
||||
"We need these permissions so that notifications can be independent of Google Play Services.",
|
||||
defaultCanContinue: false,
|
||||
content: <TauriPermissionsPage />,
|
||||
});
|
||||
}
|
||||
|
||||
if (complete || steps.length === 0) return <>{children}</>;
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ 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-10 py-20 md:min-h-[calc(100dvh-20.5rem)] md:p-24">
|
||||
<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
|
||||
|
|
|
|||
99
packages/onboarding/src/pages/tauriPermissions.tsx
Normal file
99
packages/onboarding/src/pages/tauriPermissions.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Button, useOnboardingStep } from "@methanium/ui";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission,
|
||||
} from "@tauri-apps/plugin-notification";
|
||||
import { BatteryCharging, Bell } from "lucide-react";
|
||||
|
||||
export default function TauriPermissionsPage() {
|
||||
const [notificationsGranted, setNotificationsGranted] = useState(false);
|
||||
const [batteryExempt, setBatteryExempt] = useState(false);
|
||||
const [notificationAttempted, setNotificationAttempted] = useState(false);
|
||||
const [batteryAttempted, setBatteryAttempted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = () => {
|
||||
void Promise.all([
|
||||
isPermissionGranted(),
|
||||
invoke<boolean>("mtp_is_ignoring_battery_optimizations"),
|
||||
]).then(([notifications, battery]) => {
|
||||
setNotificationsGranted(notifications);
|
||||
setBatteryExempt(battery);
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
window.addEventListener("focus", refresh);
|
||||
document.addEventListener("visibilitychange", refresh);
|
||||
const interval = window.setInterval(refresh, 1_000);
|
||||
return () => {
|
||||
window.removeEventListener("focus", refresh);
|
||||
document.removeEventListener("visibilitychange", refresh);
|
||||
window.clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useOnboardingStep({
|
||||
canContinue:
|
||||
(notificationsGranted || notificationAttempted) &&
|
||||
(batteryExempt || batteryAttempted),
|
||||
onContinue: async () => {
|
||||
await invoke("mtp_set_enabled", { enabled: true });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-xl flex-col gap-12 py-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<Bell className="h-6! w-6!" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">Allow notifications</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Allow Tensamin to notify you while it's closed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={notificationsGranted ? "outline" : "default"}
|
||||
disabled={notificationsGranted}
|
||||
onClick={() => {
|
||||
setNotificationAttempted(true);
|
||||
void requestPermission().then((permission) => {
|
||||
setNotificationsGranted(permission === "granted");
|
||||
});
|
||||
}}
|
||||
>
|
||||
{notificationsGranted ? "Allowed" : "Allow notifications"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<BatteryCharging className="h-6! w-6!" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">Allow running in the background</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Exclude Tensamin from battery optimisation so Android does not
|
||||
suspend it's connection for decryption of live messages.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={batteryExempt ? "outline" : "default"}
|
||||
disabled={batteryExempt}
|
||||
onClick={() => {
|
||||
setBatteryAttempted(true);
|
||||
void invoke("mtp_request_battery_exemption");
|
||||
}}
|
||||
>
|
||||
{batteryExempt ? "Allowed" : "Open battery prompt"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue