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 &&