Реализован интерфейс согласий

This commit is contained in:
mi
2026-07-23 14:29:16 +03:00
parent b1ed714d5b
commit 31efbf3b69
27 changed files with 817 additions and 110 deletions
@@ -0,0 +1,228 @@
import React, { useMemo, useState } from "react";
import { Linking, Pressable, Text, View } from "react-native";
import type { PublicConfig } from "../types";
import { Button, styles } from "../ui";
import { colors, radii, spacing } from "../theme";
type ConsentKey = "personal_data" | "user_agreement" | "marketing";
type ConsentLink = {
label: string;
url: string;
};
type ConsentBlock = {
key: ConsentKey;
text: string;
required: boolean;
links: ConsentLink[];
};
type ConsentConfigItem = PublicConfig["consents"][string];
type Props = {
consents: PublicConfig["consents"] | undefined;
onAccept: (accepted: Record<ConsentKey, boolean>) => void;
onCancel: () => void;
};
const LABELS: Record<ConsentKey, {
text: string;
links: Array<{ label: string; readUrl: (item: ConsentConfigItem) => string | null | undefined }>;
}> = {
personal_data: {
text: "Я ознакомлен с Политикой обработки персональных данных ООО «ХАН» и даю своё Согласие на обработку моих персональных данных",
links: [
{ label: "Согласие на обработку ПД", readUrl: (item) => item.document_url },
{ label: "Политика обработки ПД", readUrl: (item) => item.privacy_policy_document_url },
],
},
user_agreement: {
text: "Я прочитал и соглашаюсь с Пользовательским соглашением",
links: [{ label: "Пользовательское соглашение", readUrl: (item) => item.document_url }],
},
marketing: {
text: "Я даю своё согласие на получение рекламных и маркетинговых коммуникаций",
links: [{ label: "Условия получения коммуникаций", readUrl: (item) => item.document_url }],
},
};
export function ConsentModal({ consents, onAccept, onCancel }: Props) {
const blocks = useMemo(() => buildBlocks(consents), [consents]);
const [checked, setChecked] = useState<Record<ConsentKey, boolean>>({
personal_data: false,
user_agreement: false,
marketing: false,
});
const requiredDone = blocks.length > 0
&& blocks.filter((block) => block.required).every((block) => checked[block.key]);
const toggle = (key: ConsentKey) => {
setChecked((prev) => ({ ...prev, [key]: !prev[key] }));
};
return (
<View accessibilityViewIsModal style={styles.modalBackdrop}>
<View style={[styles.modal, { maxWidth: 400, gap: spacing.lg }]}>
<View style={{ gap: spacing.sm }}>
<View style={consentStyles.iconWrap}>
<Text style={consentStyles.iconGlyph}></Text>
</View>
<Text accessibilityRole="header" style={[styles.title, { fontSize: 22 }]}>
Перед началом работы
</Text>
<Text style={styles.muted}>
Для использования приложения ознакомьтесь со следующими документами и предоставьте необходимые согласия
</Text>
</View>
<View style={{ gap: spacing.md }}>
{blocks.map((block) => {
const isChecked = checked[block.key];
return (
<Pressable
key={block.key}
accessibilityRole="checkbox"
accessibilityState={{ checked: isChecked }}
onPress={() => toggle(block.key)}
style={[
consentStyles.card,
isChecked ? consentStyles.cardChecked : null,
]}
>
<View style={consentStyles.cardRow}>
<View
style={[
consentStyles.checkbox,
isChecked ? consentStyles.checkboxChecked : null,
]}
>
{isChecked ? <Text style={consentStyles.checkMark}></Text> : null}
</View>
<View style={{ flex: 1, gap: spacing.sm }}>
<Text style={styles.text}>
{block.text}
{block.required ? <Text style={{ color: colors.destructive }}> *</Text> : null}
</Text>
{block.links.length > 0 ? (
<View style={consentStyles.links}>
{block.links.map((link) => (
<Pressable
key={link.url}
accessibilityRole="link"
onPress={(event) => {
event?.stopPropagation?.();
void Linking.openURL(link.url);
}}
>
<Text style={consentStyles.link}> {link.label}</Text>
</Pressable>
))}
</View>
) : null}
</View>
</View>
</Pressable>
);
})}
<Text style={styles.muted}>
<Text style={{ color: colors.destructive }}>*</Text>
{" — обязательные согласия"}
</Text>
</View>
<View style={{ gap: spacing.sm }}>
<Button
title="Продолжить"
disabled={!requiredDone}
onPress={() => onAccept(checked)}
/>
<Button title="Отмена" secondary onPress={onCancel} />
</View>
</View>
</View>
);
}
function buildBlocks(consents: PublicConfig["consents"] | undefined): ConsentBlock[] {
return (Object.keys(LABELS) as ConsentKey[]).flatMap((key) => {
const item = consents?.[key];
if (!item) return [];
const meta = LABELS[key];
const links = meta.links.flatMap((link) => {
const url = link.readUrl(item);
return url ? [{ label: link.label, url }] : [];
});
return [{
key,
text: meta.text,
required: item.required,
links,
}];
});
}
const consentStyles = {
iconWrap: {
width: 48,
height: 48,
borderRadius: 16,
backgroundColor: "rgba(3, 2, 19, 0.08)",
alignItems: "center" as const,
justifyContent: "center" as const,
marginBottom: spacing.xs,
},
iconGlyph: {
color: colors.primary,
fontSize: 22,
fontWeight: "700" as const,
},
card: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: radii.xl,
backgroundColor: colors.card,
padding: spacing.md,
},
cardChecked: {
borderColor: "rgba(3, 2, 19, 0.3)",
backgroundColor: "rgba(3, 2, 19, 0.04)",
},
cardRow: {
flexDirection: "row" as const,
alignItems: "flex-start" as const,
gap: spacing.md,
},
checkbox: {
width: 20,
height: 20,
borderRadius: radii.sm,
borderWidth: 2,
borderColor: colors.border,
backgroundColor: colors.background,
alignItems: "center" as const,
justifyContent: "center" as const,
marginTop: 2,
},
checkboxChecked: {
borderColor: colors.primary,
backgroundColor: colors.primary,
},
checkMark: {
color: colors.primaryForeground,
fontSize: 12,
fontWeight: "700" as const,
lineHeight: 14,
},
links: {
flexDirection: "row" as const,
flexWrap: "wrap" as const,
gap: spacing.sm,
},
link: {
color: colors.primary,
fontSize: 12,
textDecorationLine: "underline" as const,
},
};