Унифицированы экраны, требующие авторизации, в гостевом режиме

This commit is contained in:
mi
2026-07-30 19:47:10 +03:00
parent e24ed9d8ef
commit 1a420eeaf0
7 changed files with 124 additions and 69 deletions
@@ -5,6 +5,7 @@ import { FlatList, Platform, Text, View } from "react-native";
import { useApp } from "../../src/app-context";
import { ChatInputBar } from "../../src/components/ChatInputBar";
import { ChatScreenHeader } from "../../src/components/ChatScreenHeader";
import { GuestAuthGate } from "../../src/components/GuestAuthGate";
import { MessageBubble } from "../../src/components/MessageBubble";
import { ScreenShell } from "../../src/components/ScreenShell";
import { isMessageBlockedError } from "../../src/api";
@@ -179,11 +180,12 @@ export default function ChatScreen() {
if (app.authStatus !== "authenticated") {
return (
<ScreenShell>
<ChatScreenHeader subtitle="Требуется вход" />
<View style={{ flex: 1, padding: spacing.lg, justifyContent: "center" }}>
<Text style={styles.error}>Для просмотра чата требуется авторизация.</Text>
<Button title="На главную" secondary onPress={() => router.replace("/")} />
</View>
<ChatScreenHeader title="Чат" subtitle="Требуется вход" />
<GuestAuthGate
icon="message-circle"
title="Чат доступен после входа"
description="Авторизуйтесь, чтобы переписываться с оператором и получать ответы."
/>
</ScreenShell>
);
}
@@ -1,9 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { Link, useRouter } from "expo-router";
import { useRouter } from "expo-router";
import React, { useEffect, useRef } from "react";
import { ScrollView, Text, View } from "react-native";
import { Text, View } from "react-native";
import { useApp } from "../../src/app-context";
import { AppHeader } from "../../src/components/AppHeader";
import { ChatScreenHeader } from "../../src/components/ChatScreenHeader";
import { GuestAuthGate } from "../../src/components/GuestAuthGate";
import { ScreenShell } from "../../src/components/ScreenShell";
import {
bindPendingFileIntent,
@@ -37,12 +39,12 @@ export default function DialogsScreen() {
if (app.authStatus !== "authenticated") {
return (
<ScreenShell>
<AppHeader guestLabel="Гость" />
<View style={{ flex: 1, padding: spacing.lg, justifyContent: "center", gap: spacing.md }}>
<Text accessibilityRole="header" style={styles.title}>Чат</Text>
<Text style={styles.text}>Чат с компанией доступен после авторизации. Отправьте сообщение на главной странице, чтобы войти.</Text>
<Link href="/" style={styles.link}>На главную</Link>
</View>
<ChatScreenHeader title="Чат" subtitle="Требуется вход" />
<GuestAuthGate
icon="message-circle"
title="Чат доступен после входа"
description="Авторизуйтесь, чтобы переписываться с оператором и получать ответы."
/>
</ScreenShell>
);
}
@@ -4,13 +4,14 @@ import { useRouter } from "expo-router";
import React, { useMemo, useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import { useApp } from "../../src/app-context";
import { GuestAuthGate } from "../../src/components/GuestAuthGate";
import { NotificationCard } from "../../src/components/NotificationCard";
import { ScreenShell } from "../../src/components/ScreenShell";
import { useNotificationAction } from "../../src/notification-actions";
import { notificationApi, notificationKeys } from "../../src/notification-api";
import { typeMap } from "../../src/notification-presenter";
import { colors, radii, spacing } from "../../src/theme";
import { Button, ErrorNotice, Loading, styles } from "../../src/ui";
import { ErrorNotice, Loading, styles } from "../../src/ui";
export default function NotificationCenterScreen() {
const { authStatus } = useApp();
@@ -44,19 +45,11 @@ export default function NotificationCenterScreen() {
</View>
{!authenticated ? (
<View style={local.gate}>
<View style={local.bell}>
<Feather name="bell" size={34} color={colors.primaryForeground} />
</View>
<Text style={styles.title}>Уведомления доступны после входа</Text>
<Text style={[styles.text, local.centerText]}>
Авторизуйтесь, чтобы видеть важные напоминания, документы и статусы услуг.
</Text>
<Button
title="Авторизоваться"
onPress={() => router.replace({ pathname: "/", params: { authorize: "1" } })}
/>
</View>
<GuestAuthGate
icon="bell"
title="Уведомления доступны после входа"
description="Авторизуйтесь, чтобы видеть важные напоминания, документы и статусы услуг."
/>
) : (
<ScrollView contentContainerStyle={local.content}>
{(notifications.isLoading || catalog.isLoading) && <Loading />}
@@ -99,9 +92,6 @@ const local = StyleSheet.create({
borderBottomColor: colors.border,
},
back: { width: 36, height: 36, borderRadius: radii.full, alignItems: "center", justifyContent: "center" },
gate: { flex: 1, justifyContent: "center", alignItems: "center", gap: spacing.md, padding: spacing.xl },
bell: { width: 68, height: 68, borderRadius: radii.full, alignItems: "center", justifyContent: "center", backgroundColor: colors.primary },
centerText: { textAlign: "center" },
content: { padding: spacing.lg, gap: spacing.md, paddingBottom: 40 },
empty: { alignItems: "center", gap: spacing.sm, paddingVertical: 48 },
});
@@ -5,7 +5,7 @@ import React, { useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import { useApp } from "../src/app-context";
import { AccordionSection } from "../src/components/AccordionSection";
import { AppHeader } from "../src/components/AppHeader";
import { GuestAuthGate } from "../src/components/GuestAuthGate";
import { ScreenShell } from "../src/components/ScreenShell";
import { isProduction } from "../src/config";
import { profileApi } from "../src/services";
@@ -30,12 +30,17 @@ export default function ProfileScreen() {
if (!enabled) {
return (
<ScreenShell>
<AppHeader guestLabel="Гость" />
<View style={{ flex: 1, padding: spacing.lg, justifyContent: "center", gap: spacing.md }}>
<View style={stylesLocal.topBar}>
<Pressable accessibilityRole="button" accessibilityLabel="Назад" onPress={() => router.back()} style={({ pressed }) => [stylesLocal.backButton, pressed && stylesLocal.pressed]}>
<Feather name="arrow-left" size={20} color={colors.foreground} />
</Pressable>
<Text accessibilityRole="header" style={styles.title}>Профиль</Text>
<Text style={styles.text}>Профиль доступен после авторизации.</Text>
<Link href="/" style={styles.link}>Перейти на главную для входа</Link>
</View>
<GuestAuthGate
icon="user"
title="Профиль доступен после входа"
description="Авторизуйтесь, чтобы видеть личные данные и документы компании."
/>
</ScreenShell>
);
}
@@ -0,0 +1,51 @@
import { Feather } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { colors, radii, spacing } from "../theme";
import { Button, styles } from "../ui";
export function GuestAuthGate({
icon,
title,
description,
}: {
icon: React.ComponentProps<typeof Feather>["name"];
title: string;
description: string;
}) {
const router = useRouter();
return (
<View style={local.gate}>
<View style={local.icon}>
<Feather name={icon} size={34} color={colors.primaryForeground} />
</View>
<Text accessibilityRole="header" style={[styles.title, local.centerText]}>{title}</Text>
<Text style={[styles.text, local.centerText]}>{description}</Text>
<Button
title="Авторизоваться"
onPress={() => router.replace({ pathname: "/", params: { authorize: "1" } })}
/>
</View>
);
}
const local = StyleSheet.create({
gate: {
flex: 1,
justifyContent: "center",
alignItems: "center",
gap: spacing.md,
padding: spacing.xl,
},
icon: {
width: 68,
height: 68,
borderRadius: radii.full,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.primary,
},
centerText: { textAlign: "center" },
});
@@ -67,7 +67,8 @@ test("Колокольчик открывает центр уведомлени
await page.getByRole("link", { name: "Уведомления" }).click();
await expect(page).toHaveURL(/\/notifications$/);
await expect(page.getByRole("heading", { name: "Центр уведомлений" })).toBeVisible();
await expect(page.getByText(/доступны после входа/)).toBeVisible();
await expect(page.getByRole("heading", { name: "Уведомления доступны после входа" })).toBeVisible();
await expect(page.getByRole("button", { name: "Авторизоваться" })).toBeVisible();
});
test("первое сообщение требует обязательные согласия", async ({ page }) => {
@@ -99,6 +100,18 @@ test("профиль гостя не делает защищённый запр
let protectedCalls = 0;
await page.route("**/api/v1/me", (route) => { protectedCalls++; return route.abort(); });
await page.goto("/profile");
await expect(page.getByText("Профиль доступен после авторизации.")).toBeVisible();
await expect(page.getByRole("heading", { name: "Профиль доступен после входа" })).toBeVisible();
await expect(page.getByText("Авторизуйтесь, чтобы видеть личные данные и документы компании.")).toBeVisible();
await expect(page.getByRole("button", { name: "Авторизоваться" })).toBeVisible();
expect(protectedCalls).toBe(0);
});
test("гостевой чат предлагает авторизоваться", async ({ page }) => {
await page.goto("/dialogs");
await expect(page.getByRole("heading", { name: "Чат доступен после входа" })).toBeVisible();
await expect(page.getByText("Авторизуйтесь, чтобы переписываться с оператором и получать ответы.")).toBeVisible();
await page.getByRole("button", { name: "Авторизоваться" }).click();
await expect(page).toHaveURL(/\/\?authorize=1$/);
await expect(page.getByRole("heading", { name: "Перед началом работы" })).toBeVisible();
});