chore: add crypto payment
This commit is contained in:
@@ -76,6 +76,7 @@ func IPNWebhook(c *gin.Context) {
|
|||||||
|
|
||||||
// GetCommandPaymentStatus - GET /api/v1/commands/:id/payment-status
|
// GetCommandPaymentStatus - GET /api/v1/commands/:id/payment-status
|
||||||
// Retourne le statut du paiement crypto d'une commande (polling côté client)
|
// Retourne le statut du paiement crypto d'une commande (polling côté client)
|
||||||
|
// Effectue un refresh temps réel depuis NowPayments si le paiement est encore en attente
|
||||||
func GetCommandPaymentStatus(c *gin.Context) {
|
func GetCommandPaymentStatus(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
|
||||||
@@ -95,6 +96,30 @@ func GetCommandPaymentStatus(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh temps réel depuis NowPayments pour les statuts intermédiaires (comme la référence)
|
||||||
|
switch payment.Status {
|
||||||
|
case "waiting", "confirming", "confirmed", "sending":
|
||||||
|
np, npOk := c.Get("nowpayments")
|
||||||
|
if npOk && np != nil {
|
||||||
|
npClient := np.(*services.NowPaymentsClient)
|
||||||
|
npStatus, err := npClient.GetPaymentStatus(payment.NowPaymentID)
|
||||||
|
if err == nil && npStatus.PaymentStatus != payment.Status {
|
||||||
|
payAmount, _ := npStatus.PayAmount.Float64()
|
||||||
|
if updateErr := database.UpdateCryptoPaymentStatus(payment.ID, npStatus.PaymentStatus, payAmount); updateErr == nil {
|
||||||
|
payment.Status = npStatus.PaymentStatus
|
||||||
|
payment.PayAmount = payAmount
|
||||||
|
log.Printf("[PAYMENT-STATUS] cmd %d: %s → %s (refresh temps réel)", commandID, payment.Status, npStatus.PaymentStatus)
|
||||||
|
}
|
||||||
|
switch npStatus.PaymentStatus {
|
||||||
|
case "finished", "confirmed":
|
||||||
|
_ = database.ActivateCryptoCommand(commandID)
|
||||||
|
case "failed", "expired":
|
||||||
|
_ = database.CancelCryptoCommand(commandID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"command_id": payment.CommandID,
|
"command_id": payment.CommandID,
|
||||||
"payment_status": payment.Status,
|
"payment_status": payment.Status,
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ func GetPublicSettings(c *gin.Context) {
|
|||||||
"pool_keys": poolKeys,
|
"pool_keys": poolKeys,
|
||||||
"referral_enabled": settings.ReferralEnabled,
|
"referral_enabled": settings.ReferralEnabled,
|
||||||
"delivery_schedule": settings.DeliverySchedule,
|
"delivery_schedule": settings.DeliverySchedule,
|
||||||
|
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
|
||||||
|
"nowpayments_currencies": settings.NowPaymentsCurrencies,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,16 @@ func main() {
|
|||||||
// Démarrage des workers Redis en arrière-plan
|
// Démarrage des workers Redis en arrière-plan
|
||||||
go workers.StartRedisWorkers(database)
|
go workers.StartRedisWorkers(database)
|
||||||
log.Println("✅ Workers Redis démarrés")
|
log.Println("✅ Workers Redis démarrés")
|
||||||
|
|
||||||
|
// Worker de vérification des paiements crypto (recharge les clés dynamiquement)
|
||||||
|
workers.StartDynamicPaymentChecker(database, func() *services.NowPaymentsClient {
|
||||||
|
s, err := database.GetSettings()
|
||||||
|
if err != nil || !s.CryptoPaymentEnabled || s.NowPaymentsAPIKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return services.NewNowPaymentsClient(s.NowPaymentsAPIKey, s.NowPaymentsIPNSecret)
|
||||||
|
}, 2*time.Minute)
|
||||||
|
log.Println("✅ Worker paiements crypto démarré (2 min)")
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
@@ -118,6 +128,16 @@ func main() {
|
|||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Middleware NowPayments : injecte le client si crypto activé dans les settings
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
settings, err := database.GetSettings()
|
||||||
|
if err == nil && settings.CryptoPaymentEnabled && settings.NowPaymentsAPIKey != "" {
|
||||||
|
np := services.NewNowPaymentsClient(settings.NowPaymentsAPIKey, settings.NowPaymentsIPNSecret)
|
||||||
|
c.Set("nowpayments", np)
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
// Fichiers statiques
|
// Fichiers statiques
|
||||||
r.Static("/uploads", "./uploads")
|
r.Static("/uploads", "./uploads")
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ func StartPaymentChecker(database *db.Database, np *services.NowPaymentsClient,
|
|||||||
log.Printf("[CRON] payment checker démarré (toutes les %s)", interval)
|
log.Printf("[CRON] payment checker démarré (toutes les %s)", interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartDynamicPaymentChecker démarre un checker qui recharge le client NowPayments à chaque tick
|
||||||
|
// (permet de prendre en compte les changements de clé API sans redémarrer)
|
||||||
|
func StartDynamicPaymentChecker(database *db.Database, clientFn func() *services.NowPaymentsClient, interval time.Duration) {
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
np := clientFn()
|
||||||
|
if np == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
checkPendingCryptoPayments(database, np)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Printf("[CRON] payment checker dynamique démarré (toutes les %s)", interval)
|
||||||
|
}
|
||||||
|
|
||||||
func checkPendingCryptoPayments(database *db.Database, np *services.NowPaymentsClient) {
|
func checkPendingCryptoPayments(database *db.Database, np *services.NowPaymentsClient) {
|
||||||
payments, err := database.GetPendingCryptoPayments()
|
payments, err := database.GetPendingCryptoPayments()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+35
-1
@@ -292,6 +292,8 @@ export const checkoutCart = async (
|
|||||||
prenom?: string,
|
prenom?: string,
|
||||||
telephone?: string,
|
telephone?: string,
|
||||||
use_referral_balance?: boolean,
|
use_referral_balance?: boolean,
|
||||||
|
payment_method?: string,
|
||||||
|
pay_currency?: string,
|
||||||
): Promise<CheckoutCartResponse> => {
|
): Promise<CheckoutCartResponse> => {
|
||||||
const jwtUsername = await getJwtUsername();
|
const jwtUsername = await getJwtUsername();
|
||||||
if (!jwtUsername) return { success: false, message: "Session invalide" };
|
if (!jwtUsername) return { success: false, message: "Session invalide" };
|
||||||
@@ -309,6 +311,8 @@ export const checkoutCart = async (
|
|||||||
if (nom) payload.nom = nom;
|
if (nom) payload.nom = nom;
|
||||||
if (prenom) payload.prenom = prenom;
|
if (prenom) payload.prenom = prenom;
|
||||||
if (telephone) payload.telephone = telephone;
|
if (telephone) payload.telephone = telephone;
|
||||||
|
if (payment_method) payload.payment_method = payment_method;
|
||||||
|
if (pay_currency) payload.pay_currency = pay_currency;
|
||||||
const { data } = await apiClient.post(`${V1}/checkout`, payload);
|
const { data } = await apiClient.post(`${V1}/checkout`, payload);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -320,6 +324,13 @@ export const checkoutCart = async (
|
|||||||
queue_info: data.queue_info,
|
queue_info: data.queue_info,
|
||||||
referral_used: data.referral_used,
|
referral_used: data.referral_used,
|
||||||
referral_balance: data.referral_balance,
|
referral_balance: data.referral_balance,
|
||||||
|
payment_method: data.payment_method,
|
||||||
|
payment_status: data.payment_status,
|
||||||
|
pay_address: data.pay_address,
|
||||||
|
pay_amount: data.pay_amount,
|
||||||
|
pay_currency: data.pay_currency,
|
||||||
|
price_amount: data.price_amount,
|
||||||
|
price_currency: data.price_currency,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errMsg: string = error.response?.data?.error || "Erreur serveur";
|
const errMsg: string = error.response?.data?.error || "Erreur serveur";
|
||||||
@@ -756,10 +767,12 @@ export interface PublicSettings {
|
|||||||
points_separated: boolean;
|
points_separated: boolean;
|
||||||
referral_enabled: boolean;
|
referral_enabled: boolean;
|
||||||
pool_names: string[];
|
pool_names: string[];
|
||||||
|
crypto_payment_enabled: boolean;
|
||||||
|
nowpayments_currencies: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||||
const defaults: PublicSettings = { penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true, pool_names: ['Pool 1', 'Pool 2'] };
|
const defaults: PublicSettings = { penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, nowpayments_currencies: [] };
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get(`${V1}/app-settings`);
|
const { data } = await apiClient.get(`${V1}/app-settings`);
|
||||||
return {
|
return {
|
||||||
@@ -771,12 +784,33 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
|
|||||||
pool_names: Array.isArray(data.pool_names) && data.pool_names.length > 0
|
pool_names: Array.isArray(data.pool_names) && data.pool_names.length > 0
|
||||||
? data.pool_names
|
? data.pool_names
|
||||||
: defaults.pool_names,
|
: defaults.pool_names,
|
||||||
|
crypto_payment_enabled: data.crypto_payment_enabled ?? false,
|
||||||
|
nowpayments_currencies: Array.isArray(data.nowpayments_currencies) ? data.nowpayments_currencies : [],
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return defaults;
|
return defaults;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface CryptoPaymentStatus {
|
||||||
|
command_id: number;
|
||||||
|
payment_status: string;
|
||||||
|
pay_address: string;
|
||||||
|
pay_amount: number;
|
||||||
|
pay_currency: string;
|
||||||
|
price_amount: number;
|
||||||
|
price_currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCryptoPaymentStatus = async (commandId: number): Promise<CryptoPaymentStatus | null> => {
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get(`${V1}/commands/${commandId}/payment-status`);
|
||||||
|
return data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const registerPushToken = async (token: string): Promise<void> => {
|
export const registerPushToken = async (token: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await apiClient.post(`${V1}/push-token`, { token });
|
await apiClient.post(`${V1}/push-token`, { token });
|
||||||
|
|||||||
@@ -764,6 +764,14 @@ export interface CheckoutCartResponse {
|
|||||||
};
|
};
|
||||||
referral_used?: number;
|
referral_used?: number;
|
||||||
referral_balance?: number;
|
referral_balance?: number;
|
||||||
|
// Crypto payment fields
|
||||||
|
payment_method?: string;
|
||||||
|
payment_status?: string;
|
||||||
|
pay_address?: string;
|
||||||
|
pay_amount?: number;
|
||||||
|
pay_currency?: string;
|
||||||
|
price_amount?: number;
|
||||||
|
price_currency?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfirmReceptionResponse {
|
export interface ConfirmReceptionResponse {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -8,13 +8,21 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
Switch,
|
Switch,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
Clipboard,
|
||||||
|
Alert,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { useCart } from "../../context/CartContext";
|
import { useCart } from "../../context/CartContext";
|
||||||
import { checkoutCart, getReferralBalance } from "../../api/api";
|
import {
|
||||||
|
checkoutCart,
|
||||||
|
getReferralBalance,
|
||||||
|
getPublicSettings,
|
||||||
|
getCryptoPaymentStatus,
|
||||||
|
} from "../../api/api";
|
||||||
|
import type { CryptoPaymentStatus } from "../../api/api";
|
||||||
import type { ClientStackParamList } from "../../navigation/types";
|
import type { ClientStackParamList } from "../../navigation/types";
|
||||||
import TextInput from "../../components/ui/TextInput";
|
import TextInput from "../../components/ui/TextInput";
|
||||||
import Button from "../../components/ui/Button";
|
import Button from "../../components/ui/Button";
|
||||||
@@ -24,6 +32,8 @@ import { spacing, borderRadius, fontSize, fontWeight } from "../../theme";
|
|||||||
|
|
||||||
type Nav = NativeStackNavigationProp<ClientStackParamList>;
|
type Nav = NativeStackNavigationProp<ClientStackParamList>;
|
||||||
|
|
||||||
|
const CRYPTO_COLOR = "#f7931a";
|
||||||
|
|
||||||
export default function CheckoutScreen() {
|
export default function CheckoutScreen() {
|
||||||
const navigation = useNavigation<Nav>();
|
const navigation = useNavigation<Nav>();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
@@ -39,13 +49,32 @@ export default function CheckoutScreen() {
|
|||||||
const [confirmationData, setConfirmationData] = useState<any>(null);
|
const [confirmationData, setConfirmationData] = useState<any>(null);
|
||||||
const [invalidAddressModal, setInvalidAddressModal] = useState(false);
|
const [invalidAddressModal, setInvalidAddressModal] = useState(false);
|
||||||
const [suggestedAddress, setSuggestedAddress] = useState("");
|
const [suggestedAddress, setSuggestedAddress] = useState("");
|
||||||
|
|
||||||
|
// Parrainage
|
||||||
const [referralBalance, setReferralBalance] = useState(0);
|
const [referralBalance, setReferralBalance] = useState(0);
|
||||||
const [useReferral, setUseReferral] = useState(false);
|
const [useReferral, setUseReferral] = useState(false);
|
||||||
|
|
||||||
|
// Crypto
|
||||||
|
const [cryptoEnabled, setCryptoEnabled] = useState(false);
|
||||||
|
const [cryptoCurrencies, setCryptoCurrencies] = useState<string[]>([]);
|
||||||
|
const [paymentMethod, setPaymentMethod] = useState<"especes" | "crypto">("especes");
|
||||||
|
const [payCurrency, setPayCurrency] = useState("");
|
||||||
|
const [cryptoData, setCryptoData] = useState<CryptoPaymentStatus | null>(null);
|
||||||
|
const [showCryptoModal, setShowCryptoModal] = useState(false);
|
||||||
|
const [cryptoPolling, setCryptoPolling] = useState(false);
|
||||||
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getReferralBalance().then((res) => {
|
getReferralBalance().then((res) => {
|
||||||
if (res.success && res.balance > 0) setReferralBalance(res.balance);
|
if (res.success && res.balance > 0) setReferralBalance(res.balance);
|
||||||
});
|
});
|
||||||
|
getPublicSettings().then((settings) => {
|
||||||
|
if (settings.crypto_payment_enabled && settings.nowpayments_currencies.length > 0) {
|
||||||
|
setCryptoEnabled(true);
|
||||||
|
setCryptoCurrencies(settings.nowpayments_currencies);
|
||||||
|
setPayCurrency(settings.nowpayments_currencies[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
AsyncStorage.multiGet([
|
AsyncStorage.multiGet([
|
||||||
"profile_default_address",
|
"profile_default_address",
|
||||||
"profile_default_phone",
|
"profile_default_phone",
|
||||||
@@ -55,27 +84,49 @@ export default function CheckoutScreen() {
|
|||||||
if (savedAddress) setAddress(savedAddress);
|
if (savedAddress) setAddress(savedAddress);
|
||||||
if (savedPhone) setTelephone(savedPhone);
|
if (savedPhone) setTelephone(savedPhone);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (pollRef.current) clearInterval(pollRef.current);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const startPolling = (commandId: number) => {
|
||||||
|
setCryptoPolling(true);
|
||||||
|
pollRef.current = setInterval(async () => {
|
||||||
|
const status = await getCryptoPaymentStatus(commandId);
|
||||||
|
if (!status) return;
|
||||||
|
setCryptoData(status);
|
||||||
|
if (
|
||||||
|
status.payment_status === "finished" ||
|
||||||
|
status.payment_status === "confirmed"
|
||||||
|
) {
|
||||||
|
stopPolling();
|
||||||
|
await refreshCart();
|
||||||
|
} else if (
|
||||||
|
status.payment_status === "failed" ||
|
||||||
|
status.payment_status === "expired"
|
||||||
|
) {
|
||||||
|
stopPolling();
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPolling = () => {
|
||||||
|
setCryptoPolling(false);
|
||||||
|
if (pollRef.current) {
|
||||||
|
clearInterval(pollRef.current);
|
||||||
|
pollRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCheckout = async () => {
|
const handleCheckout = async () => {
|
||||||
if (!nom.trim()) {
|
if (!nom.trim()) { setError("Veuillez saisir votre nom"); return; }
|
||||||
setError("Veuillez saisir votre nom");
|
if (!prenom.trim()) { setError("Veuillez saisir votre prenom"); return; }
|
||||||
return;
|
if (!telephone.trim()) { setError("Veuillez saisir votre numero de telephone"); return; }
|
||||||
}
|
if (!address.trim()) { setError("Veuillez saisir une adresse de livraison"); return; }
|
||||||
if (!prenom.trim()) {
|
if (cartItems.length === 0) { setError("Votre panier est vide"); return; }
|
||||||
setError("Veuillez saisir votre prenom");
|
if (paymentMethod === "crypto" && !payCurrency) {
|
||||||
return;
|
setError("Veuillez sélectionner une cryptomonnaie");
|
||||||
}
|
|
||||||
if (!telephone.trim()) {
|
|
||||||
setError("Veuillez saisir votre numero de telephone");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!address.trim()) {
|
|
||||||
setError("Veuillez saisir une adresse de livraison");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cartItems.length === 0) {
|
|
||||||
setError("Votre panier est vide");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +139,23 @@ export default function CheckoutScreen() {
|
|||||||
prenom.trim(),
|
prenom.trim(),
|
||||||
telephone.trim(),
|
telephone.trim(),
|
||||||
useReferral && referralBalance > 0,
|
useReferral && referralBalance > 0,
|
||||||
|
paymentMethod === "crypto" ? "crypto" : undefined,
|
||||||
|
paymentMethod === "crypto" ? payCurrency : undefined,
|
||||||
);
|
);
|
||||||
if (res.success) {
|
|
||||||
|
if (res.success && res.payment_method === "crypto") {
|
||||||
|
setCryptoData({
|
||||||
|
command_id: res.command_id!,
|
||||||
|
payment_status: res.payment_status!,
|
||||||
|
pay_address: res.pay_address!,
|
||||||
|
pay_amount: res.pay_amount!,
|
||||||
|
pay_currency: res.pay_currency!,
|
||||||
|
price_amount: res.price_amount!,
|
||||||
|
price_currency: res.price_currency!,
|
||||||
|
});
|
||||||
|
setShowCryptoModal(true);
|
||||||
|
startPolling(res.command_id!);
|
||||||
|
} else if (res.success) {
|
||||||
setConfirmationData(res);
|
setConfirmationData(res);
|
||||||
setShowConfirmation(true);
|
setShowConfirmation(true);
|
||||||
await refreshCart();
|
await refreshCart();
|
||||||
@@ -111,6 +177,37 @@ export default function CheckoutScreen() {
|
|||||||
navigation.navigate("ClientTabs");
|
navigation.navigate("ClientTabs");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCryptoClose = () => {
|
||||||
|
stopPolling();
|
||||||
|
setShowCryptoModal(false);
|
||||||
|
navigation.navigate("ClientTabs");
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyAddress = () => {
|
||||||
|
if (!cryptoData) return;
|
||||||
|
Clipboard.setString(cryptoData.pay_address);
|
||||||
|
Alert.alert("Copié", "Adresse copiée dans le presse-papier");
|
||||||
|
};
|
||||||
|
|
||||||
|
const cryptoStatusLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "waiting": return "⏳ En attente de paiement";
|
||||||
|
case "confirming": return "🔄 Confirmation en cours...";
|
||||||
|
case "confirmed": return "✅ Confirmé";
|
||||||
|
case "finished": return "✅ Paiement reçu !";
|
||||||
|
case "failed": return "❌ Paiement échoué";
|
||||||
|
case "expired": return "⌛ Expiré";
|
||||||
|
default: return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cryptoStatusColor = (status: string) => {
|
||||||
|
if (status === "finished" || status === "confirmed") return colors.success;
|
||||||
|
if (status === "failed" || status === "expired") return colors.danger;
|
||||||
|
if (status === "confirming") return colors.info;
|
||||||
|
return colors.warning;
|
||||||
|
};
|
||||||
|
|
||||||
const styles = useMemo(
|
const styles = useMemo(
|
||||||
() =>
|
() =>
|
||||||
StyleSheet.create({
|
StyleSheet.create({
|
||||||
@@ -144,14 +241,8 @@ export default function CheckoutScreen() {
|
|||||||
borderBottomColor: colors.borderLight,
|
borderBottomColor: colors.borderLight,
|
||||||
},
|
},
|
||||||
summaryItemLeft: { flex: 1, marginRight: spacing.m },
|
summaryItemLeft: { flex: 1, marginRight: spacing.m },
|
||||||
summaryItemName: {
|
summaryItemName: { color: colors.textPrimary, fontSize: fontSize.md },
|
||||||
color: colors.textPrimary,
|
summaryItemQty: { color: colors.textMuted, fontSize: fontSize.sm },
|
||||||
fontSize: fontSize.md,
|
|
||||||
},
|
|
||||||
summaryItemQty: {
|
|
||||||
color: colors.textMuted,
|
|
||||||
fontSize: fontSize.sm,
|
|
||||||
},
|
|
||||||
summaryItemPrice: {
|
summaryItemPrice: {
|
||||||
color: colors.textPrimary,
|
color: colors.textPrimary,
|
||||||
fontSize: fontSize.md,
|
fontSize: fontSize.md,
|
||||||
@@ -198,87 +289,96 @@ export default function CheckoutScreen() {
|
|||||||
fontSize: fontSize.md,
|
fontSize: fontSize.md,
|
||||||
fontWeight: fontWeight.semibold,
|
fontWeight: fontWeight.semibold,
|
||||||
},
|
},
|
||||||
referralAmount: {
|
referralAmount: { color: colors.accent, fontSize: fontSize.sm, marginTop: 2 },
|
||||||
color: colors.accent,
|
|
||||||
fontSize: fontSize.sm,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
referralHint: {
|
referralHint: {
|
||||||
color: colors.textMuted,
|
color: colors.textMuted,
|
||||||
fontSize: fontSize.xs,
|
fontSize: fontSize.xs,
|
||||||
marginTop: spacing.s,
|
marginTop: spacing.s,
|
||||||
lineHeight: 16,
|
lineHeight: 16,
|
||||||
},
|
},
|
||||||
prefillHint: {
|
prefillHint: { color: "#6ee7b7", fontSize: fontSize.xs, marginTop: 4, opacity: 0.85 },
|
||||||
color: "#6ee7b7",
|
|
||||||
fontSize: fontSize.xs,
|
|
||||||
marginTop: 4,
|
|
||||||
opacity: 0.85,
|
|
||||||
},
|
|
||||||
invalidAddrContent: { gap: spacing.m },
|
invalidAddrContent: { gap: spacing.m },
|
||||||
invalidAddrIconContainer: { alignItems: "center" },
|
invalidAddrIconContainer: { alignItems: "center" },
|
||||||
invalidAddrIconCircle: {
|
invalidAddrIconCircle: {
|
||||||
width: 64,
|
width: 64, height: 64, borderRadius: 32,
|
||||||
height: 64,
|
|
||||||
borderRadius: 32,
|
|
||||||
backgroundColor: "rgba(251,191,36,0.12)",
|
backgroundColor: "rgba(251,191,36,0.12)",
|
||||||
justifyContent: "center",
|
justifyContent: "center", alignItems: "center",
|
||||||
alignItems: "center",
|
|
||||||
},
|
},
|
||||||
invalidAddrLabel: {
|
invalidAddrLabel: {
|
||||||
color: colors.textSecondary,
|
color: colors.textSecondary, fontSize: fontSize.sm, textAlign: "center",
|
||||||
fontSize: fontSize.sm,
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
},
|
||||||
invalidAddrSuggestion: {
|
invalidAddrSuggestion: {
|
||||||
backgroundColor: colors.bgInput,
|
backgroundColor: colors.bgInput, borderRadius: 10,
|
||||||
borderRadius: 10,
|
padding: spacing.m, borderWidth: 1, borderColor: colors.borderLight,
|
||||||
padding: spacing.m,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.borderLight,
|
|
||||||
},
|
},
|
||||||
invalidAddrSuggestionText: {
|
invalidAddrSuggestionText: {
|
||||||
color: colors.textPrimary,
|
color: colors.textPrimary, fontSize: fontSize.md,
|
||||||
fontSize: fontSize.md,
|
fontWeight: fontWeight.semibold, textAlign: "center",
|
||||||
fontWeight: fontWeight.semibold,
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
invalidAddrActions: {
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: spacing.m,
|
|
||||||
marginTop: spacing.s,
|
|
||||||
},
|
},
|
||||||
|
invalidAddrActions: { flexDirection: "row", gap: spacing.m, marginTop: spacing.s },
|
||||||
confirmContent: { gap: spacing.m },
|
confirmContent: { gap: spacing.m },
|
||||||
confirmIconContainer: { alignItems: "center" },
|
confirmIconContainer: { alignItems: "center" },
|
||||||
confirmIconCircle: {
|
confirmIconCircle: {
|
||||||
width: 72,
|
width: 72, height: 72, borderRadius: 36,
|
||||||
height: 72,
|
|
||||||
borderRadius: 36,
|
|
||||||
backgroundColor: "rgba(74,222,128,0.1)",
|
backgroundColor: "rgba(74,222,128,0.1)",
|
||||||
justifyContent: "center",
|
justifyContent: "center", alignItems: "center",
|
||||||
alignItems: "center",
|
|
||||||
},
|
},
|
||||||
confirmText: {
|
confirmText: {
|
||||||
color: colors.textPrimary,
|
color: colors.textPrimary, fontSize: fontSize.md,
|
||||||
fontSize: fontSize.md,
|
textAlign: "center", lineHeight: 22,
|
||||||
textAlign: "center",
|
|
||||||
lineHeight: 22,
|
|
||||||
},
|
},
|
||||||
confirmInfo: {
|
confirmInfo: {
|
||||||
flexDirection: "row",
|
flexDirection: "row", alignItems: "center", gap: spacing.s,
|
||||||
alignItems: "center",
|
backgroundColor: colors.bgInput, padding: spacing.m,
|
||||||
|
borderRadius: 12, borderWidth: 1, borderColor: colors.borderLight,
|
||||||
|
},
|
||||||
|
confirmInfoText: { color: colors.textSecondary, fontSize: fontSize.sm, flex: 1 },
|
||||||
|
// Méthode de paiement
|
||||||
|
paymentMethodRow: { flexDirection: "row", gap: spacing.m, marginBottom: spacing.m },
|
||||||
|
paymentMethodBtn: {
|
||||||
|
flex: 1, padding: spacing.m, borderRadius: borderRadius.md,
|
||||||
|
borderWidth: 2, borderColor: colors.borderLight,
|
||||||
|
backgroundColor: colors.bgCard,
|
||||||
|
alignItems: "center", justifyContent: "center", gap: spacing.xs,
|
||||||
|
},
|
||||||
|
paymentMethodBtnActive: { borderColor: CRYPTO_COLOR },
|
||||||
|
paymentMethodBtnText: { color: colors.textMuted, fontSize: fontSize.sm, fontWeight: fontWeight.medium },
|
||||||
|
paymentMethodBtnTextActive: { color: CRYPTO_COLOR },
|
||||||
|
// Sélection devise crypto
|
||||||
|
currencyGrid: { flexDirection: "row", flexWrap: "wrap", gap: spacing.s, marginTop: spacing.s },
|
||||||
|
currencyBtn: {
|
||||||
|
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||||
|
borderRadius: borderRadius.sm, borderWidth: 2,
|
||||||
|
borderColor: colors.borderLight, backgroundColor: colors.bgCard,
|
||||||
|
},
|
||||||
|
currencyBtnActive: { borderColor: CRYPTO_COLOR, backgroundColor: "rgba(247,147,26,0.08)" },
|
||||||
|
currencyBtnText: { color: colors.textMuted, fontSize: fontSize.sm, fontWeight: fontWeight.semibold },
|
||||||
|
currencyBtnTextActive: { color: CRYPTO_COLOR },
|
||||||
|
cryptoHint: { color: colors.textMuted, fontSize: fontSize.xs, marginTop: spacing.s },
|
||||||
|
// Modal crypto
|
||||||
|
cryptoContent: { gap: spacing.m },
|
||||||
|
cryptoRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
|
||||||
|
cryptoLabel: { color: colors.textSecondary, fontSize: fontSize.sm },
|
||||||
|
cryptoValue: { color: colors.textPrimary, fontSize: fontSize.md, fontWeight: fontWeight.semibold },
|
||||||
|
cryptoAddressBox: {
|
||||||
|
backgroundColor: colors.bgInput, borderRadius: borderRadius.md,
|
||||||
|
padding: spacing.m, borderWidth: 1, borderColor: colors.borderLight,
|
||||||
gap: spacing.s,
|
gap: spacing.s,
|
||||||
backgroundColor: colors.bgInput,
|
|
||||||
padding: spacing.m,
|
|
||||||
borderRadius: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.borderLight,
|
|
||||||
},
|
},
|
||||||
confirmInfoText: {
|
cryptoAddressText: {
|
||||||
color: colors.textSecondary,
|
color: colors.textPrimary, fontSize: fontSize.xs,
|
||||||
fontSize: fontSize.sm,
|
fontFamily: Platform.OS === "ios" ? "Courier" : "monospace",
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
|
cryptoCopyBtn: {
|
||||||
|
flexDirection: "row", alignItems: "center", justifyContent: "center",
|
||||||
|
gap: spacing.xs, padding: spacing.s, borderRadius: borderRadius.sm,
|
||||||
|
backgroundColor: colors.bgCard, borderWidth: 1, borderColor: colors.borderLight,
|
||||||
|
},
|
||||||
|
cryptoCopyText: { color: colors.textSecondary, fontSize: fontSize.sm },
|
||||||
|
cryptoPollingText: {
|
||||||
|
color: colors.textMuted, fontSize: fontSize.xs, textAlign: "center",
|
||||||
|
},
|
||||||
|
cryptoSuccessText: { color: colors.success, fontSize: fontSize.sm, textAlign: "center" },
|
||||||
}),
|
}),
|
||||||
[colors],
|
[colors],
|
||||||
);
|
);
|
||||||
@@ -289,146 +389,73 @@ export default function CheckoutScreen() {
|
|||||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||||
>
|
>
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
|
{/* Résumé commande */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>
|
<Text style={styles.sectionTitle}>Resume de la commande</Text>
|
||||||
Resume de la commande
|
|
||||||
</Text>
|
|
||||||
<View style={styles.summaryCard}>
|
<View style={styles.summaryCard}>
|
||||||
{cartItems.map((item) => (
|
{cartItems.map((item) => (
|
||||||
<View key={item.id} style={styles.summaryItem}>
|
<View key={item.id} style={styles.summaryItem}>
|
||||||
<View style={styles.summaryItemLeft}>
|
<View style={styles.summaryItemLeft}>
|
||||||
<Text
|
<Text style={styles.summaryItemName} numberOfLines={1}>
|
||||||
style={styles.summaryItemName}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{item.name_product}
|
{item.name_product}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.summaryItemQty}>
|
<Text style={styles.summaryItemQty}>{item.quantity}g</Text>
|
||||||
{item.quantity}g
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.summaryItemPrice}>
|
<Text style={styles.summaryItemPrice}>{item.price.toFixed(2)} €</Text>
|
||||||
{item.price.toFixed(2)} €
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
<View style={styles.summaryTotal}>
|
<View style={styles.summaryTotal}>
|
||||||
<Text style={styles.summaryTotalLabel}>Total</Text>
|
<Text style={styles.summaryTotalLabel}>Total</Text>
|
||||||
<Text style={styles.summaryTotalValue}>
|
<Text style={styles.summaryTotalValue}>{cartTotal.toFixed(2)} €</Text>
|
||||||
{cartTotal.toFixed(2)} €
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Infos personnelles */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>
|
<Text style={styles.sectionTitle}>Informations personnelles</Text>
|
||||||
Informations personnelles
|
|
||||||
</Text>
|
|
||||||
<View style={styles.fieldGroup}>
|
<View style={styles.fieldGroup}>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<View style={styles.halfField}>
|
<View style={styles.halfField}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Nom"
|
placeholder="Nom"
|
||||||
value={nom}
|
value={nom}
|
||||||
onChangeText={(text) => {
|
onChangeText={(t) => { setNom(t); setError(null); }}
|
||||||
setNom(text);
|
icon={<Ionicons name="person-outline" size={20} color={colors.textMuted} />}
|
||||||
setError(null);
|
error={error && !nom.trim() ? error : undefined}
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
<Ionicons
|
|
||||||
name="person-outline"
|
|
||||||
size={20}
|
|
||||||
color={colors.textMuted}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
error={
|
|
||||||
error && !nom.trim() ? error : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.halfField}>
|
<View style={styles.halfField}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Prenom"
|
placeholder="Prenom"
|
||||||
value={prenom}
|
value={prenom}
|
||||||
onChangeText={(text) => {
|
onChangeText={(t) => { setPrenom(t); setError(null); }}
|
||||||
setPrenom(text);
|
icon={<Ionicons name="person-outline" size={20} color={colors.textMuted} />}
|
||||||
setError(null);
|
error={error && nom.trim() && !prenom.trim() ? error : undefined}
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
<Ionicons
|
|
||||||
name="person-outline"
|
|
||||||
size={20}
|
|
||||||
color={colors.textMuted}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
error={
|
|
||||||
error && nom.trim() && !prenom.trim()
|
|
||||||
? error
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Numero de telephone"
|
placeholder="Numero de telephone"
|
||||||
value={telephone}
|
value={telephone}
|
||||||
onChangeText={(text) => {
|
onChangeText={(t) => { setTelephone(t); setError(null); }}
|
||||||
setTelephone(text);
|
icon={<Ionicons name="call-outline" size={20} color={colors.textMuted} />}
|
||||||
setError(null);
|
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
<Ionicons
|
|
||||||
name="call-outline"
|
|
||||||
size={20}
|
|
||||||
color={colors.textMuted}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
keyboardType="phone-pad"
|
keyboardType="phone-pad"
|
||||||
error={
|
error={error && nom.trim() && prenom.trim() && !telephone.trim() ? error : undefined}
|
||||||
error &&
|
|
||||||
nom.trim() &&
|
|
||||||
prenom.trim() &&
|
|
||||||
!telephone.trim()
|
|
||||||
? error
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{!!telephone && (
|
{!!telephone && <Text style={styles.prefillHint}>Pre-rempli depuis votre profil</Text>}
|
||||||
<Text style={styles.prefillHint}>
|
|
||||||
Pre-rempli depuis votre profil
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Adresse */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>
|
<Text style={styles.sectionTitle}>Adresse de livraison</Text>
|
||||||
Adresse de livraison
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Entrez votre adresse complete"
|
placeholder="Entrez votre adresse complete"
|
||||||
value={address}
|
value={address}
|
||||||
onChangeText={(text) => {
|
onChangeText={(t) => { setAddress(t); setError(null); }}
|
||||||
setAddress(text);
|
icon={<Ionicons name="location-outline" size={20} color={colors.textMuted} />}
|
||||||
setError(null);
|
error={error && nom.trim() && prenom.trim() && telephone.trim() && !address.trim() ? error : undefined}
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
<Ionicons
|
|
||||||
name="location-outline"
|
|
||||||
size={20}
|
|
||||||
color={colors.textMuted}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
error={
|
|
||||||
error &&
|
|
||||||
nom.trim() &&
|
|
||||||
prenom.trim() &&
|
|
||||||
telephone.trim() &&
|
|
||||||
!address.trim()
|
|
||||||
? error
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
multiline
|
multiline
|
||||||
numberOfLines={3}
|
numberOfLines={3}
|
||||||
/>
|
/>
|
||||||
@@ -439,7 +466,64 @@ export default function CheckoutScreen() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Section parrainage — visible uniquement si solde > 0 */}
|
{/* Méthode de paiement */}
|
||||||
|
{cryptoEnabled && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Méthode de paiement</Text>
|
||||||
|
<View style={styles.paymentMethodRow}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.paymentMethodBtn, paymentMethod === "especes" && styles.paymentMethodBtnActive]}
|
||||||
|
onPress={() => setPaymentMethod("especes")}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="cash-outline"
|
||||||
|
size={22}
|
||||||
|
color={paymentMethod === "especes" ? CRYPTO_COLOR : colors.textMuted}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.paymentMethodBtnText, paymentMethod === "especes" && styles.paymentMethodBtnTextActive]}>
|
||||||
|
Espèces
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.paymentMethodBtn, paymentMethod === "crypto" && styles.paymentMethodBtnActive]}
|
||||||
|
onPress={() => setPaymentMethod("crypto")}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="logo-bitcoin"
|
||||||
|
size={22}
|
||||||
|
color={paymentMethod === "crypto" ? CRYPTO_COLOR : colors.textMuted}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.paymentMethodBtnText, paymentMethod === "crypto" && styles.paymentMethodBtnTextActive]}>
|
||||||
|
Crypto
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{paymentMethod === "crypto" && (
|
||||||
|
<>
|
||||||
|
<Text style={styles.sectionTitle}>Cryptomonnaie</Text>
|
||||||
|
<View style={styles.currencyGrid}>
|
||||||
|
{cryptoCurrencies.map((c) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={c}
|
||||||
|
style={[styles.currencyBtn, payCurrency === c && styles.currencyBtnActive]}
|
||||||
|
onPress={() => setPayCurrency(c)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.currencyBtnText, payCurrency === c && styles.currencyBtnTextActive]}>
|
||||||
|
{c.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.cryptoHint}>
|
||||||
|
Vous recevrez l'adresse wallet après validation
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Parrainage */}
|
||||||
{referralBalance > 0 && (
|
{referralBalance > 0 && (
|
||||||
<View style={styles.referralCard}>
|
<View style={styles.referralCard}>
|
||||||
<View style={styles.referralRow}>
|
<View style={styles.referralRow}>
|
||||||
@@ -447,9 +531,7 @@ export default function CheckoutScreen() {
|
|||||||
<Ionicons name="gift-outline" size={22} color={colors.accent} />
|
<Ionicons name="gift-outline" size={22} color={colors.accent} />
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.referralTitle}>Credit parrainage</Text>
|
<Text style={styles.referralTitle}>Credit parrainage</Text>
|
||||||
<Text style={styles.referralAmount}>
|
<Text style={styles.referralAmount}>{referralBalance.toFixed(2)} € disponibles</Text>
|
||||||
{referralBalance.toFixed(2)} € disponibles
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -467,11 +549,7 @@ export default function CheckoutScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error &&
|
{error && nom.trim() && prenom.trim() && telephone.trim() && address.trim() && (
|
||||||
nom.trim() &&
|
|
||||||
prenom.trim() &&
|
|
||||||
telephone.trim() &&
|
|
||||||
address.trim() && (
|
|
||||||
<Text style={styles.errorText}>{error}</Text>
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -487,6 +565,7 @@ export default function CheckoutScreen() {
|
|||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Modal adresse invalide */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={invalidAddressModal}
|
visible={invalidAddressModal}
|
||||||
onClose={() => setInvalidAddressModal(false)}
|
onClose={() => setInvalidAddressModal(false)}
|
||||||
@@ -497,44 +576,23 @@ export default function CheckoutScreen() {
|
|||||||
<View style={styles.invalidAddrContent}>
|
<View style={styles.invalidAddrContent}>
|
||||||
<View style={styles.invalidAddrIconContainer}>
|
<View style={styles.invalidAddrIconContainer}>
|
||||||
<View style={styles.invalidAddrIconCircle}>
|
<View style={styles.invalidAddrIconCircle}>
|
||||||
<Ionicons
|
<Ionicons name="location-outline" size={32} color={colors.warning} />
|
||||||
name="location-outline"
|
|
||||||
size={32}
|
|
||||||
color={colors.warning}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.invalidAddrLabel}>
|
<Text style={styles.invalidAddrLabel}>
|
||||||
L'adresse saisie n'est pas reconnue. Voulez-vous
|
L'adresse saisie n'est pas reconnue. Voulez-vous utiliser l'adresse correcte suggérée ?
|
||||||
utiliser l'adresse correcte suggérée ?
|
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.invalidAddrSuggestion}>
|
<View style={styles.invalidAddrSuggestion}>
|
||||||
<Text style={styles.invalidAddrSuggestionText}>
|
<Text style={styles.invalidAddrSuggestionText}>{suggestedAddress}</Text>
|
||||||
{suggestedAddress}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.invalidAddrActions}>
|
<View style={styles.invalidAddrActions}>
|
||||||
<Button
|
<Button title="Modifier" variant="outline" size="md" onPress={() => setInvalidAddressModal(false)} style={{ flex: 1 }} />
|
||||||
title="Modifier"
|
<Button title="Utiliser" variant="success" size="md" onPress={() => { setAddress(suggestedAddress); setInvalidAddressModal(false); }} style={{ flex: 1 }} />
|
||||||
variant="outline"
|
|
||||||
size="md"
|
|
||||||
onPress={() => setInvalidAddressModal(false)}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
title="Utiliser"
|
|
||||||
variant="success"
|
|
||||||
size="md"
|
|
||||||
onPress={() => {
|
|
||||||
setAddress(suggestedAddress);
|
|
||||||
setInvalidAddressModal(false);
|
|
||||||
}}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Modal confirmation (paiement espèces) */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={showConfirmation}
|
visible={showConfirmation}
|
||||||
onClose={handleConfirmClose}
|
onClose={handleConfirmClose}
|
||||||
@@ -545,24 +603,15 @@ export default function CheckoutScreen() {
|
|||||||
<View style={styles.confirmContent}>
|
<View style={styles.confirmContent}>
|
||||||
<View style={styles.confirmIconContainer}>
|
<View style={styles.confirmIconContainer}>
|
||||||
<View style={styles.confirmIconCircle}>
|
<View style={styles.confirmIconCircle}>
|
||||||
<Ionicons
|
<Ionicons name="checkmark-circle" size={48} color={colors.success} />
|
||||||
name="checkmark-circle"
|
|
||||||
size={48}
|
|
||||||
color={colors.success}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.confirmText}>
|
<Text style={styles.confirmText}>
|
||||||
Votre commande #{confirmationData?.command_id} a ete
|
Votre commande #{confirmationData?.command_id} a ete creee avec succes.
|
||||||
creee avec succes.
|
|
||||||
</Text>
|
</Text>
|
||||||
{confirmationData?.referral_used > 0 && (
|
{confirmationData?.referral_used > 0 && (
|
||||||
<View style={styles.confirmInfo}>
|
<View style={styles.confirmInfo}>
|
||||||
<Ionicons
|
<Ionicons name="gift-outline" size={16} color={colors.accent} />
|
||||||
name="gift-outline"
|
|
||||||
size={16}
|
|
||||||
color={colors.accent}
|
|
||||||
/>
|
|
||||||
<Text style={styles.confirmInfoText}>
|
<Text style={styles.confirmInfoText}>
|
||||||
{confirmationData.referral_used.toFixed(2)} € de credit parrainage utilises
|
{confirmationData.referral_used.toFixed(2)} € de credit parrainage utilises
|
||||||
{confirmationData.referral_balance !== undefined
|
{confirmationData.referral_balance !== undefined
|
||||||
@@ -572,24 +621,86 @@ export default function CheckoutScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View style={styles.confirmInfo}>
|
<View style={styles.confirmInfo}>
|
||||||
<Ionicons
|
<Ionicons name="location-outline" size={16} color={colors.info} />
|
||||||
name="location-outline"
|
|
||||||
size={16}
|
|
||||||
color={colors.info}
|
|
||||||
/>
|
|
||||||
<Text style={styles.confirmInfoText}>
|
<Text style={styles.confirmInfoText}>
|
||||||
Suivez votre livraison en temps réel depuis la page Suivi
|
Suivez votre livraison en temps réel depuis la page Suivi
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<Button title="Voir mes commandes" onPress={handleConfirmClose} variant="primary" size="md" fullWidth style={{ marginTop: spacing.l }} />
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Modal paiement crypto */}
|
||||||
|
<Modal
|
||||||
|
visible={showCryptoModal}
|
||||||
|
onClose={handleCryptoClose}
|
||||||
|
title="Paiement Crypto"
|
||||||
|
icon="logo-bitcoin"
|
||||||
|
iconColor={CRYPTO_COLOR}
|
||||||
|
>
|
||||||
|
{cryptoData && (
|
||||||
|
<View style={styles.cryptoContent}>
|
||||||
|
{/* Commande */}
|
||||||
|
<View style={styles.cryptoRow}>
|
||||||
|
<Text style={styles.cryptoLabel}>Commande</Text>
|
||||||
|
<Text style={styles.cryptoValue}>#{cryptoData.command_id}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Statut */}
|
||||||
|
<View style={styles.cryptoRow}>
|
||||||
|
<Text style={styles.cryptoLabel}>Statut</Text>
|
||||||
|
<Text style={[styles.cryptoValue, { color: cryptoStatusColor(cryptoData.payment_status) }]}>
|
||||||
|
{cryptoStatusLabel(cryptoData.payment_status)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Montant */}
|
||||||
|
<View style={styles.cryptoRow}>
|
||||||
|
<Text style={styles.cryptoLabel}>Montant</Text>
|
||||||
|
<Text style={styles.cryptoValue}>
|
||||||
|
{cryptoData.pay_amount} {cryptoData.pay_currency.toUpperCase()}
|
||||||
|
{"\n"}
|
||||||
|
<Text style={{ color: colors.textMuted, fontSize: fontSize.xs }}>
|
||||||
|
≈ {cryptoData.price_amount.toFixed(2)} {cryptoData.price_currency.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Adresse wallet */}
|
||||||
|
<Text style={styles.cryptoLabel}>Adresse wallet</Text>
|
||||||
|
<View style={styles.cryptoAddressBox}>
|
||||||
|
<Text style={styles.cryptoAddressText} selectable>
|
||||||
|
{cryptoData.pay_address}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity style={styles.cryptoCopyBtn} onPress={copyAddress}>
|
||||||
|
<Ionicons name="copy-outline" size={16} color={colors.textSecondary} />
|
||||||
|
<Text style={styles.cryptoCopyText}>Copier l'adresse</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Polling */}
|
||||||
|
{cryptoPolling && (
|
||||||
|
<Text style={styles.cryptoPollingText}>
|
||||||
|
Vérification automatique toutes les 10 secondes...
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(cryptoData.payment_status === "finished" || cryptoData.payment_status === "confirmed") && (
|
||||||
|
<Text style={styles.cryptoSuccessText}>
|
||||||
|
✅ Paiement confirmé ! Votre commande est en cours de traitement.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
title="Voir mes commandes"
|
title="Suivre ma commande"
|
||||||
onPress={handleConfirmClose}
|
onPress={handleCryptoClose}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="md"
|
size="md"
|
||||||
fullWidth
|
fullWidth
|
||||||
style={{ marginTop: spacing.l }}
|
style={{ marginTop: spacing.s }}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function OrderHistoryScreen() {
|
|||||||
const [orders, setOrders] = useState<CompletedOrder[]>([]);
|
const [orders, setOrders] = useState<CompletedOrder[]>([]);
|
||||||
const [stats, setStats] = useState<ClientStats | null>(null);
|
const [stats, setStats] = useState<ClientStats | null>(null);
|
||||||
const [penalties, setPenalties] = useState<PenaltyInfo | 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'] });
|
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: [] });
|
||||||
const [referralBalance, setReferralBalance] = useState(0);
|
const [referralBalance, setReferralBalance] = useState(0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user