chore: build
This commit is contained in:
+117
-20
@@ -1,4 +1,5 @@
|
||||
import apiClient from "./client";
|
||||
import { API_BASE_URL } from "./client";
|
||||
import type {
|
||||
ConfirmReceptionResponse,
|
||||
CheckoutCartResponse,
|
||||
@@ -12,7 +13,7 @@ import type {
|
||||
import { getToken } from "../auth/tokenStorage";
|
||||
import { extractUsernameFromToken } from "../auth/jwtUtils";
|
||||
|
||||
const V1 = `${process.env.EXPO_PUBLIC_API_URL ?? "https://mln-uber.club"}/api/v1`;
|
||||
const V1 = `${API_BASE_URL}/api/v1`;
|
||||
|
||||
export const getJwtUsername = async (): Promise<string | null> => {
|
||||
const token = await getToken();
|
||||
@@ -110,10 +111,6 @@ export const logoutUser = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PRODUCTS
|
||||
// ============================================
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -160,10 +157,6 @@ export const getProductById = async (id: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// CART
|
||||
// ============================================
|
||||
|
||||
export const getCart = async (username: string) => {
|
||||
const jwtUsername = await getJwtUsername();
|
||||
if (!jwtUsername || jwtUsername !== username) {
|
||||
@@ -247,10 +240,6 @@ export const clearCart = async (username: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ORDERS
|
||||
// ============================================
|
||||
|
||||
export const getMyOrders = async () => {
|
||||
try {
|
||||
const { data } = await apiClient.get(`${V1}/my-commands`);
|
||||
@@ -787,6 +776,8 @@ export interface PublicSettings {
|
||||
crypto_only: boolean;
|
||||
nowpayments_currencies: string[];
|
||||
telegram_notifications_enabled: boolean;
|
||||
two_fa_enabled: boolean;
|
||||
contact_telegram: string;
|
||||
}
|
||||
|
||||
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||
@@ -801,6 +792,8 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||
crypto_only: false,
|
||||
nowpayments_currencies: [],
|
||||
telegram_notifications_enabled: false,
|
||||
two_fa_enabled: false,
|
||||
contact_telegram: "",
|
||||
};
|
||||
try {
|
||||
const { data } = await apiClient.get(`${V1}/app-settings`);
|
||||
@@ -821,6 +814,8 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||
: [],
|
||||
telegram_notifications_enabled:
|
||||
data.telegram_notifications_enabled ?? false,
|
||||
two_fa_enabled: data.two_fa_enabled ?? false,
|
||||
contact_telegram: data.contact_telegram || "",
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
@@ -889,17 +884,119 @@ export const unlinkTelegram = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const get2FAStatus = async (): Promise<{
|
||||
two_fa_enabled: boolean;
|
||||
telegram_linked: boolean;
|
||||
admin_2fa_enabled: boolean;
|
||||
}> => {
|
||||
try {
|
||||
const { data } = await apiClient.get(`${V1}/two-fa/status`);
|
||||
return data;
|
||||
} catch {
|
||||
return {
|
||||
two_fa_enabled: false,
|
||||
telegram_linked: false,
|
||||
admin_2fa_enabled: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const toggle2FA = async (
|
||||
enabled: boolean,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const { data } = await apiClient.post(`${V1}/two-fa/toggle`, {
|
||||
enabled,
|
||||
});
|
||||
return { success: data.success ?? true };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: e?.response?.data?.error || "Erreur" };
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 🏆 POINTS — RÉCOMPENSES
|
||||
// ============================================
|
||||
|
||||
export type RewardCategoryConfig = {
|
||||
category: string;
|
||||
all_products: boolean;
|
||||
product_ids: number[];
|
||||
product_names: string[];
|
||||
amount: number;
|
||||
};
|
||||
|
||||
export type PointsPoolInfo = {
|
||||
key: string;
|
||||
name: string;
|
||||
points: number;
|
||||
rewards_earned: number;
|
||||
rewards_claimed: number;
|
||||
rewards_available: number;
|
||||
eligible_configs: RewardCategoryConfig[];
|
||||
};
|
||||
|
||||
export type RewardItemConfig = {
|
||||
product_id: number;
|
||||
product_name: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type PointsRewardConfig = {
|
||||
threshold: number;
|
||||
type: string;
|
||||
description: string;
|
||||
reward_items: RewardItemConfig[];
|
||||
};
|
||||
|
||||
export const getMyPointsRewards = async (): Promise<{
|
||||
success: boolean;
|
||||
enabled: boolean;
|
||||
pools: PointsPoolInfo[];
|
||||
reward: PointsRewardConfig | null;
|
||||
}> => {
|
||||
try {
|
||||
const { data } = await apiClient.get(`${V1}/points/rewards`);
|
||||
return {
|
||||
success: true,
|
||||
enabled: data.enabled ?? false,
|
||||
pools: data.pools ?? [],
|
||||
reward: data.reward ?? null,
|
||||
};
|
||||
} catch {
|
||||
return { success: false, enabled: false, pools: [], reward: null };
|
||||
}
|
||||
};
|
||||
|
||||
export const claimMyReward = async (poolKey: string): Promise<{
|
||||
success: boolean;
|
||||
description?: string;
|
||||
remaining_rewards?: number;
|
||||
product_added?: boolean;
|
||||
product_name?: string;
|
||||
error?: string;
|
||||
}> => {
|
||||
try {
|
||||
const { data } = await apiClient.post(`${V1}/points/claim`, { pool_key: poolKey });
|
||||
return {
|
||||
success: true,
|
||||
description: data.description,
|
||||
remaining_rewards: data.remaining_rewards,
|
||||
product_added: data.product_added,
|
||||
product_name: data.product_name,
|
||||
};
|
||||
} catch (e: any) {
|
||||
return { success: false, error: e?.response?.data?.error || "Erreur" };
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateOrderTotal = (order: any): number => {
|
||||
if (typeof order.total_prix === "number" && order.total_prix > 0) return order.total_prix;
|
||||
if (typeof order.total === "number" && order.total > 0) return order.total;
|
||||
if (typeof order.total_prix === "number" && order.total_prix > 0)
|
||||
return order.total_prix;
|
||||
if (Array.isArray(order.items) && order.items.length > 0) {
|
||||
return order.items.reduce((sum: number, item: any) => {
|
||||
return (
|
||||
sum +
|
||||
(item.prix || item.price || 0) *
|
||||
(item.quantite || item.quantity || 1)
|
||||
);
|
||||
return sum + (item.prix || item.price || 0) * (item.quantite || item.quantity || 1);
|
||||
}, 0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -356,6 +356,7 @@ export interface TrackingResponse {
|
||||
export interface ProductPrice {
|
||||
quantity: number;
|
||||
price: number;
|
||||
active_price?: boolean;
|
||||
}
|
||||
export interface Product {
|
||||
id: number;
|
||||
@@ -567,6 +568,7 @@ export interface CompletedOrder {
|
||||
status: string;
|
||||
adresse: string;
|
||||
total_prix: number;
|
||||
referral_used?: number;
|
||||
livreur_assign?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from "axios";
|
||||
import { getToken, getAdminToken } from "../auth/tokenStorage";
|
||||
|
||||
// Change this to your server IP/domain
|
||||
export const API_BASE_URL = "https://mln-uber.club";
|
||||
export const API_BASE_URL =
|
||||
process.env.EXPO_PUBLIC_API_URL ?? "https://mln-uber.club";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
@@ -26,7 +26,6 @@ apiClient.interceptors.request.use(async (config) => {
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor: handle common errors
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
|
||||
@@ -6,7 +6,6 @@ const USERNAME_KEY = "username";
|
||||
const ADMIN_USERNAME_KEY = "admin_username";
|
||||
const ROLE_KEY = "user_role";
|
||||
|
||||
// Client token
|
||||
export const getToken = () => AsyncStorage.getItem(TOKEN_KEY);
|
||||
export const setToken = (token: string) =>
|
||||
AsyncStorage.setItem(TOKEN_KEY, token);
|
||||
@@ -22,7 +21,6 @@ export const setUsername = (username: string) =>
|
||||
AsyncStorage.setItem(USERNAME_KEY, username);
|
||||
export const removeUsername = () => AsyncStorage.removeItem(USERNAME_KEY);
|
||||
|
||||
// Admin username
|
||||
export const getAdminUsername = () => AsyncStorage.getItem(ADMIN_USERNAME_KEY);
|
||||
export const setAdminUsername = (username: string) =>
|
||||
AsyncStorage.setItem(ADMIN_USERNAME_KEY, username);
|
||||
|
||||
@@ -13,7 +13,13 @@ import {
|
||||
|
||||
function getTextColor(hex: string): string {
|
||||
const h = hex.replace("#", "");
|
||||
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
||||
const full =
|
||||
h.length === 3
|
||||
? h
|
||||
.split("")
|
||||
.map((c) => c + c)
|
||||
.join("")
|
||||
: h;
|
||||
const r = parseInt(full.slice(0, 2), 16);
|
||||
const g = parseInt(full.slice(2, 4), 16);
|
||||
const b = parseInt(full.slice(4, 6), 16);
|
||||
@@ -36,19 +42,31 @@ interface ProductCardProps {
|
||||
category: string;
|
||||
stock: number;
|
||||
unit?: string;
|
||||
prices?: Array<{ quantity: number; price: number }>;
|
||||
coming_soon?: boolean;
|
||||
prices?: Array<{
|
||||
quantity: number;
|
||||
price: number;
|
||||
active_price?: boolean;
|
||||
}>;
|
||||
media?: Array<{ url: string; type: string }>;
|
||||
};
|
||||
onPress: () => void;
|
||||
categoryColor?: string;
|
||||
}
|
||||
|
||||
export default function ProductCard({ product, onPress, categoryColor }: ProductCardProps) {
|
||||
export default function ProductCard({
|
||||
product,
|
||||
onPress,
|
||||
categoryColor,
|
||||
}: ProductCardProps) {
|
||||
const { colors } = useTheme();
|
||||
const { addToCart } = useCart();
|
||||
const catColor = categoryColor ?? colors.accent;
|
||||
const isSoldOut = product.stock <= 0;
|
||||
const firstPrice = product.prices?.[0]?.price ?? null;
|
||||
const isComingSoon = product.coming_soon === true;
|
||||
const activePrices =
|
||||
product.prices?.filter((p) => p.active_price !== false) ?? [];
|
||||
const firstPrice = activePrices[0]?.price ?? null;
|
||||
|
||||
const imageMedia = product.media?.find((m) => m.type === "image");
|
||||
const videoMedia = product.media?.find((m) => m.type === "video");
|
||||
@@ -156,6 +174,13 @@ export default function ProductCard({ product, onPress, categoryColor }: Product
|
||||
<Text style={styles.soldOutText}>SOLD OUT</Text>
|
||||
</View>
|
||||
)}
|
||||
{isComingSoon && (
|
||||
<View style={styles.comingSoonOverlay}>
|
||||
<View style={styles.comingSoonBox}>
|
||||
<Text style={styles.comingSoonText}>BIENTÔT DISPONIBLE</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View
|
||||
@@ -210,13 +235,13 @@ export default function ProductCard({ product, onPress, categoryColor }: Product
|
||||
style={[
|
||||
styles.quickAddBtn,
|
||||
{ backgroundColor: catColor },
|
||||
isSoldOut && {
|
||||
(isSoldOut || isComingSoon) && {
|
||||
backgroundColor: colors.textMuted,
|
||||
opacity: 0.6,
|
||||
},
|
||||
]}
|
||||
onPress={handleQuickAdd}
|
||||
disabled={isSoldOut}
|
||||
disabled={isSoldOut || isComingSoon}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
@@ -227,7 +252,9 @@ export default function ProductCard({ product, onPress, categoryColor }: Product
|
||||
>
|
||||
{isSoldOut
|
||||
? "Rupture de stock"
|
||||
: "Ajouter rapidement"}
|
||||
: isComingSoon
|
||||
? "Bientôt disponible"
|
||||
: "Ajouter rapidement"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
@@ -267,7 +294,7 @@ export default function ProductCard({ product, onPress, categoryColor }: Product
|
||||
style={styles.pickerScroll}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{product.prices?.map((p) => (
|
||||
{activePrices.map((p) => (
|
||||
<TouchableOpacity
|
||||
key={p.quantity}
|
||||
style={[
|
||||
@@ -287,7 +314,8 @@ export default function ProductCard({ product, onPress, categoryColor }: Product
|
||||
{ color: colors.textWhite },
|
||||
]}
|
||||
>
|
||||
{p.quantity}{product.unit || "g"}
|
||||
{p.quantity}
|
||||
{product.unit || "g"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
@@ -449,6 +477,32 @@ const styles = StyleSheet.create({
|
||||
textShadowOffset: { width: 2, height: 2 },
|
||||
textShadowRadius: 6,
|
||||
},
|
||||
comingSoonOverlay: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
comingSoonBox: {
|
||||
backgroundColor: "rgba(0,0,0,0.8)",
|
||||
borderWidth: 4,
|
||||
borderColor: "rgba(34,197,94,0.95)",
|
||||
paddingHorizontal: 22,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
comingSoonText: {
|
||||
color: "rgba(34,197,94,0.95)",
|
||||
fontSize: 16,
|
||||
fontWeight: "900",
|
||||
letterSpacing: 2,
|
||||
textTransform: "uppercase",
|
||||
textShadowColor: "rgba(0,0,0,0.9)",
|
||||
textShadowOffset: { width: 2, height: 2 },
|
||||
textShadowRadius: 6,
|
||||
},
|
||||
info: { padding: spacing.m, borderTopWidth: 1 },
|
||||
name: {
|
||||
fontSize: fontSize.lg,
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function Modal({
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
behavior="padding"
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<Animated.View
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function ChangePasswordScreen() {
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={[styles.container, { backgroundColor: colors.bgPrimary }]}
|
||||
behavior="height"
|
||||
behavior="padding"
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -85,25 +85,89 @@ const LoginClient = () => {
|
||||
if (apiError) setApiError("");
|
||||
};
|
||||
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.bgSecondary },
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 16,
|
||||
},
|
||||
card: {
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
backgroundColor: colors.bgPrimary,
|
||||
},
|
||||
header: { alignItems: "center", marginBottom: 24 },
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
marginTop: 8,
|
||||
color: colors.textPrimary,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
textAlign: "center",
|
||||
color: colors.textMuted,
|
||||
},
|
||||
apiError: {
|
||||
backgroundColor: "#fee2e2",
|
||||
color: "#991b1b",
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
marginBottom: 16,
|
||||
textAlign: "center",
|
||||
},
|
||||
inputGroup: { marginBottom: 16 },
|
||||
label: { marginBottom: 4, fontWeight: "500", color: colors.textSecondary },
|
||||
inputWrapper: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.bgInput,
|
||||
},
|
||||
icon: { marginRight: 8 },
|
||||
input: { flex: 1, height: 40, color: colors.textPrimary },
|
||||
inputError: { borderColor: "#ef4444" },
|
||||
errorText: { color: "#ef4444", fontSize: 12, marginTop: 4 },
|
||||
eyeButton: { padding: 4 },
|
||||
submitButton: {
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
},
|
||||
submitText: { color: colors.white, fontWeight: "600", fontSize: 16 },
|
||||
}),
|
||||
[colors],
|
||||
);
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={[styles.container, { backgroundColor: colors.bgSecondary }]}
|
||||
behavior="height"
|
||||
style={styles.container}
|
||||
behavior="padding"
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.card, { backgroundColor: colors.bgPrimary }]}>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.header}>
|
||||
<FontAwesome name="user-circle" size={48} color="#7c3aed" />
|
||||
<Text style={[styles.title, { color: colors.textWhite }]}>
|
||||
<FontAwesome name="user-circle" size={48} color={colors.accent} />
|
||||
<Text style={styles.title}>
|
||||
Connexion Client
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.subtitle, { color: colors.textMuted }]}
|
||||
>
|
||||
<Text style={styles.subtitle}>
|
||||
Accédez à votre espace personnel
|
||||
</Text>
|
||||
</View>
|
||||
@@ -113,20 +177,10 @@ const LoginClient = () => {
|
||||
) : null}
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Text
|
||||
style={[styles.label, { color: colors.textSecondary }]}
|
||||
>
|
||||
<Text style={styles.label}>
|
||||
Username Telegram
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.bgInput,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.inputWrapper}>
|
||||
<Feather
|
||||
name="user"
|
||||
size={20}
|
||||
@@ -134,11 +188,7 @@ const LoginClient = () => {
|
||||
style={styles.icon}
|
||||
/>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{ color: colors.textWhite },
|
||||
errors.username && styles.inputError,
|
||||
]}
|
||||
style={[styles.input, errors.username && styles.inputError]}
|
||||
placeholder="Votre username"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={formData.username}
|
||||
@@ -156,20 +206,10 @@ const LoginClient = () => {
|
||||
</View>
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Text
|
||||
style={[styles.label, { color: colors.textSecondary }]}
|
||||
>
|
||||
<Text style={styles.label}>
|
||||
Mot de passe
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputWrapper,
|
||||
{
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.bgInput,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.inputWrapper}>
|
||||
<Feather
|
||||
name="lock"
|
||||
size={20}
|
||||
@@ -177,11 +217,7 @@ const LoginClient = () => {
|
||||
style={styles.icon}
|
||||
/>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{ color: colors.textWhite },
|
||||
errors.password && styles.inputError,
|
||||
]}
|
||||
style={[styles.input, errors.password && styles.inputError]}
|
||||
placeholder="••••••"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
secureTextEntry={!showPassword}
|
||||
@@ -215,7 +251,7 @@ const LoginClient = () => {
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="white" />
|
||||
<ActivityIndicator color={colors.white} />
|
||||
) : (
|
||||
<Text style={styles.submitText}>Se connecter</Text>
|
||||
)}
|
||||
@@ -227,52 +263,3 @@ const LoginClient = () => {
|
||||
};
|
||||
|
||||
export default LoginClient;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 16,
|
||||
},
|
||||
card: { width: "100%", maxWidth: 400, borderRadius: 16, padding: 24 },
|
||||
header: { alignItems: "center", marginBottom: 24 },
|
||||
title: { fontSize: 24, fontWeight: "bold", marginTop: 8 },
|
||||
subtitle: { fontSize: 14, marginTop: 4, textAlign: "center" },
|
||||
apiError: {
|
||||
backgroundColor: "#fee2e2",
|
||||
color: "#991b1b",
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
marginBottom: 16,
|
||||
textAlign: "center",
|
||||
},
|
||||
inputGroup: { marginBottom: 16 },
|
||||
label: { marginBottom: 4, fontWeight: "500" },
|
||||
inputWrapper: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
icon: { marginRight: 8 },
|
||||
input: { flex: 1, height: 40 },
|
||||
inputError: { borderColor: "#ef4444" },
|
||||
errorText: { color: "#ef4444", fontSize: 12, marginTop: 4 },
|
||||
eyeButton: { padding: 4 },
|
||||
submitButton: {
|
||||
backgroundColor: "#7c3aed",
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
},
|
||||
submitText: { color: "white", fontWeight: "600", fontSize: 16 },
|
||||
signup: { marginTop: 16, alignItems: "center" },
|
||||
signupText: {},
|
||||
signupLink: { color: "#a78bfa", fontWeight: "600" },
|
||||
});
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
getReferralBalance,
|
||||
getPublicSettings,
|
||||
getCryptoPaymentStatus,
|
||||
getTelegramStatus,
|
||||
getMyProfile,
|
||||
} from "../../api/api";
|
||||
import type { CryptoPaymentStatus } from "../../api/api";
|
||||
import type { ClientStackParamList } from "../../navigation/types";
|
||||
@@ -57,6 +59,7 @@ export default function CheckoutScreen() {
|
||||
// Notif telegram
|
||||
const [telegramNotificationEnabled, setTelegramNotificationEnabled] =
|
||||
useState(false);
|
||||
const [telegramLinked, setTelegramLinked] = useState(false);
|
||||
|
||||
// Crypto
|
||||
const [cryptoEnabled, setCryptoEnabled] = useState(false);
|
||||
@@ -105,6 +108,18 @@ export default function CheckoutScreen() {
|
||||
if (savedPhone) setTelephone(savedPhone);
|
||||
});
|
||||
|
||||
getMyProfile().then((res) => {
|
||||
if (res.success && res.client) {
|
||||
if (res.client.nom) setNom((prev) => prev || res.client.nom);
|
||||
if (res.client.prenom) setPrenom((prev) => prev || res.client.prenom);
|
||||
if (res.client.telephone) setTelephone((prev) => prev || res.client.telephone);
|
||||
}
|
||||
});
|
||||
|
||||
getTelegramStatus().then((res) => {
|
||||
if (res.linked) setTelegramLinked(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
};
|
||||
@@ -657,9 +672,12 @@ export default function CheckoutScreen() {
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
behavior="padding"
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.content}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* Résumé commande */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
@@ -687,9 +705,22 @@ export default function CheckoutScreen() {
|
||||
<View style={styles.summaryTotal}>
|
||||
<Text style={styles.summaryTotalLabel}>Total</Text>
|
||||
<Text style={styles.summaryTotalValue}>
|
||||
{cartTotal.toFixed(2)} €
|
||||
{(useReferral && referralBalance > 0
|
||||
? Math.max(0, cartTotal - referralBalance)
|
||||
: cartTotal
|
||||
).toFixed(2)} €
|
||||
</Text>
|
||||
</View>
|
||||
{useReferral && referralBalance > 0 && (
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", marginTop: 4 }}>
|
||||
<Text style={{ color: colors.textMuted, fontSize: fontSize.sm }}>
|
||||
Dont crédit parrainage
|
||||
</Text>
|
||||
<Text style={{ color: colors.success, fontSize: fontSize.sm }}>
|
||||
-{Math.min(referralBalance, cartTotal).toFixed(2)} €
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1011,7 +1042,7 @@ export default function CheckoutScreen() {
|
||||
)}
|
||||
|
||||
{/* Note Telegram */}
|
||||
{telegramNotificationEnabled && (
|
||||
{telegramNotificationEnabled && !telegramLinked && (
|
||||
<View style={styles.telegramNote}>
|
||||
<Ionicons
|
||||
name="paper-plane-outline"
|
||||
@@ -1166,7 +1197,10 @@ export default function CheckoutScreen() {
|
||||
<View style={styles.recapTotalRow}>
|
||||
<Text style={styles.recapTotalLabel}>Total</Text>
|
||||
<Text style={styles.recapTotalValue}>
|
||||
{cartTotal.toFixed(2)} €
|
||||
{(useReferral && referralBalance > 0
|
||||
? Math.max(0, cartTotal - referralBalance)
|
||||
: cartTotal
|
||||
).toFixed(2)} €
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -315,10 +315,12 @@ export default function OrderDetailsScreen() {
|
||||
const currentIdx = STATUS_INDEX[status] ?? -1;
|
||||
const isCancelled = status === "cancelled";
|
||||
const address = order.delivery_address || order.adresse || "N/A";
|
||||
const total =
|
||||
const rawTotal =
|
||||
order.total ||
|
||||
order.total_prix ||
|
||||
products.reduce((s: number, p: any) => s + (p.prix || p.price || 0), 0);
|
||||
const referralUsed: number = order.referral_used || 0;
|
||||
const total = rawTotal - referralUsed;
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
@@ -524,6 +526,14 @@ export default function OrderDetailsScreen() {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
{referralUsed > 0 && (
|
||||
<View style={styles.totalRow}>
|
||||
<Text style={styles.totalLabel}>Crédit parrainage</Text>
|
||||
<Text style={[styles.totalValue, { color: colors.accent }]}>
|
||||
-{formatPrice(referralUsed)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.totalRow}>
|
||||
<Text style={styles.totalLabel}>Total</Text>
|
||||
<Text style={styles.totalValue}>{formatPrice(total)}</Text>
|
||||
|
||||
@@ -15,10 +15,12 @@ import {
|
||||
getMyPenalties,
|
||||
getPublicSettings,
|
||||
getReferralBalance,
|
||||
getMyPointsRewards,
|
||||
claimMyReward,
|
||||
formatOrderDate,
|
||||
formatPrice,
|
||||
} from "../../api/api";
|
||||
import type { PublicSettings } from "../../api/api";
|
||||
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig, RewardItemConfig } from "../../api/api";
|
||||
import type {
|
||||
CompletedOrder,
|
||||
ClientStats,
|
||||
@@ -45,18 +47,26 @@ export default function OrderHistoryScreen() {
|
||||
const [orders, setOrders] = useState<CompletedOrder[]>([]);
|
||||
const [stats, setStats] = useState<ClientStats | null>(null);
|
||||
const [penalties, setPenalties] = useState<PenaltyInfo | null>(null);
|
||||
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: false, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, nowpayments_currencies: [], crypto_only: false, telegram_notifications_enabled: false });
|
||||
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: false, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, nowpayments_currencies: [], crypto_only: false, telegram_notifications_enabled: false, two_fa_enabled: false, contact_telegram: "" });
|
||||
const [referralBalance, setReferralBalance] = useState(0);
|
||||
const [pointsRewards, setPointsRewards] = useState<{
|
||||
enabled: boolean;
|
||||
pools: PointsPoolInfo[];
|
||||
reward: PointsRewardConfig | null;
|
||||
} | null>(null);
|
||||
const [claimingPool, setClaimingPool] = useState<string | null>(null);
|
||||
const [claimFeedback, setClaimFeedback] = useState<{ pool: string; type: "success" | "error"; text: string } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [histRes, penRes, settings, refRes] = await Promise.all([
|
||||
const [histRes, penRes, settings, refRes, rewardsRes] = await Promise.all([
|
||||
getMyCompletedOrders(),
|
||||
getMyPenalties(),
|
||||
getPublicSettings(),
|
||||
getReferralBalance(),
|
||||
getMyPointsRewards(),
|
||||
]);
|
||||
if (histRes.success) {
|
||||
setOrders(histRes.commands || []);
|
||||
@@ -69,6 +79,9 @@ export default function OrderHistoryScreen() {
|
||||
setReferralBalance(refRes.balance);
|
||||
}
|
||||
setAppSettings(settings);
|
||||
if (rewardsRes.success && rewardsRes.enabled) {
|
||||
setPointsRewards(rewardsRes);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
@@ -77,6 +90,22 @@ export default function OrderHistoryScreen() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClaim = async (poolKey: string) => {
|
||||
setClaimingPool(poolKey);
|
||||
setClaimFeedback(null);
|
||||
const res = await claimMyReward(poolKey);
|
||||
setClaimingPool(null);
|
||||
if (res.success) {
|
||||
const text = res.product_added && res.product_name
|
||||
? `🎁 ${res.product_name} ajouté à votre panier ! Commandez au moins un produit pour en profiter.`
|
||||
: res.description || "Récompense réclamée !";
|
||||
setClaimFeedback({ pool: poolKey, type: "success", text });
|
||||
getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); });
|
||||
} else {
|
||||
setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" });
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setLoading(true);
|
||||
@@ -132,6 +161,107 @@ export default function OrderHistoryScreen() {
|
||||
letterSpacing: 1,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
rewardsSection: {
|
||||
backgroundColor: colors.bgCard,
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: "#f59e0b44",
|
||||
padding: spacing.m,
|
||||
marginBottom: spacing.l,
|
||||
},
|
||||
rewardsSectionTitle: {
|
||||
color: "#f59e0b",
|
||||
fontSize: fontSize.xs,
|
||||
fontWeight: fontWeight.bold,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
rewardsSectionDesc: {
|
||||
color: colors.textMuted,
|
||||
fontSize: fontSize.xs,
|
||||
fontStyle: "italic",
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
rewardPoolCard: {
|
||||
backgroundColor: "rgba(245,158,11,0.07)",
|
||||
borderRadius: borderRadius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(245,158,11,0.2)",
|
||||
padding: spacing.m,
|
||||
marginTop: spacing.s,
|
||||
},
|
||||
rewardPoolName: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: fontWeight.semibold,
|
||||
},
|
||||
rewardPoolPts: {
|
||||
color: "#f59e0b",
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: fontWeight.bold,
|
||||
},
|
||||
rewardAmountBadge: {
|
||||
backgroundColor: "rgba(16,185,129,0.15)",
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingHorizontal: spacing.s,
|
||||
paddingVertical: 2,
|
||||
flexDirection: "row" as const,
|
||||
},
|
||||
rewardAmountText: {
|
||||
color: "#10b981",
|
||||
fontSize: fontSize.xs,
|
||||
fontWeight: fontWeight.bold,
|
||||
},
|
||||
progressBarBg: {
|
||||
height: 5,
|
||||
backgroundColor: "rgba(245,158,11,0.15)",
|
||||
borderRadius: 3,
|
||||
overflow: "hidden",
|
||||
marginVertical: spacing.xs,
|
||||
},
|
||||
progressBarFill: {
|
||||
height: "100%",
|
||||
backgroundColor: "#f59e0b",
|
||||
borderRadius: 3,
|
||||
},
|
||||
rewardAvailable: {
|
||||
color: "#10b981",
|
||||
fontSize: fontSize.xs,
|
||||
fontWeight: fontWeight.semibold,
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
rewardRemaining: {
|
||||
color: colors.textMuted,
|
||||
fontSize: fontSize.xs,
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
feedbackSuccess: {
|
||||
color: "#10b981",
|
||||
fontSize: fontSize.xs,
|
||||
fontStyle: "italic",
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
feedbackError: {
|
||||
color: "#ef4444",
|
||||
fontSize: fontSize.xs,
|
||||
fontStyle: "italic",
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
claimBtn: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: spacing.xs,
|
||||
backgroundColor: "#f59e0b",
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingVertical: spacing.s,
|
||||
paddingHorizontal: spacing.m,
|
||||
},
|
||||
claimBtnText: {
|
||||
color: "#fff",
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: fontWeight.bold,
|
||||
},
|
||||
referralBtn: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
@@ -342,6 +472,86 @@ export default function OrderHistoryScreen() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Section récompenses */}
|
||||
{pointsRewards?.enabled && pointsRewards.reward && pointsRewards.pools.length > 0 && (
|
||||
<View style={styles.rewardsSection}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, marginBottom: spacing.xs }}>
|
||||
<Ionicons name="trophy-outline" size={14} color="#f59e0b" />
|
||||
<Text style={styles.rewardsSectionTitle}>Récompenses</Text>
|
||||
</View>
|
||||
{pointsRewards.reward.description !== "" && (
|
||||
<Text style={styles.rewardsSectionDesc}>{pointsRewards.reward.description}</Text>
|
||||
)}
|
||||
{(pointsRewards.reward.reward_items ?? []).filter((it) => it.price > 0 || it.product_name).length > 0 && (
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, marginBottom: spacing.s }}>
|
||||
{(pointsRewards.reward.reward_items ?? []).map((it: RewardItemConfig, idx: number) => (
|
||||
<View key={idx} style={[styles.rewardAmountBadge, { flexDirection: "row", alignItems: "center", gap: 4 }]}>
|
||||
<Ionicons name="gift-outline" size={11} color="#f59e0b" />
|
||||
<Text style={styles.rewardAmountText}>
|
||||
{it.product_name}{it.quantity > 0 && it.quantity !== 1 ? ` ×${it.quantity}` : ""}{it.price > 0 ? ` — ${it.price}€` : ""}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{pointsRewards.pools.map((pool) => {
|
||||
const threshold = pointsRewards.reward!.threshold;
|
||||
const progress = Math.min(1, (pool.points % threshold) / threshold);
|
||||
const remaining = threshold - (pool.points % threshold);
|
||||
const isClaiming = claimingPool === pool.key;
|
||||
const feedback = claimFeedback?.pool === pool.key ? claimFeedback : null;
|
||||
return (
|
||||
<View key={pool.key} style={styles.rewardPoolCard}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", marginBottom: spacing.xs }}>
|
||||
<Text style={styles.rewardPoolName}>{pool.name}</Text>
|
||||
<Text style={styles.rewardPoolPts}>{pool.points} pts</Text>
|
||||
</View>
|
||||
{pool.eligible_configs.length > 0 && (
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4, marginBottom: spacing.xs }}>
|
||||
{pool.eligible_configs.flatMap((cfg) =>
|
||||
cfg.all_products
|
||||
? [<View key={cfg.category} style={styles.rewardAmountBadge}>
|
||||
<Text style={styles.rewardAmountText}>{cfg.category}</Text>
|
||||
</View>]
|
||||
: (cfg.product_names ?? []).map((name) => (
|
||||
<View key={`${cfg.category}-${name}`} style={styles.rewardAmountBadge}>
|
||||
<Text style={styles.rewardAmountText}>{name}</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.progressBarBg}>
|
||||
<View style={[styles.progressBarFill, { width: `${Math.round(progress * 100)}%` as any }]} />
|
||||
</View>
|
||||
<Text style={pool.rewards_available > 0 ? styles.rewardAvailable : styles.rewardRemaining}>
|
||||
{pool.rewards_available > 0
|
||||
? `${pool.rewards_available} récompense${pool.rewards_available > 1 ? "s" : ""} disponible${pool.rewards_available > 1 ? "s" : ""}`
|
||||
: `Encore ${remaining} pts pour une récompense`}
|
||||
</Text>
|
||||
{feedback && (
|
||||
<Text style={feedback.type === "success" ? styles.feedbackSuccess : styles.feedbackError}>
|
||||
{feedback.text}
|
||||
</Text>
|
||||
)}
|
||||
{pool.rewards_available > 0 && (
|
||||
<TouchableOpacity
|
||||
style={[styles.claimBtn, isClaiming && { opacity: 0.5 }]}
|
||||
onPress={() => handleClaim(pool.key)}
|
||||
disabled={isClaiming}
|
||||
>
|
||||
<Ionicons name="gift-outline" size={14} color="#fff" />
|
||||
<Text style={styles.claimBtnText}>
|
||||
{isClaiming ? "..." : "Réclamer ma récompense"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Bouton parrainage — visible seulement si activé dans les settings */}
|
||||
{appSettings.referral_enabled && <TouchableOpacity
|
||||
style={styles.referralBtn}
|
||||
@@ -432,7 +642,7 @@ export default function OrderHistoryScreen() {
|
||||
)}
|
||||
<View style={styles.orderFooter}>
|
||||
<Text style={styles.orderTotal}>
|
||||
{formatPrice(order.total_prix || 0)}
|
||||
{formatPrice((order.total_prix || 0) - (order.referral_used || 0))}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
|
||||
@@ -385,7 +385,8 @@ export default function OrderTrackingScreen() {
|
||||
contentContainerStyle={styles.list}
|
||||
renderItem={({ item: order }) => {
|
||||
const expanded = expandedId === order.id;
|
||||
const total = calculateOrderTotal(order);
|
||||
const gross = calculateOrderTotal(order);
|
||||
const total = Math.max(0, gross - (order.referral_used ?? 0));
|
||||
const progress = STATUS_PROGRESS[order.status] || 0;
|
||||
const track = tracking[order.id];
|
||||
const eta = etas[order.id];
|
||||
@@ -444,9 +445,16 @@ export default function OrderTrackingScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.cardFooter}>
|
||||
<Text style={styles.cardTotal}>
|
||||
{formatPrice(total)}
|
||||
</Text>
|
||||
<View>
|
||||
<Text style={styles.cardTotal}>
|
||||
{formatPrice(total)}
|
||||
</Text>
|
||||
{(order.referral_used ?? 0) > 0 && (
|
||||
<Text style={{ fontSize: 11, color: colors.success, marginTop: 2 }}>
|
||||
dont -{(order.referral_used as number).toFixed(2)} € parrainage
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Ionicons
|
||||
name={
|
||||
expanded
|
||||
|
||||
@@ -5,27 +5,36 @@ import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Linking,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { spacing, borderRadius, fontSize, fontWeight } from "../../theme";
|
||||
import { getReferralBalance } from "../../api/api";
|
||||
import { getReferralBalance, getPublicSettings } from "../../api/api";
|
||||
|
||||
export default function ParrainageScreen() {
|
||||
const { colors } = useTheme();
|
||||
const [balance, setBalance] = useState<number>(0);
|
||||
const [contactTelegram, setContactTelegram] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
getReferralBalance().then((res) => {
|
||||
if (res.success) setBalance(res.balance);
|
||||
});
|
||||
getPublicSettings().then((settings) => {
|
||||
if (settings.contact_telegram) {
|
||||
setContactTelegram(settings.contact_telegram);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
icon: "chatbubble-ellipses-outline" as const,
|
||||
title: "1. Contacte-nous sur Telegram",
|
||||
desc: "Envoie un message à @milieu_nantais en indiquant ton username et le username de la personne que tu as parrainée.",
|
||||
desc: contactTelegram
|
||||
? `Envoie un message à @${contactTelegram} en indiquant ton username et le username de la personne que tu as parrainée.`
|
||||
: "Envoie-nous un message sur Telegram en indiquant ton username et le username de la personne que tu as parrainée.",
|
||||
},
|
||||
{
|
||||
icon: "checkmark-circle-outline" as const,
|
||||
@@ -152,6 +161,12 @@ export default function ParrainageScreen() {
|
||||
[colors],
|
||||
);
|
||||
|
||||
const handleTelegramPress = () => {
|
||||
if (contactTelegram) {
|
||||
Linking.openURL(`https://t.me/${contactTelegram}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
{/* Solde actuel */}
|
||||
@@ -190,18 +205,14 @@ export default function ParrainageScreen() {
|
||||
</View>
|
||||
|
||||
{/* Bouton Telegram */}
|
||||
<View
|
||||
style={styles.telegramBtn}
|
||||
// Utiliser TouchableOpacity si besoin d'interaction
|
||||
>
|
||||
<Ionicons name="paper-plane-outline" size={20} color="#fff" />
|
||||
<Text
|
||||
style={styles.telegramText}
|
||||
onPress={() => Linking.openURL("https://t.me/milieu_nantais")}
|
||||
>
|
||||
Contacter @milieu_nantais
|
||||
</Text>
|
||||
</View>
|
||||
{contactTelegram ? (
|
||||
<TouchableOpacity style={styles.telegramBtn} onPress={handleTelegramPress}>
|
||||
<Ionicons name="paper-plane-outline" size={20} color="#fff" />
|
||||
<Text style={styles.telegramText}>
|
||||
Contacter @{contactTelegram}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,10 +56,13 @@ export default function ProductDetailScreen() {
|
||||
const fixedProduct = {
|
||||
...p,
|
||||
prices:
|
||||
p.prices?.map((pr: any) => ({
|
||||
quantity: parseFloat(String(pr.quantity)),
|
||||
price: parseFloat(String(pr.price)),
|
||||
})) || [],
|
||||
p.prices
|
||||
?.filter((pr: any) => pr.active_price !== false)
|
||||
.map((pr: any) => ({
|
||||
quantity: parseFloat(String(pr.quantity)),
|
||||
price: parseFloat(String(pr.price)),
|
||||
active_price: pr.active_price,
|
||||
})) || [],
|
||||
};
|
||||
setProduct(fixedProduct);
|
||||
if (fixedProduct.prices.length > 0) {
|
||||
@@ -67,7 +70,9 @@ export default function ProductDetailScreen() {
|
||||
setSelectedPrice(fixedProduct.prices[0].price);
|
||||
}
|
||||
const matched = categories.find(
|
||||
(c) => c.name.toLowerCase() === (p.category || "").toLowerCase(),
|
||||
(c) =>
|
||||
c.name.toLowerCase() ===
|
||||
(p.category || "").toLowerCase(),
|
||||
);
|
||||
if (matched?.color) setCatColor(matched.color);
|
||||
} else {
|
||||
@@ -108,11 +113,19 @@ export default function ProductDetailScreen() {
|
||||
|
||||
const catTextColor = (() => {
|
||||
const h = catColor.replace("#", "");
|
||||
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
||||
const full =
|
||||
h.length === 3
|
||||
? h
|
||||
.split("")
|
||||
.map((c) => c + c)
|
||||
.join("")
|
||||
: h;
|
||||
const r = parseInt(full.slice(0, 2), 16);
|
||||
const g = parseInt(full.slice(2, 4), 16);
|
||||
const b = parseInt(full.slice(4, 6), 16);
|
||||
return (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
|
||||
return (r * 299 + g * 587 + b * 114) / 1000 > 128
|
||||
? "#000000"
|
||||
: "#ffffff";
|
||||
})();
|
||||
|
||||
const overlayBg = isDark ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)";
|
||||
@@ -206,6 +219,32 @@ export default function ProductDetailScreen() {
|
||||
textShadowOffset: { width: 2, height: 2 },
|
||||
textShadowRadius: 12,
|
||||
},
|
||||
comingSoonBadge: {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: [
|
||||
{ translateX: -80 },
|
||||
{ translateY: -30 },
|
||||
{ rotate: "-15deg" },
|
||||
],
|
||||
backgroundColor: "rgba(0,0,0,0.8)",
|
||||
borderWidth: 4,
|
||||
borderColor: "rgba(34,197,94,0.95)",
|
||||
paddingHorizontal: 40,
|
||||
paddingVertical: 16,
|
||||
elevation: 10,
|
||||
},
|
||||
comingSoonText: {
|
||||
color: "rgba(34,197,94,0.95)",
|
||||
fontSize: 28,
|
||||
fontWeight: "900",
|
||||
letterSpacing: 4,
|
||||
textTransform: "uppercase",
|
||||
textShadowColor: "rgba(0,0,0,0.9)",
|
||||
textShadowOffset: { width: 2, height: 2 },
|
||||
textShadowRadius: 12,
|
||||
},
|
||||
videoBtn: {
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
@@ -467,6 +506,7 @@ export default function ProductDetailScreen() {
|
||||
}
|
||||
|
||||
const isOutOfStock = product.stock === 0;
|
||||
const isComingSoon = product.coming_soon === true;
|
||||
const hasValidPrices = product.prices && product.prices.length > 0;
|
||||
const imageMedia = product.media?.find((m) => m.type === "image");
|
||||
const videoMedia = product.media?.find((m) => m.type === "video");
|
||||
@@ -514,6 +554,13 @@ export default function ProductDetailScreen() {
|
||||
<Text style={styles.soldOutText}>SOLD OUT</Text>
|
||||
</View>
|
||||
)}
|
||||
{isComingSoon && (
|
||||
<View style={styles.comingSoonBadge}>
|
||||
<Text style={styles.comingSoonText}>
|
||||
COMMING SOON
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{videoUri && !isOutOfStock && (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
@@ -538,7 +585,8 @@ export default function ProductDetailScreen() {
|
||||
<View style={styles.priceIndicator} />
|
||||
<Text style={styles.priceText}>
|
||||
{selectedPrice.toFixed(2)} €{" "}
|
||||
{selectedGrams && `pour ${selectedGrams}${product.unit || "g"}`}
|
||||
{selectedGrams &&
|
||||
`pour ${selectedGrams}${product.unit || "g"}`}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -580,25 +628,34 @@ export default function ProductDetailScreen() {
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.addToCartBtn,
|
||||
(isOutOfStock || selectedGrams === null) &&
|
||||
(isOutOfStock ||
|
||||
isComingSoon ||
|
||||
selectedGrams === null) &&
|
||||
styles.addToCartBtnDisabled,
|
||||
]}
|
||||
onPress={handleAddToCart}
|
||||
disabled={
|
||||
isOutOfStock || selectedGrams === null || adding
|
||||
isOutOfStock ||
|
||||
isComingSoon ||
|
||||
selectedGrams === null ||
|
||||
adding
|
||||
}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.addToCartText,
|
||||
(isOutOfStock || selectedGrams === null) &&
|
||||
(isOutOfStock ||
|
||||
isComingSoon ||
|
||||
selectedGrams === null) &&
|
||||
styles.addToCartTextDisabled,
|
||||
]}
|
||||
>
|
||||
{isOutOfStock
|
||||
? "Rupture de stock"
|
||||
: "Ajouter au panier"}
|
||||
: isComingSoon
|
||||
? "Bientôt disponible"
|
||||
: "Ajouter au panier"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -654,7 +711,8 @@ export default function ProductDetailScreen() {
|
||||
},
|
||||
]}
|
||||
>
|
||||
{p.quantity}{product.unit || "g"}
|
||||
{p.quantity}
|
||||
{product.unit || "g"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Switch,
|
||||
Alert,
|
||||
Modal,
|
||||
KeyboardAvoidingView,
|
||||
@@ -14,10 +15,10 @@ import {
|
||||
Linking,
|
||||
} from "react-native";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useFocusEffect, useNavigation } from "@react-navigation/native";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, changePassword } from "../../api/api";
|
||||
import { getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, changePassword, get2FAStatus, toggle2FA, getPublicSettings } from "../../api/api";
|
||||
import TextInput from "../../components/ui/TextInput";
|
||||
import Button from "../../components/ui/Button";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
@@ -29,6 +30,7 @@ const STORAGE_SIGNAL = "profile_signal_pseudo";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const { colors } = useTheme();
|
||||
const navigation = useNavigation<any>();
|
||||
|
||||
// Données compte (backend)
|
||||
const [nom, setNom] = useState("");
|
||||
@@ -49,11 +51,29 @@ export default function ProfileScreen() {
|
||||
const [telegramEnabled, setTelegramEnabled] = useState(false);
|
||||
const [telegramLoading, setTelegramLoading] = useState(false);
|
||||
|
||||
// Modal confirmation infos par défaut
|
||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||
// 2FA
|
||||
const [twoFAEnabled, setTwoFAEnabled] = useState(false);
|
||||
const [twoFAAdminEnabled, setTwoFAAdminEnabled] = useState(false);
|
||||
const [twoFALoading, setTwoFALoading] = useState(false);
|
||||
|
||||
// Modals confirmation sauvegarde
|
||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||
const [showConfirmContactModal, setShowConfirmContactModal] = useState(false);
|
||||
const [showConfirmAddressModal, setShowConfirmAddressModal] = useState(false);
|
||||
|
||||
// Modal succès / erreur générique
|
||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||
const [successTitle, setSuccessTitle] = useState("");
|
||||
const [successMsg, setSuccessMsg] = useState("");
|
||||
const [showErrorModal, setShowErrorModal] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
|
||||
// Modal changement de mot de passe
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
|
||||
// Modals Telegram unlink
|
||||
const [showTelegramUnlinkModal, setShowTelegramUnlinkModal] = useState(false);
|
||||
const [showTelegramSuccessModal, setShowTelegramSuccessModal] = useState(false);
|
||||
const [currentPwd, setCurrentPwd] = useState("");
|
||||
const [newPwd, setNewPwd] = useState("");
|
||||
const [confirmPwd, setConfirmPwd] = useState("");
|
||||
@@ -64,15 +84,19 @@ export default function ProfileScreen() {
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoadingProfile(true);
|
||||
const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus] = await Promise.all([
|
||||
const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus, twoFAStatus, pubSettings] = await Promise.all([
|
||||
AsyncStorage.getItem(STORAGE_ADDRESS),
|
||||
AsyncStorage.getItem(STORAGE_PHONE),
|
||||
AsyncStorage.getItem(STORAGE_SIGNAL),
|
||||
getMyProfile(),
|
||||
getTelegramStatus(),
|
||||
get2FAStatus(),
|
||||
getPublicSettings(),
|
||||
]);
|
||||
setTelegramLinked(tgStatus.linked);
|
||||
setTelegramEnabled(tgStatus.enabled);
|
||||
setTwoFAEnabled(twoFAStatus.two_fa_enabled);
|
||||
setTwoFAAdminEnabled(pubSettings.two_fa_enabled);
|
||||
|
||||
if (savedAddress !== null) setDefaultAddress(savedAddress);
|
||||
if (savedPhone !== null) setDefaultPhone(savedPhone);
|
||||
@@ -103,7 +127,17 @@ export default function ProfileScreen() {
|
||||
AsyncStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim()),
|
||||
]);
|
||||
setShowSaveModal(false);
|
||||
Alert.alert("Enregistré", "Informations par défaut sauvegardées");
|
||||
setSuccessTitle("Informations sauvegardées");
|
||||
setSuccessMsg("Téléphone et pseudo Signal seront pré-remplis lors de vos prochaines commandes.");
|
||||
setShowSuccessModal(true);
|
||||
};
|
||||
|
||||
const saveAddress = async () => {
|
||||
setShowConfirmAddressModal(false);
|
||||
await AsyncStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
|
||||
setSuccessTitle("Adresse sauvegardée");
|
||||
setSuccessMsg("Votre adresse par défaut sera pré-remplie à la prochaine commande.");
|
||||
setShowSuccessModal(true);
|
||||
};
|
||||
|
||||
const handleLinkTelegram = async () => {
|
||||
@@ -124,35 +158,32 @@ export default function ProfileScreen() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleUnlinkTelegram = async () => {
|
||||
Alert.alert(
|
||||
"Délier Telegram",
|
||||
"Vous ne recevrez plus de notifications Telegram. Continuer ?",
|
||||
[
|
||||
{ text: "Annuler", style: "cancel" },
|
||||
{
|
||||
text: "Délier",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
await unlinkTelegram();
|
||||
setTelegramLinked(false);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
const handleUnlinkTelegram = () => {
|
||||
setShowTelegramUnlinkModal(true);
|
||||
};
|
||||
|
||||
const confirmUnlinkTelegram = async () => {
|
||||
setShowTelegramUnlinkModal(false);
|
||||
await unlinkTelegram();
|
||||
setTelegramLinked(false);
|
||||
setTwoFAEnabled(false);
|
||||
setShowTelegramSuccessModal(true);
|
||||
};
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
if (!currentPwd || !newPwd || !confirmPwd) {
|
||||
Alert.alert("Erreur", "Veuillez remplir tous les champs");
|
||||
setErrorMsg("Veuillez remplir tous les champs.");
|
||||
setShowErrorModal(true);
|
||||
return;
|
||||
}
|
||||
if (newPwd.length < 8) {
|
||||
Alert.alert("Erreur", "Le nouveau mot de passe doit contenir au moins 8 caractères");
|
||||
setErrorMsg("Le nouveau mot de passe doit contenir au moins 8 caractères.");
|
||||
setShowErrorModal(true);
|
||||
return;
|
||||
}
|
||||
if (newPwd !== confirmPwd) {
|
||||
Alert.alert("Erreur", "Les nouveaux mots de passe ne correspondent pas");
|
||||
setErrorMsg("Les nouveaux mots de passe ne correspondent pas.");
|
||||
setShowErrorModal(true);
|
||||
return;
|
||||
}
|
||||
setSavingPassword(true);
|
||||
@@ -161,23 +192,41 @@ export default function ProfileScreen() {
|
||||
if (result.success) {
|
||||
setShowPasswordModal(false);
|
||||
setCurrentPwd(""); setNewPwd(""); setConfirmPwd("");
|
||||
Alert.alert("Succès", "Mot de passe modifié avec succès");
|
||||
setSuccessTitle("Mot de passe modifié");
|
||||
setSuccessMsg("Votre mot de passe a été changé avec succès.");
|
||||
setShowSuccessModal(true);
|
||||
} else {
|
||||
Alert.alert("Erreur", result.message || "Erreur inattendue");
|
||||
setErrorMsg(result.message || "Erreur inattendue lors du changement de mot de passe.");
|
||||
setShowErrorModal(true);
|
||||
}
|
||||
} finally {
|
||||
setSavingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle2FA = async (value: boolean) => {
|
||||
setTwoFALoading(true);
|
||||
const res = await toggle2FA(value);
|
||||
setTwoFALoading(false);
|
||||
if (res.success) {
|
||||
setTwoFAEnabled(value);
|
||||
} else {
|
||||
Alert.alert("Erreur", res.error || "Impossible de modifier la 2FA");
|
||||
}
|
||||
};
|
||||
|
||||
const saveContact = async () => {
|
||||
setShowConfirmContactModal(false);
|
||||
setSavingContact(true);
|
||||
const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
|
||||
setSavingContact(false);
|
||||
if (res.success) {
|
||||
Alert.alert("Succès", res.message ?? "Profil mis à jour");
|
||||
setSuccessTitle("Compte mis à jour");
|
||||
setSuccessMsg(res.message ?? "Vos informations de compte ont été enregistrées avec succès.");
|
||||
setShowSuccessModal(true);
|
||||
} else {
|
||||
Alert.alert("Erreur", res.message ?? "Erreur lors de la mise à jour");
|
||||
setErrorMsg(res.message ?? "Erreur lors de la mise à jour du profil.");
|
||||
setShowErrorModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -327,6 +376,17 @@ export default function ProfileScreen() {
|
||||
},
|
||||
pwdInputIcon: { marginRight: spacing.s },
|
||||
pwdInput: { flex: 1, fontSize: fontSize.sm },
|
||||
twoFARow: {
|
||||
flexDirection: "row" as const,
|
||||
alignItems: "center" as const,
|
||||
justifyContent: "space-between" as const,
|
||||
paddingTop: spacing.xs,
|
||||
},
|
||||
twoFALabel: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: fontWeight.semibold,
|
||||
},
|
||||
}), [colors]);
|
||||
|
||||
if (loadingProfile) {
|
||||
@@ -338,7 +398,7 @@ export default function ProfileScreen() {
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === "ios" ? "padding" : undefined}>
|
||||
<KeyboardAvoidingView style={styles.container} behavior="padding">
|
||||
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
||||
|
||||
{/* Header */}
|
||||
@@ -387,7 +447,7 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.saveBtn}
|
||||
onPress={saveContact}
|
||||
onPress={() => setShowConfirmContactModal(true)}
|
||||
disabled={savingContact}
|
||||
>
|
||||
<Ionicons name="save-outline" size={16} color="#fff" />
|
||||
@@ -414,6 +474,15 @@ export default function ProfileScreen() {
|
||||
multiline
|
||||
numberOfLines={2}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.saveBtn, styles.saveBtnSecondary, { marginTop: spacing.m }]}
|
||||
onPress={() => setShowConfirmAddressModal(true)}
|
||||
>
|
||||
<Ionicons name="save-outline" size={16} color={colors.accent} />
|
||||
<Text style={[styles.saveBtnText, styles.saveBtnTextSecondary]}>
|
||||
Enregistrer l'adresse
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Carte contact livraison */}
|
||||
@@ -466,6 +535,21 @@ export default function ProfileScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Carte Parrainage */}
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardTitle}>
|
||||
<Ionicons name="gift-outline" size={18} color="#8b5cf6" />
|
||||
<Text style={styles.cardTitleText}>Parrainage</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.saveBtn, { backgroundColor: "#8b5cf622", borderWidth: 1, borderColor: "#8b5cf666" }]}
|
||||
onPress={() => navigation.navigate("Parrainage")}
|
||||
>
|
||||
<Ionicons name="gift-outline" size={16} color="#8b5cf6" />
|
||||
<Text style={[styles.saveBtnText, { color: "#8b5cf6" }]}>Voir mon parrainage</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Carte Telegram */}
|
||||
{telegramEnabled && (
|
||||
<View style={styles.card}>
|
||||
@@ -505,6 +589,42 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Carte 2FA — visible uniquement si l'admin l'a activé ET Telegram est lié */}
|
||||
{twoFAAdminEnabled && telegramLinked && (
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardTitle}>
|
||||
<Ionicons name="shield-checkmark-outline" size={18} color="#6366f1" />
|
||||
<Text style={styles.cardTitleText}>Double authentification (2FA)</Text>
|
||||
</View>
|
||||
<Text style={styles.hint}>
|
||||
À chaque connexion, un code à 6 chiffres vous sera envoyé sur Telegram avant d'accéder à votre compte.
|
||||
</Text>
|
||||
<View style={styles.twoFARow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.twoFALabel}>
|
||||
{twoFAEnabled ? "Activée" : "Désactivée"}
|
||||
</Text>
|
||||
{twoFAEnabled && (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, marginTop: 2 }}>
|
||||
<Ionicons name="checkmark-circle" size={13} color="#10b981" />
|
||||
<Text style={{ color: "#10b981", fontSize: fontSize.xs }}>Protection activée</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{twoFALoading ? (
|
||||
<ActivityIndicator size="small" color="#6366f1" />
|
||||
) : (
|
||||
<Switch
|
||||
value={twoFAEnabled}
|
||||
onValueChange={handleToggle2FA}
|
||||
trackColor={{ false: colors.border, true: "#6366f155" }}
|
||||
thumbColor={twoFAEnabled ? "#6366f1" : colors.textMuted}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<Modal
|
||||
@@ -643,6 +763,224 @@ export default function ProfileScreen() {
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
{/* Modal confirmation déliaison Telegram */}
|
||||
<Modal
|
||||
visible={showTelegramUnlinkModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowTelegramUnlinkModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowTelegramUnlinkModal(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
|
||||
<View style={styles.modalBox}>
|
||||
<View style={[styles.modalIconWrap, { borderColor: "#ef444433", backgroundColor: "#ef444411" }]}>
|
||||
<Ionicons name="unlink-outline" size={24} color="#ef4444" />
|
||||
</View>
|
||||
<Text style={styles.modalTitle}>Délier Telegram ?</Text>
|
||||
<Text style={styles.modalBody}>
|
||||
Vous ne recevrez plus de notifications Telegram. La double authentification sera également désactivée.
|
||||
</Text>
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={styles.modalBtnCancel}
|
||||
onPress={() => setShowTelegramUnlinkModal(false)}
|
||||
>
|
||||
<Text style={styles.modalBtnCancelText}>Annuler</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtnConfirm, { backgroundColor: "#ef444422", borderColor: "#ef444466" }]}
|
||||
onPress={confirmUnlinkTelegram}
|
||||
>
|
||||
<Text style={[styles.modalBtnConfirmText, { color: "#ef4444" }]}>Délier</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
{/* Modal succès déliaison Telegram */}
|
||||
<Modal
|
||||
visible={showTelegramSuccessModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowTelegramSuccessModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowTelegramSuccessModal(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
|
||||
<View style={styles.modalBox}>
|
||||
<View style={[styles.modalIconWrap, { borderColor: "#10b98133", backgroundColor: "#10b98111" }]}>
|
||||
<Ionicons name="checkmark-circle-outline" size={24} color="#10b981" />
|
||||
</View>
|
||||
<Text style={styles.modalTitle}>Compte délié</Text>
|
||||
<Text style={styles.modalBody}>
|
||||
Votre compte Telegram a été délié avec succès. Vous ne recevrez plus de notifications via Telegram.
|
||||
</Text>
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtnConfirm, { flex: 1, borderColor: "#10b98166" }]}
|
||||
onPress={() => setShowTelegramSuccessModal(false)}
|
||||
>
|
||||
<Text style={[styles.modalBtnConfirmText, { color: "#10b981" }]}>Fermer</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
{/* Modal confirmation — Enregistrer le compte */}
|
||||
<Modal
|
||||
visible={showConfirmContactModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowConfirmContactModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowConfirmContactModal(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
|
||||
<View style={styles.modalBox}>
|
||||
<View style={styles.modalIconWrap}>
|
||||
<Ionicons name="person-outline" size={24} color={colors.accent} />
|
||||
</View>
|
||||
<Text style={styles.modalTitle}>Mettre à jour le compte ?</Text>
|
||||
<Text style={styles.modalBody}>
|
||||
Vos informations (prénom, nom, téléphone) seront enregistrées sur votre compte.
|
||||
</Text>
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={styles.modalBtnCancel}
|
||||
onPress={() => setShowConfirmContactModal(false)}
|
||||
>
|
||||
<Text style={styles.modalBtnCancelText}>Annuler</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.modalBtnConfirm}
|
||||
onPress={saveContact}
|
||||
>
|
||||
<Text style={styles.modalBtnConfirmText}>Confirmer</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
{/* Modal confirmation — Enregistrer l'adresse */}
|
||||
<Modal
|
||||
visible={showConfirmAddressModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowConfirmAddressModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowConfirmAddressModal(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
|
||||
<View style={styles.modalBox}>
|
||||
<View style={[styles.modalIconWrap, { borderColor: "#10b98133", backgroundColor: "#10b98111" }]}>
|
||||
<Ionicons name="location-outline" size={24} color="#10b981" />
|
||||
</View>
|
||||
<Text style={styles.modalTitle}>Enregistrer l'adresse ?</Text>
|
||||
<Text style={styles.modalBody}>
|
||||
Cette adresse sera pré-remplie automatiquement lors de vos prochaines commandes.
|
||||
</Text>
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={styles.modalBtnCancel}
|
||||
onPress={() => setShowConfirmAddressModal(false)}
|
||||
>
|
||||
<Text style={styles.modalBtnCancelText}>Annuler</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtnConfirm, { borderColor: "#10b98166" }]}
|
||||
onPress={saveAddress}
|
||||
>
|
||||
<Text style={[styles.modalBtnConfirmText, { color: "#10b981" }]}>Confirmer</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
{/* Modal succès générique */}
|
||||
<Modal
|
||||
visible={showSuccessModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowSuccessModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowSuccessModal(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
|
||||
<View style={styles.modalBox}>
|
||||
<View style={[styles.modalIconWrap, { borderColor: "#10b98133", backgroundColor: "#10b98111" }]}>
|
||||
<Ionicons name="checkmark-circle-outline" size={24} color="#10b981" />
|
||||
</View>
|
||||
<Text style={styles.modalTitle}>{successTitle}</Text>
|
||||
<Text style={styles.modalBody}>{successMsg}</Text>
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtnConfirm, { flex: 1, borderColor: "#10b98166" }]}
|
||||
onPress={() => setShowSuccessModal(false)}
|
||||
>
|
||||
<Text style={[styles.modalBtnConfirmText, { color: "#10b981" }]}>Fermer</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
{/* Modal erreur générique */}
|
||||
<Modal
|
||||
visible={showErrorModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowErrorModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowErrorModal(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
|
||||
<View style={styles.modalBox}>
|
||||
<View style={[styles.modalIconWrap, { borderColor: "#ef444433", backgroundColor: "#ef444411" }]}>
|
||||
<Ionicons name="alert-circle-outline" size={24} color="#ef4444" />
|
||||
</View>
|
||||
<Text style={styles.modalTitle}>Une erreur est survenue</Text>
|
||||
<Text style={styles.modalBody}>{errorMsg}</Text>
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtnConfirm, { flex: 1, borderColor: "#ef444466", backgroundColor: "#ef444411" }]}
|
||||
onPress={() => setShowErrorModal(false)}
|
||||
>
|
||||
<Text style={[styles.modalBtnConfirmText, { color: "#ef4444" }]}>Fermer</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user