chore: build
This commit is contained in:
@@ -15,8 +15,9 @@ import { useNavigation } from "@react-navigation/native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
|
||||
import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
|
||||
import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, getAllProductsAdmin, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
|
||||
import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
|
||||
import type { Product } from "../../api/types";
|
||||
import AlertModal from "../../components/ui/AlertModal";
|
||||
import { useAlert } from "../../hooks/useAlert";
|
||||
|
||||
@@ -643,6 +644,358 @@ function TiersSection({
|
||||
);
|
||||
}
|
||||
|
||||
const REWARD_ACCENT = "#f59e0b";
|
||||
|
||||
const REWARD_TYPES: { value: PointsReward["type"]; label: string; icon: string }[] = [
|
||||
{ value: "free_product", label: "Produit offert", icon: "gift-outline" },
|
||||
{ value: "half_price_product", label: "Produit à -50%", icon: "pricetag-outline" },
|
||||
{ value: "custom", label: "Personnalisé", icon: "star-outline" },
|
||||
];
|
||||
|
||||
const EMPTY_REWARD: PointsReward = {
|
||||
threshold: 20,
|
||||
type: "free_product",
|
||||
description: "",
|
||||
category_configs: [],
|
||||
};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Sélecteur de produits pour une catégorie dans la récompense
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
function CategoryProductPicker({
|
||||
catConfig,
|
||||
products,
|
||||
onChange,
|
||||
colors,
|
||||
s,
|
||||
}: {
|
||||
catConfig: RewardCategoryConfig;
|
||||
products: Product[];
|
||||
onChange: (cfg: RewardCategoryConfig) => void;
|
||||
colors: any;
|
||||
s: any;
|
||||
}) {
|
||||
const catProducts = products.filter((p) => p.category === catConfig.category);
|
||||
|
||||
const toggleProduct = (id: number) => {
|
||||
const ids = catConfig.product_ids.includes(id)
|
||||
? catConfig.product_ids.filter((x) => x !== id)
|
||||
: [...catConfig.product_ids, id];
|
||||
onChange({ ...catConfig, product_ids: ids, all_products: false });
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}>
|
||||
{/* Montant pour cette catégorie */}
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
|
||||
<Text style={{ fontSize: 12, color: s.thresholdSep.color ?? "#888", flex: 1 }}>
|
||||
Valeur du produit offert
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: 56, fontSize: 13 }]}
|
||||
keyboardType="decimal-pad"
|
||||
value={catConfig.amount > 0 ? String(catConfig.amount) : ""}
|
||||
onChangeText={(v) => {
|
||||
const n = parseFloat(v);
|
||||
onChange({ ...catConfig, amount: isNaN(n) ? 0 : n });
|
||||
}}
|
||||
placeholder="0"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted }}>€</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Toggle tous / sélection */}
|
||||
<View style={{ flexDirection: "row", gap: spacing.s }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => onChange({ ...catConfig, all_products: true, product_ids: [] })}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.xs,
|
||||
borderRadius: borderRadius.full, borderWidth: 1.5,
|
||||
borderColor: catConfig.all_products ? REWARD_ACCENT : colors.border,
|
||||
backgroundColor: catConfig.all_products ? REWARD_ACCENT + "22" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark-done-outline" size={13} color={catConfig.all_products ? REWARD_ACCENT : colors.textMuted} />
|
||||
<Text style={{ fontSize: 12, fontWeight: catConfig.all_products ? "700" : "400", color: catConfig.all_products ? REWARD_ACCENT : colors.textMuted }}>
|
||||
Tous ({catProducts.length})
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => onChange({ ...catConfig, all_products: false })}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.xs,
|
||||
borderRadius: borderRadius.full, borderWidth: 1.5,
|
||||
borderColor: !catConfig.all_products ? REWARD_ACCENT : colors.border,
|
||||
backgroundColor: !catConfig.all_products ? REWARD_ACCENT + "22" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="list-outline" size={13} color={!catConfig.all_products ? REWARD_ACCENT : colors.textMuted} />
|
||||
<Text style={{ fontSize: 12, fontWeight: !catConfig.all_products ? "700" : "400", color: !catConfig.all_products ? REWARD_ACCENT : colors.textMuted }}>
|
||||
Sélection
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Liste des produits si mode sélection */}
|
||||
{!catConfig.all_products && (
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
|
||||
{catProducts.length === 0 ? (
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>
|
||||
Aucun produit dans cette catégorie
|
||||
</Text>
|
||||
) : catProducts.map((p) => {
|
||||
const sel = catConfig.product_ids.includes(p.id);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={p.id}
|
||||
onPress={() => toggleProduct(p.id)}
|
||||
style={{
|
||||
paddingHorizontal: spacing.s, paddingVertical: 4,
|
||||
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||
borderColor: sel ? REWARD_ACCENT : colors.border,
|
||||
backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent",
|
||||
flexDirection: "row", alignItems: "center", gap: 4,
|
||||
}}
|
||||
>
|
||||
{sel && <Ionicons name="checkmark" size={11} color={REWARD_ACCENT} />}
|
||||
<Text style={{ fontSize: 12, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{p.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Section centralisée récompenses par palier
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
function CentralRewardSection({
|
||||
reward,
|
||||
allCategories,
|
||||
productsByCategory,
|
||||
onChange,
|
||||
colors,
|
||||
s,
|
||||
}: {
|
||||
reward: PointsReward | null | undefined;
|
||||
allCategories: Category[];
|
||||
productsByCategory: Record<string, Product[]>;
|
||||
onChange: (reward: PointsReward | null) => void;
|
||||
colors: any;
|
||||
s: any;
|
||||
}) {
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
const enabled = !!reward;
|
||||
const r = reward ?? EMPTY_REWARD;
|
||||
|
||||
const update = (patch: Partial<PointsReward>) =>
|
||||
onChange({ ...r, ...patch });
|
||||
|
||||
const getCatConfig = (catName: string): RewardCategoryConfig =>
|
||||
r.category_configs.find((c) => c.category === catName) ??
|
||||
{ category: catName, all_products: true, product_ids: [], amount: 0 };
|
||||
|
||||
const isCatSelected = (catName: string) =>
|
||||
r.category_configs.some((c) => c.category === catName);
|
||||
|
||||
const toggleCategory = (catName: string) => {
|
||||
if (isCatSelected(catName)) {
|
||||
update({ category_configs: r.category_configs.filter((c) => c.category !== catName) });
|
||||
} else {
|
||||
update({ category_configs: [...r.category_configs, { category: catName, all_products: true, product_ids: [], amount: 0 }] });
|
||||
}
|
||||
};
|
||||
|
||||
const updateCatConfig = (cfg: RewardCategoryConfig) => {
|
||||
update({
|
||||
category_configs: r.category_configs.map((c) =>
|
||||
c.category === cfg.category ? cfg : c
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={s.section}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingRight: spacing.l }}>
|
||||
<Text style={s.sectionTitle}>Récompenses par palier</Text>
|
||||
<View style={{
|
||||
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
||||
backgroundColor: enabled ? REWARD_ACCENT + "25" : colors.border + "40",
|
||||
borderWidth: 1, borderColor: enabled ? REWARD_ACCENT : colors.border,
|
||||
marginBottom: spacing.s,
|
||||
}}>
|
||||
<Text style={{ fontSize: 10, fontWeight: "700", color: enabled ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{enabled ? "Activée" : "Désactivée"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[s.row, s.rowFirst]}>
|
||||
<View style={s.rowLeft}>
|
||||
<Text style={s.rowLabel}>Récompense activée</Text>
|
||||
<Text style={s.rowDesc}>
|
||||
Le seuil s'applique indépendamment à chaque type de points.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={enabled}
|
||||
onValueChange={(v) => onChange(v ? EMPTY_REWARD : null)}
|
||||
trackColor={{ false: colors.border, true: REWARD_ACCENT }}
|
||||
thumbColor="#fff"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{enabled && (
|
||||
<View style={{ borderTopWidth: 1, borderTopColor: colors.border, paddingHorizontal: spacing.l, paddingTop: spacing.l, paddingBottom: spacing.l, gap: spacing.l }}>
|
||||
|
||||
{/* Seuil */}
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={s.rowLabel}>Seuil de points</Text>
|
||||
<Text style={s.rowDesc}>
|
||||
Dès X points cumulés dans un type, la récompense est débloquée pour ce type.
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: screenWidth < 380 ? 52 : 64 }]}
|
||||
keyboardType="number-pad"
|
||||
value={String(r.threshold)}
|
||||
onChangeText={(v) => { const n = parseInt(v, 10); if (!isNaN(n) && n > 0) update({ threshold: n }); }}
|
||||
/>
|
||||
<Text style={s.thresholdSep}>pts</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Type */}
|
||||
<View>
|
||||
<Text style={[s.rowLabel, { marginBottom: spacing.s }]}>Type de récompense</Text>
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.s }}>
|
||||
{REWARD_TYPES.map((rt) => {
|
||||
const sel = r.type === rt.value;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={rt.value}
|
||||
onPress={() => update({ type: rt.value })}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||
borderColor: sel ? REWARD_ACCENT : colors.border,
|
||||
backgroundColor: sel ? REWARD_ACCENT + "20" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Ionicons name={rt.icon as any} size={14} color={sel ? REWARD_ACCENT : colors.textMuted} />
|
||||
<Text style={{ fontSize: 13, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{rt.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Description */}
|
||||
<View>
|
||||
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Description affichée au client</Text>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: "100%", textAlign: "left", paddingHorizontal: spacing.m, paddingVertical: spacing.s, height: 72, textAlignVertical: "top" }]}
|
||||
value={r.description}
|
||||
onChangeText={(v) => update({ description: v })}
|
||||
placeholder="Ex : Un produit 30€ de ton choix parmi les produits conditionnés en 30€"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
multiline
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Catégories éligibles */}
|
||||
<View>
|
||||
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles</Text>
|
||||
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
|
||||
Sélectionnez les catégories, puis pour chacune choisissez tous les produits ou une sélection.
|
||||
</Text>
|
||||
{allCategories.length === 0 ? (
|
||||
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucune catégorie disponible</Text>
|
||||
) : (
|
||||
<View style={{ gap: spacing.m }}>
|
||||
{allCategories.map((cat) => {
|
||||
const selected = isCatSelected(cat.name);
|
||||
const catColor = cat.color || REWARD_ACCENT;
|
||||
return (
|
||||
<View key={cat.name}>
|
||||
{/* Chip catégorie */}
|
||||
<TouchableOpacity
|
||||
onPress={() => toggleCategory(cat.name)}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||
borderRadius: borderRadius.full, borderWidth: 1.5,
|
||||
borderColor: selected ? catColor : colors.border,
|
||||
backgroundColor: selected ? catColor + "22" : "transparent",
|
||||
}}
|
||||
>
|
||||
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: catColor }} />
|
||||
<Text style={{ fontSize: 13, fontWeight: selected ? "700" : "400", color: selected ? catColor : colors.textMuted }}>
|
||||
{cat.name}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={selected ? "chevron-down" : "chevron-forward"}
|
||||
size={12}
|
||||
color={selected ? catColor : colors.textMuted}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Sélecteur produits (visible si catégorie sélectionnée) */}
|
||||
{selected && (
|
||||
<CategoryProductPicker
|
||||
catConfig={getCatConfig(cat.name)}
|
||||
products={productsByCategory[cat.name] ?? []}
|
||||
onChange={updateCatConfig}
|
||||
colors={colors}
|
||||
s={s}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Récapitulatif */}
|
||||
{r.category_configs.length > 0 && (
|
||||
<View style={{ backgroundColor: REWARD_ACCENT + "12", borderRadius: borderRadius.sm, borderLeftWidth: 3, borderLeftColor: REWARD_ACCENT, padding: spacing.m, gap: 4 }}>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: REWARD_ACCENT }}>Récapitulatif</Text>
|
||||
<Text style={{ fontSize: 13, color: colors.textPrimary }}>
|
||||
Dès <Text style={{ fontWeight: "700" }}>{r.threshold} pts</Text> par type →{" "}
|
||||
{REWARD_TYPES.find((x) => x.value === r.type)?.label}
|
||||
</Text>
|
||||
{r.description !== "" && (
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>"{r.description}"</Text>
|
||||
)}
|
||||
{r.category_configs.map((cfg) => (
|
||||
<Text key={cfg.category} style={{ fontSize: 12, color: colors.textSecondary }}>
|
||||
• {cfg.category}{cfg.amount > 0 ? ` (${cfg.amount}€)` : ""} : {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s) sélectionné(s)`}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const { colors } = useTheme();
|
||||
const { alert, showError, showSuccess, hideAlert } = useAlert();
|
||||
@@ -680,10 +1033,13 @@ export default function SettingsScreen() {
|
||||
telegram_2fa_enabled: false,
|
||||
delivery_mode: { mode: "single" as const, category_routes: [] },
|
||||
shop_name: "Milieu-Nantais",
|
||||
contact_telegram: "",
|
||||
points_reward: null,
|
||||
});
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [showIpnSecret, setShowIpnSecret] = useState(false);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [productsByCategory, setProductsByCategory] = useState<Record<string, Product[]>>({});
|
||||
const [livreurs, setLivreurs] = useState<string[]>([]);
|
||||
|
||||
// Refs pour l'auto-sauvegarde au départ de la page
|
||||
@@ -713,10 +1069,11 @@ export default function SettingsScreen() {
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
isLoaded.current = false;
|
||||
const [settingsRes, categoriesRes, livreursRes] = await Promise.all([
|
||||
const [settingsRes, categoriesRes, livreursRes, productsRes] = await Promise.all([
|
||||
getSettings(),
|
||||
getCategories(),
|
||||
getAvailableDeliveryPersons(),
|
||||
getAllProductsAdmin(),
|
||||
]);
|
||||
if (settingsRes.success && settingsRes.settings) {
|
||||
setSettings({
|
||||
@@ -725,6 +1082,7 @@ export default function SettingsScreen() {
|
||||
delivery_schedule: settingsRes.settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE,
|
||||
postal_zones: settingsRes.settings.postal_zones ?? DEFAULT_POSTAL_ZONES,
|
||||
delivery_mode: settingsRes.settings.delivery_mode ?? { mode: "single", category_routes: [] },
|
||||
points_reward: settingsRes.settings.points_reward ?? null,
|
||||
});
|
||||
}
|
||||
if (categoriesRes) {
|
||||
@@ -733,6 +1091,15 @@ export default function SettingsScreen() {
|
||||
if (livreursRes.success) {
|
||||
setLivreurs(livreursRes.livreurs.map((l: any) => l.username));
|
||||
}
|
||||
if (productsRes.success) {
|
||||
const byCategory: Record<string, Product[]> = {};
|
||||
for (const p of productsRes.data) {
|
||||
const cat = p.category || "";
|
||||
if (!byCategory[cat]) byCategory[cat] = [];
|
||||
byCategory[cat].push(p);
|
||||
}
|
||||
setProductsByCategory(byCategory);
|
||||
}
|
||||
setLoading(false);
|
||||
// Marquer comme chargé après un tick pour que le useEffect de settings ne
|
||||
// considère pas le setSettings initial comme une modification utilisateur
|
||||
@@ -1256,6 +1623,16 @@ export default function SettingsScreen() {
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Récompense centralisée par palier */}
|
||||
<CentralRewardSection
|
||||
reward={settings.points_reward ?? null}
|
||||
allCategories={categories}
|
||||
productsByCategory={productsByCategory}
|
||||
onChange={(reward) => setSettings((p) => ({ ...p, points_reward: reward }))}
|
||||
colors={colors}
|
||||
s={s}
|
||||
/>
|
||||
|
||||
{/* Horaires de livraison */}
|
||||
<DeliveryScheduleSection
|
||||
schedule={settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE}
|
||||
@@ -1486,6 +1863,30 @@ export default function SettingsScreen() {
|
||||
<Text style={{ color: "#10b981", fontSize: fontSize.sm }}>Bot configuré — @{settings.telegram_bot_username}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View>
|
||||
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Contact SAV Telegram</Text>
|
||||
<Text style={[s.rowDesc, { marginBottom: spacing.s }]}>
|
||||
Username du compte Telegram SAV (sans le @). Utilisé pour les boutons de contact client.
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
|
||||
<Text style={{ fontSize: fontSize.md, color: colors.textMuted }}>@</Text>
|
||||
<TextInput
|
||||
style={[s.input, { flex: 1 }]}
|
||||
value={settings.contact_telegram}
|
||||
onChangeText={(v) => setSettings((p) => ({ ...p, contact_telegram: v }))}
|
||||
placeholder="MonSAV"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
{settings.contact_telegram !== "" && (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s, marginTop: spacing.s, padding: spacing.m, backgroundColor: "#0088cc22", borderRadius: borderRadius.sm }}>
|
||||
<Ionicons name="paper-plane-outline" size={16} color="#0088cc" />
|
||||
<Text style={{ color: "#0088cc", fontSize: fontSize.sm }}>@{settings.contact_telegram}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user