Унифицированы экраны, требующие авторизации, в гостевом режиме
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user