From e7b83f406b1d0ea8f837464ee07f662d545e7159 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Tue, 9 Jun 2026 20:10:24 +0200 Subject: [PATCH] chore: build --- mobile/eas.json | 2 +- mobile/src/api/api.ts | 68 +++++- mobile/src/api/client.ts | 1 - mobile/src/auth/tokenStorage.ts | 1 - mobile/src/screens/client/CheckoutScreen.tsx | 20 +- .../src/screens/client/OrderHistoryScreen.tsx | 195 +++++++++++++++++- 6 files changed, 276 insertions(+), 11 deletions(-) diff --git a/mobile/eas.json b/mobile/eas.json index bf99a48f..2590c902 100644 --- a/mobile/eas.json +++ b/mobile/eas.json @@ -22,7 +22,7 @@ "buildType": "apk" }, "env": { - "EXPO_PUBLIC_API_URL": "https://5.181.0.112.nip.io", + "EXPO_PUBLIC_API_URL": "https://uber-demo.club", "EXPO_PUBLIC_UPDATE_URL": "https://ota.uber-stup.club" }, "channel": "pre-prod" diff --git a/mobile/src/api/api.ts b/mobile/src/api/api.ts index 7ca72413..a615ba0a 100644 --- a/mobile/src/api/api.ts +++ b/mobile/src/api/api.ts @@ -240,10 +240,6 @@ export const clearCart = async (username: string) => { } }; -// ============================================ -// ORDERS -// ============================================ - export const getMyOrders = async () => { try { const { data } = await apiClient.get(`${V1}/my-commands`); @@ -915,6 +911,70 @@ export const toggle2FA = async ( } }; +// ============================================ +// 🏆 POINTS — RÉCOMPENSES +// ============================================ + +export type RewardCategoryConfig = { + category: string; + all_products: boolean; + product_ids: number[]; + 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 PointsRewardConfig = { + threshold: number; + type: string; + description: string; +}; + +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; + 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, + }; + } catch (e: any) { + return { success: false, error: e?.response?.data?.error || "Erreur" }; + } +}; + export const calculateOrderTotal = (order: any): number => { if (typeof order.total === "number" && order.total > 0) return order.total; if (typeof order.total_prix === "number" && order.total_prix > 0) diff --git a/mobile/src/api/client.ts b/mobile/src/api/client.ts index 458c494f..d52169b2 100644 --- a/mobile/src/api/client.ts +++ b/mobile/src/api/client.ts @@ -1,7 +1,6 @@ import axios from "axios"; import { getToken, getAdminToken } from "../auth/tokenStorage"; -// Change this to your server IP/domain export const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? "https://mln-uber.club"; diff --git a/mobile/src/auth/tokenStorage.ts b/mobile/src/auth/tokenStorage.ts index d258bdb1..6fb60b1c 100644 --- a/mobile/src/auth/tokenStorage.ts +++ b/mobile/src/auth/tokenStorage.ts @@ -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); diff --git a/mobile/src/screens/client/CheckoutScreen.tsx b/mobile/src/screens/client/CheckoutScreen.tsx index 4374ca47..1345b831 100644 --- a/mobile/src/screens/client/CheckoutScreen.tsx +++ b/mobile/src/screens/client/CheckoutScreen.tsx @@ -690,9 +690,22 @@ export default function CheckoutScreen() { Total - {cartTotal.toFixed(2)} € + {(useReferral && referralBalance > 0 + ? Math.max(0, cartTotal - referralBalance) + : cartTotal + ).toFixed(2)} € + {useReferral && referralBalance > 0 && ( + + + Dont crédit parrainage + + + -{Math.min(referralBalance, cartTotal).toFixed(2)} € + + + )} @@ -1169,7 +1182,10 @@ export default function CheckoutScreen() { Total - {cartTotal.toFixed(2)} € + {(useReferral && referralBalance > 0 + ? Math.max(0, cartTotal - referralBalance) + : cartTotal + ).toFixed(2)} € diff --git a/mobile/src/screens/client/OrderHistoryScreen.tsx b/mobile/src/screens/client/OrderHistoryScreen.tsx index 6f8614f5..49d6d70b 100644 --- a/mobile/src/screens/client/OrderHistoryScreen.tsx +++ b/mobile/src/screens/client/OrderHistoryScreen.tsx @@ -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 } from "../../api/api"; import type { CompletedOrder, ClientStats, @@ -47,16 +49,24 @@ export default function OrderHistoryScreen() { const [penalties, setPenalties] = useState(null); const [appSettings, setAppSettings] = useState({ 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 }); const [referralBalance, setReferralBalance] = useState(0); + const [pointsRewards, setPointsRewards] = useState<{ + enabled: boolean; + pools: PointsPoolInfo[]; + reward: PointsRewardConfig | null; + } | null>(null); + const [claimingPool, setClaimingPool] = useState(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,19 @@ export default function OrderHistoryScreen() { } }, []); + const handleClaim = async (poolKey: string) => { + setClaimingPool(poolKey); + setClaimFeedback(null); + const res = await claimMyReward(poolKey); + setClaimingPool(null); + if (res.success) { + setClaimFeedback({ pool: poolKey, type: "success", text: res.description || "Récompense réclamée !" }); + getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); }); + } else { + setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" }); + } + }; + useFocusEffect( useCallback(() => { setLoading(true); @@ -132,6 +158,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 +469,70 @@ export default function OrderHistoryScreen() { )} + {/* Section récompenses */} + {pointsRewards?.enabled && pointsRewards.reward && pointsRewards.pools.length > 0 && ( + + + + Récompenses + + {pointsRewards.reward.description !== "" && ( + {pointsRewards.reward.description} + )} + {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 ( + + + {pool.name} + {pool.points} pts + + {pool.eligible_configs.length > 0 && ( + + {pool.eligible_configs.map((cfg) => ( + + + {cfg.category}{cfg.amount > 0 ? ` — ${cfg.amount}€` : ""} + + + ))} + + )} + + + + 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`} + + {feedback && ( + + {feedback.text} + + )} + {pool.rewards_available > 0 && ( + handleClaim(pool.key)} + disabled={isClaiming} + > + + + {isClaiming ? "..." : "Réclamer ma récompense"} + + + )} + + ); + })} + + )} + {/* Bouton parrainage — visible seulement si activé dans les settings */} {appSettings.referral_enabled &&