229 lines
7.5 KiB
TypeScript
229 lines
7.5 KiB
TypeScript
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,
|
|
},
|
|
};
|