chore: build
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@
|
|||||||
"buildType": "apk"
|
"buildType": "apk"
|
||||||
},
|
},
|
||||||
"env": {
|
"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"
|
"EXPO_PUBLIC_UPDATE_URL": "https://ota.uber-stup.club"
|
||||||
},
|
},
|
||||||
"channel": "pre-prod"
|
"channel": "pre-prod"
|
||||||
|
|||||||
+64
-4
@@ -240,10 +240,6 @@ export const clearCart = async (username: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// ORDERS
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export const getMyOrders = async () => {
|
export const getMyOrders = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get(`${V1}/my-commands`);
|
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 => {
|
export const calculateOrderTotal = (order: any): number => {
|
||||||
if (typeof order.total === "number" && order.total > 0) return order.total;
|
if (typeof order.total === "number" && order.total > 0) return order.total;
|
||||||
if (typeof order.total_prix === "number" && order.total_prix > 0)
|
if (typeof order.total_prix === "number" && order.total_prix > 0)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getToken, getAdminToken } from "../auth/tokenStorage";
|
import { getToken, getAdminToken } from "../auth/tokenStorage";
|
||||||
|
|
||||||
// Change this to your server IP/domain
|
|
||||||
export const API_BASE_URL =
|
export const API_BASE_URL =
|
||||||
process.env.EXPO_PUBLIC_API_URL ?? "https://mln-uber.club";
|
process.env.EXPO_PUBLIC_API_URL ?? "https://mln-uber.club";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const USERNAME_KEY = "username";
|
|||||||
const ADMIN_USERNAME_KEY = "admin_username";
|
const ADMIN_USERNAME_KEY = "admin_username";
|
||||||
const ROLE_KEY = "user_role";
|
const ROLE_KEY = "user_role";
|
||||||
|
|
||||||
// Client token
|
|
||||||
export const getToken = () => AsyncStorage.getItem(TOKEN_KEY);
|
export const getToken = () => AsyncStorage.getItem(TOKEN_KEY);
|
||||||
export const setToken = (token: string) =>
|
export const setToken = (token: string) =>
|
||||||
AsyncStorage.setItem(TOKEN_KEY, token);
|
AsyncStorage.setItem(TOKEN_KEY, token);
|
||||||
|
|||||||
@@ -690,9 +690,22 @@ export default function CheckoutScreen() {
|
|||||||
<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)} €
|
{(useReferral && referralBalance > 0
|
||||||
|
? Math.max(0, cartTotal - referralBalance)
|
||||||
|
: cartTotal
|
||||||
|
).toFixed(2)} €
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -1169,7 +1182,10 @@ export default function CheckoutScreen() {
|
|||||||
<View style={styles.recapTotalRow}>
|
<View style={styles.recapTotalRow}>
|
||||||
<Text style={styles.recapTotalLabel}>Total</Text>
|
<Text style={styles.recapTotalLabel}>Total</Text>
|
||||||
<Text style={styles.recapTotalValue}>
|
<Text style={styles.recapTotalValue}>
|
||||||
{cartTotal.toFixed(2)} €
|
{(useReferral && referralBalance > 0
|
||||||
|
? Math.max(0, cartTotal - referralBalance)
|
||||||
|
: cartTotal
|
||||||
|
).toFixed(2)} €
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ import {
|
|||||||
getMyPenalties,
|
getMyPenalties,
|
||||||
getPublicSettings,
|
getPublicSettings,
|
||||||
getReferralBalance,
|
getReferralBalance,
|
||||||
|
getMyPointsRewards,
|
||||||
|
claimMyReward,
|
||||||
formatOrderDate,
|
formatOrderDate,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
import type { PublicSettings } from "../../api/api";
|
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig } from "../../api/api";
|
||||||
import type {
|
import type {
|
||||||
CompletedOrder,
|
CompletedOrder,
|
||||||
ClientStats,
|
ClientStats,
|
||||||
@@ -47,16 +49,24 @@ export default function OrderHistoryScreen() {
|
|||||||
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'], crypto_payment_enabled: false, nowpayments_currencies: [], crypto_only: false, telegram_notifications_enabled: false, two_fa_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 });
|
||||||
const [referralBalance, setReferralBalance] = useState(0);
|
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 [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [histRes, penRes, settings, refRes] = await Promise.all([
|
const [histRes, penRes, settings, refRes, rewardsRes] = await Promise.all([
|
||||||
getMyCompletedOrders(),
|
getMyCompletedOrders(),
|
||||||
getMyPenalties(),
|
getMyPenalties(),
|
||||||
getPublicSettings(),
|
getPublicSettings(),
|
||||||
getReferralBalance(),
|
getReferralBalance(),
|
||||||
|
getMyPointsRewards(),
|
||||||
]);
|
]);
|
||||||
if (histRes.success) {
|
if (histRes.success) {
|
||||||
setOrders(histRes.commands || []);
|
setOrders(histRes.commands || []);
|
||||||
@@ -69,6 +79,9 @@ export default function OrderHistoryScreen() {
|
|||||||
setReferralBalance(refRes.balance);
|
setReferralBalance(refRes.balance);
|
||||||
}
|
}
|
||||||
setAppSettings(settings);
|
setAppSettings(settings);
|
||||||
|
if (rewardsRes.success && rewardsRes.enabled) {
|
||||||
|
setPointsRewards(rewardsRes);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
} finally {
|
} 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(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -132,6 +158,107 @@ export default function OrderHistoryScreen() {
|
|||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
marginBottom: spacing.m,
|
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: {
|
referralBtn: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -342,6 +469,70 @@ export default function OrderHistoryScreen() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</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.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.map((cfg) => (
|
||||||
|
<View key={cfg.category} style={styles.rewardAmountBadge}>
|
||||||
|
<Text style={styles.rewardAmountText}>
|
||||||
|
{cfg.category}{cfg.amount > 0 ? ` — ${cfg.amount}€` : ""}
|
||||||
|
</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 */}
|
{/* Bouton parrainage — visible seulement si activé dans les settings */}
|
||||||
{appSettings.referral_enabled && <TouchableOpacity
|
{appSettings.referral_enabled && <TouchableOpacity
|
||||||
style={styles.referralBtn}
|
style={styles.referralBtn}
|
||||||
|
|||||||
Reference in New Issue
Block a user