Khởi tạo ban đầu

This commit is contained in:
Tran Anh Tuan
2025-11-28 16:59:57 +07:00
parent 2911be97b2
commit 4ba46a7df2
131 changed files with 28066 additions and 0 deletions

35
config/auth.ts Normal file
View File

@@ -0,0 +1,35 @@
import { DOMAIN, TOKEN } from "@/constants";
import { removeStorageItem } from "@/utils/storage";
import { Router } from "expo-router";
let routerInstance: Router | null = null;
/**
* Set router instance để dùng trong non-component context
*/
export const setRouterInstance = (router: Router) => {
routerInstance = router;
};
/**
* Handle 401 error - redirect to login
*/
export const handle401 = () => {
if (routerInstance) {
removeStorageItem(TOKEN);
// Cancel all pending requests to prevent further API calls
if (typeof window !== "undefined" && (window as any).axiosAbortController) {
(window as any).axiosAbortController.abort();
}
routerInstance.navigate("/auth/login");
} else {
console.warn("Router instance not set, cannot redirect to login");
}
};
/**
* Clear router instance (optional, for cleanup)
*/
export const clearRouterInstance = () => {
routerInstance = null;
};

83
config/axios.ts Normal file
View File

@@ -0,0 +1,83 @@
import { DOMAIN, TOKEN } from "@/constants";
import { showErrorToast } from "@/services/toast_service";
import { getStorageItem } from "@/utils/storage";
import axios, { AxiosInstance } from "axios";
import { handle401 } from "./auth";
const codeMessage = {
200: "The server successfully returned the requested data。",
201: "New or modified data succeeded。",
202: "A request has been queued in the background (asynchronous task)。",
204: "Data deleted successfully。",
400: "There is an error in the request sent, the server did not perform the operation of creating or modifying data。",
401: "The user does not have permission (token, username, password is wrong) 。",
403: "User is authorized, but access is prohibited。",
404: "The request issued was for a non-existent record, the server did not operate。",
406: "The requested format is not available。",
410: "The requested resource is permanently deleted and will no longer be available。",
422: "When creating an object, a validation error occurred。",
500: "Server error, please check the server。",
502: "Gateway error。",
503: "Service unavailable, server temporarily overloaded or maintained。",
504: "Gateway timeout。",
};
// Tạo instance axios với cấu hình cơ bản
const api: AxiosInstance = axios.create({
baseURL: "https://sgw.gms.vn",
timeout: 20000, // Timeout 20 giây
headers: {
"Content-Type": "application/json",
},
});
console.log("🚀 Axios baseURL:", api.defaults.baseURL);
// Interceptor cho request (thêm token nếu cần)
api.interceptors.request.use(
async (config) => {
// Thêm auth token nếu có
const token = await getStorageItem(TOKEN);
if (token) {
config.headers.Authorization = `${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Interceptor cho response (xử lý lỗi chung)
api.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (!error.response) {
const networkErrorMsg =
error.message || "Network error - please check connection";
showErrorToast("Lỗi kết nối");
console.error("Response Network Error: ", networkErrorMsg);
return Promise.reject(error);
}
const { status, statusText, data } = error.response;
// Ưu tiên: codeMessage → backend message → statusText
const errMsg =
codeMessage[status as keyof typeof codeMessage] ||
data?.message ||
statusText ||
"Unknown error";
showErrorToast(`Lỗi ${status}: ${errMsg}`);
if (status === 401) {
handle401();
}
return Promise.reject(error);
}
);
export default api;

3
config/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from "./auth";
export { default as api } from "./axios";
export * from "./toast";

2
config/localization.ts Normal file
View File

@@ -0,0 +1,2 @@
export { useI18n } from "@/hooks/use-i18n";
export { default as i18n } from "./localization/i18n";

View File

@@ -0,0 +1,27 @@
import en from "@/locales/en.json";
import vi from "@/locales/vi.json";
import { getLocales } from "expo-localization";
import { I18n } from "i18n-js";
// Set the key-value pairs for the different languages you want to support
const translations = {
en,
vi,
};
const i18n = new I18n(translations);
// Set the locale once at the beginning of your app
// This will be set from storage in the useI18n hook, default to device language or 'en'
i18n.locale = getLocales()[0].languageCode ?? "vi";
// Enable fallback mechanism - if a key is missing in the current language, it will use the key from English
i18n.enableFallback = true;
// Set default locale to English if no locale is available
i18n.defaultLocale = "vi";
// Storage key for locale preference
export const LOCALE_STORAGE_KEY = "app_locale_preference";
export default i18n;

214
config/toast.tsx Normal file
View File

@@ -0,0 +1,214 @@
import { MaterialIcons } from "@expo/vector-icons";
import { View } from "react-native";
import {
BaseToast,
BaseToastProps,
SuccessToast,
} from "react-native-toast-message";
export const Colors: any = {
light: {
text: "#000",
back: "#ffffff",
},
dark: {
text: "#ffffff",
back: "#2B2D2E",
},
default: "#3498db",
info: "#3498db",
success: "#07bc0c",
warn: {
background: "#ffffff",
text: "black",
iconColor: "#f1c40f",
},
error: {
background: "#ffffff",
text: "black",
iconColor: "#e74c3c",
},
textDefault: "#4c4c4c",
textDark: "black",
};
export const toastConfig = {
success: (props: BaseToastProps) => (
<SuccessToast
{...props}
style={{
borderLeftColor: Colors.success,
backgroundColor: Colors.success,
borderRadius: 10,
}}
contentContainerStyle={{
paddingHorizontal: 10,
}}
text1Style={{
color: "white",
fontSize: 16,
fontWeight: "600",
}}
text2Style={{
color: "#f0f0f0",
}}
renderLeadingIcon={() => (
<View
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: 40,
height: "100%",
}}
>
<MaterialIcons name="check-circle" size={30} color="white" />
</View>
)}
/>
),
default: (props: BaseToastProps) => (
<BaseToast
{...props}
style={{
borderLeftColor: Colors.default,
backgroundColor: Colors.default,
borderRadius: 10,
}}
contentContainerStyle={{
paddingHorizontal: 10,
}}
text1Style={{
color: "white",
fontSize: 16,
fontWeight: "600",
}}
text2Style={{
color: "#f0f0f0",
}}
renderLeadingIcon={() => (
<View
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: 50,
height: "100%",
}}
>
<MaterialIcons name="info" size={30} color="white" />
</View>
)}
/>
),
info: (props: BaseToastProps) => (
<BaseToast
{...props}
style={{
borderLeftColor: Colors.info,
backgroundColor: Colors.info,
borderRadius: 10,
}}
contentContainerStyle={{
paddingHorizontal: 10,
}}
text1Style={{
color: "white",
fontSize: 16,
fontWeight: "600",
}}
text2Style={{
color: "#f0f0f0",
}}
renderLeadingIcon={() => (
<View
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: 50,
height: "100%",
}}
>
<MaterialIcons name="info-outline" size={30} color="white" />
</View>
)}
/>
),
warn: (props: BaseToastProps) => (
<BaseToast
{...props}
style={{
borderLeftColor: Colors.warn.background,
backgroundColor: Colors.warn.background,
borderRadius: 10,
}}
contentContainerStyle={{
paddingHorizontal: 10,
}}
text1Style={{
color: Colors.warn.text,
fontSize: 16,
fontWeight: "600",
}}
text2Style={{
color: "#333",
}}
renderLeadingIcon={() => (
<View
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: 50,
height: "100%",
}}
>
<MaterialIcons
name="warning"
size={30}
color={Colors.warn.iconColor}
/>
</View>
)}
/>
),
error: (props: BaseToastProps) => (
<BaseToast
{...props}
style={{
borderLeftColor: Colors.error.background,
backgroundColor: Colors.error.background,
borderRadius: 10,
}}
contentContainerStyle={{
paddingHorizontal: 10,
}}
text1Style={{
color: Colors.error.text,
fontSize: 16,
fontWeight: "600",
}}
text2Style={{
color: "#f0f0f0",
}}
renderLeadingIcon={() => (
<View
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: 50,
height: "100%",
}}
>
<MaterialIcons
name="error"
size={30}
color={Colors.error.iconColor}
/>
</View>
)}
/>
),
};