Browse Source

Add partner login & Lao locale, downgrade to .NET7

Introduce partner login support and Lao localization, plus project updates for .NET 7 compatibility.

Backend:
- Downgrade Common, Database and Esim.Apis projects from net9.0 to net7.0 and adjust related package version.
- Add PartnerLoginReq model and expose PartnerLogin method in IUserBusiness.
- Implement PartnerLogin in UserBusinessImpl: decrypts partner payload, finds/creates customer, issues JWT tokens, revokes old tokens and persists UserToken. Add Partner config (EncryptionKey) to appsettings and simple encrypt/decrypt logging in Program.cs. Update publish profile to target net7.0.

Frontend:
- Add Lao translations (la.json) and register 'la' locale in i18n.
- Add partnerLogin API method and add crypto-js dependency.
- Switch localStorage usage to getWithExpiry/setWithExpiry across axios, Header, and account slice; set Content-Type header by default.
- Update Header to conditionally show language/guide UI for partner 'laotravel' and persist selectedPartner/selectedLanguage handling.
- Update Footer text and other UI text translations.

Misc:
- Update build/static asset routing in Program.cs (commented MapStaticAssets) and minor formatting/import reorders.

This change enables third-party partner authentication (encrypted payloads) and supports Lao language while ensuring compatibility with .NET 7.
trunghieubui 3 weeks ago
parent
commit
c5491efd6c
32 changed files with 732 additions and 213 deletions
  1. 1 1
      EsimLao/Common/Common.csproj
  2. 1 0
      EsimLao/Common/Constant/CommonConstant.cs
  3. 11 0
      EsimLao/Common/Http/AuthRequest.cs
  4. 1 1
      EsimLao/Database/Database.csproj
  5. 1 0
      EsimLao/Esim.Apis/Business/User/IUserBusiness.cs
  6. 160 6
      EsimLao/Esim.Apis/Business/User/UserBusinessImpl.cs
  7. 14 0
      EsimLao/Esim.Apis/Controllers/UserController.cs
  8. 2 2
      EsimLao/Esim.Apis/Esim.Apis.csproj
  9. 10 4
      EsimLao/Esim.Apis/Program.cs
  10. 1 6
      EsimLao/Esim.Apis/Properties/PublishProfiles/FolderProfile.pubxml
  11. 3 0
      EsimLao/Esim.Apis/appsettings.json
  12. BIN
      EsimLao/esim-vite/dist.zip
  13. 7 0
      EsimLao/esim-vite/package-lock.json
  14. 1 0
      EsimLao/esim-vite/package.json
  15. 7 0
      EsimLao/esim-vite/src/apis/authApi.ts
  16. 5 3
      EsimLao/esim-vite/src/apis/axios.ts
  17. 8 1
      EsimLao/esim-vite/src/components/Footer.tsx
  18. 117 101
      EsimLao/esim-vite/src/components/Header.tsx
  19. 6 3
      EsimLao/esim-vite/src/features/account/accuntSlice.ts
  20. 2 1
      EsimLao/esim-vite/src/i18n/index.ts
  21. 231 0
      EsimLao/esim-vite/src/i18n/locales/la.json
  22. 2 1
      EsimLao/esim-vite/src/i18n/locales/vi.json
  23. 1 1
      EsimLao/esim-vite/src/logic/loigicUtils.ts
  24. 15 7
      EsimLao/esim-vite/src/pages/checkout/CheckoutView.tsx
  25. 36 2
      EsimLao/esim-vite/src/pages/home/HomeView.tsx
  26. 72 63
      EsimLao/esim-vite/src/pages/home/components/HomeFaq.tsx
  27. 0 1
      EsimLao/esim-vite/src/pages/home/components/HomeProduct.tsx
  28. 1 1
      EsimLao/esim-vite/src/pages/news/NewsView.tsx
  29. 7 7
      EsimLao/esim-vite/src/pages/support/SupportView.tsx
  30. 1 0
      EsimLao/esim-vite/src/services/auth/types.ts
  31. 8 1
      EsimLao/esim-vite/src/services/product/type.ts
  32. BIN
      EsimLao/lib/DotnetLib.dll

+ 1 - 1
EsimLao/Common/Common.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net9.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
 	  

+ 1 - 0
EsimLao/Common/Constant/CommonConstant.cs

@@ -149,6 +149,7 @@ public static class ApiUrlConstant
     public const String ResendOtpUrl = "/apis/auth/resend-otp";
     public const String GoogleLoginUrl = "/apis/auth/google-login";
     public const String GoogleCallbackUrl = "/apis/auth/google-callback";
+    public const String PartnerLoginUrl = "/apis/auth/partner-login";
 
     // Article URLs
     public const String ArticleCategoryUrl = "/apis/article/category";

+ 11 - 0
EsimLao/Common/Http/AuthRequest.cs

@@ -54,4 +54,15 @@ namespace Common.Http
         /// </summary>
         public string? lang { get; set; } = "lo";
     }
+
+    public class PartnerLoginReq
+    {
+        [Required(ErrorMessage = "Authorization code is required")]
+        public string data { get; set; }
+
+        [Required(ErrorMessage = "Partner is required")]
+        public string partner { get; set; } // Optional: if frontend handles redirect, it might need to pass this
+
+    }
+
 }

+ 1 - 1
EsimLao/Database/Database.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net9.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
   </PropertyGroup>

+ 1 - 0
EsimLao/Esim.Apis/Business/User/IUserBusiness.cs

@@ -19,6 +19,7 @@ namespace Esim.Apis.Business
         Task<IActionResult> RequestOtp(HttpRequest httpRequest, RequestOtpReq request);
         Task<IActionResult> ResendOtp(HttpRequest httpRequest, RequestOtpReq request);
         Task<IActionResult> VerifyOtp(HttpRequest httpRequest, VerifyOtpReq request);
+        Task<IActionResult> PartnerLogin(HttpRequest httpRequest, PartnerLoginReq request);
         Task<IActionResult> GoogleLogin(HttpRequest httpRequest, GoogleLoginReq request);
         Task<IActionResult> GoogleCallback(HttpRequest httpRequest, GoogleCallbackReq request);
     }

+ 160 - 6
EsimLao/Esim.Apis/Business/User/UserBusinessImpl.cs

@@ -1,9 +1,3 @@
-using System;
-using System.Linq;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading.Tasks;
-using System.Xml.Serialization;
 using Common;
 using Common.Constant;
 using Common.Http;
@@ -17,6 +11,13 @@ using Microsoft.AspNetCore.Mvc;
 using Microsoft.VisualBasic;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
 
 namespace Esim.Apis.Business
 {
@@ -646,6 +647,7 @@ namespace Esim.Apis.Business
                         email = customer.Email ?? "",
                         fullName = $"{customer.SurName} {customer.LastName}".Trim(),
                         avatarUrl = customer.AvatarUrl,
+                        msisdn = customer.PhoneNumber,
                         accessToken,
                         refreshToken,
                         expiresAt
@@ -1002,6 +1004,7 @@ namespace Esim.Apis.Business
                             email = customer.Email ?? "",
                             fullName = $"{customer.SurName} {customer.LastName}".Trim(),
                             avatarUrl = customer.AvatarUrl,
+                            msisdn = customer.PhoneNumber,
                             accessToken = jwtAccessToken,
                             refreshToken = jwtRefreshToken,
                             expiresAt
@@ -1022,5 +1025,156 @@ namespace Esim.Apis.Business
                 );
             }
         }
+
+        public async Task<IActionResult> PartnerLogin(HttpRequest httpRequest, PartnerLoginReq request)
+        {
+            var url = httpRequest.Path;
+            var json = JsonConvert.SerializeObject(request);
+            log.Debug("URL: " + url + " => GoogleCallback Request: " + json);
+
+            try
+            {
+                byte[] keyBytes = Encoding.UTF8.GetBytes(configuration["Partner:EncryptionKey"] ?? "");
+
+                var decrypt = DotnetLib.Secure.SecureLogic.DecryptAes(
+                    request.data.Replace(" ", "+"),
+                    configuration["Partner:EncryptionKey"] ?? ""
+                );
+                if (decrypt == null)
+                {
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log,
+                        url,
+                        json,
+                        CommonErrorCode.Error,
+                        ConfigManager.Instance.GetConfigWebValue("INVALID_PARTNER_DATA"),
+                        new { }
+                    );
+                }
+
+                var requestData = JsonConvert.DeserializeObject<JObject>(decrypt);
+                log.Debug("Decrypted PartnerLogin data: " + JsonConvert.SerializeObject(requestData));
+                string email = requestData["email"]?.ToString() ?? "";
+                if (string.IsNullOrEmpty(email))
+                {
+                    return DotnetLib.Http.HttpResponse.BuildResponse(
+                        log,
+                        url,
+                        json,
+                        CommonErrorCode.RequiredFieldMissing,
+                        ConfigManager.Instance.GetConfigWebValue("EMAIL_REQUIRED"),
+                        new { }
+                    );
+                }
+
+                // Get customer info
+                var customer = dbContext
+                    .CustomerInfos.Where(c => c.Email == email)
+                    .FirstOrDefault();
+
+                if (customer == null)
+                {
+                    // create a new customer
+                    customer = new CustomerInfo
+                    {
+                        Id = await Database.DbLogic.GenIdAsync(dbContext, "CUSTOMER_INFO_SEQ"),
+                        Email = email,
+                        SurName = requestData["fullName"]?.ToString() ?? "",
+                        LastName = requestData["fullName"]?.ToString() ?? "",
+                        AvatarUrl = requestData["avatarUrl"]?.ToString() ?? "",
+                        PhoneNumber = requestData["phoneNumber"]?.ToString() ?? "",
+                        Status = true,
+                        IsVerified = true,
+                        CreatedDate = DateTime.Now,
+                        LastUpdate = DateTime.Now
+                    };
+                    dbContext.CustomerInfos.Add(customer);
+                    await dbContext.SaveChangesAsync();
+                }
+
+                // Update customer verification status
+                customer.IsVerified = true;
+                customer.LastLoginDate = DateTime.Now;
+                customer.LastUpdate = DateTime.Now;
+
+                // Generate JWT tokens
+                int tokenExpireHours = 24;
+                int refreshTokenExpireDays = 30;
+
+                string accessToken = CommonLogic.GenToken(
+                    configuration,
+                    customer.Email ?? "",
+                    customer.Id.ToString() ?? ""
+                );
+                string refreshToken = CommonLogic.GenRefreshToken(
+                    configuration,
+                    customer.Email ?? ""
+                );
+                var expiresAt = DateTime.Now.AddHours(tokenExpireHours);
+                var refreshExpiresAt = DateTime.Now.AddDays(refreshTokenExpireDays);
+
+                // Revoke old tokens
+                var oldTokens = dbContext
+                    .UserTokens.Where(t => t.CustomerId == customer.Id && t.IsRevoked == false)
+                    .ToList();
+
+                foreach (var oldToken in oldTokens)
+                {
+                    oldToken.IsRevoked = true;
+                }
+
+                // Save new token
+                var tokenId = (int)await Database.DbLogic.GenIdAsync(dbContext, "USER_TOKEN_SEQ");
+                var userToken = new UserToken
+                {
+                    Id = tokenId,
+                    CustomerId = customer.Id,
+                    AccessToken = accessToken,
+                    RefreshToken = refreshToken,
+                    TokenType = "Bearer",
+                    DeviceInfo = httpRequest.Headers["User-Agent"].ToString(),
+                    IpAddress = GetClientIpAddress(httpRequest),
+                    ExpiredAt = expiresAt,
+                    RefreshExpiredAt = refreshExpiresAt,
+                    IsRevoked = false,
+                    CreatedDate = DateTime.Now,
+                    LastUsed = DateTime.Now
+                };
+
+                dbContext.UserTokens.Add(userToken);
+                await dbContext.SaveChangesAsync();
+
+                return DotnetLib.Http.HttpResponse.BuildResponse(
+                    log,
+                    url,
+                    json,
+                    CommonErrorCode.Success,
+                    ConfigManager.Instance.GetConfigWebValue("LOGIN_SUCCESS", "en"),
+                    new
+                    {
+                        userId = customer.Id,
+                        email = customer.Email ?? "",
+                        fullName = $"{customer.SurName} {customer.LastName}".Trim(),
+                        avatarUrl = customer.AvatarUrl,
+                        msisdn = customer.PhoneNumber,
+                        accessToken,
+                        refreshToken,
+                        expiresAt
+                    }
+                );
+            }
+            catch (Exception exception)
+            {
+                log.Error("GoogleCallback Exception: ", exception);
+            }
+            return DotnetLib.Http.HttpResponse.BuildResponse(
+                log,
+                url,
+                json,
+                CommonErrorCode.SystemError,
+                ConfigManager.Instance.GetConfigWebValue("SYSTEM_FAILURE"),
+                new { }
+            );
+        }
     }
 }

+ 14 - 0
EsimLao/Esim.Apis/Controllers/UserController.cs

@@ -100,5 +100,19 @@ namespace RevoSystem.Apis.Controllers
         {
             return await userBusiness.GoogleCallback(HttpContext.Request, request);
         }
+
+        /// <summary>
+        /// Google OAuth Callback (Exchange code for token)
+        /// POST /apis/auth/google-callback
+        /// </summary>
+        /// <param name="request">Contains the authorization 'code' (required) and 'redirectUri' (optional)</param>
+        /// <returns>Returns user profile and JWT tokens (accessToken, refreshToken)</returns>
+        [ProducesResponseType(typeof(object), 200)]
+        [HttpPost]
+        [Route(ApiUrlConstant.PartnerLoginUrl)]
+        public async Task<IActionResult> PartnerLogin([FromBody] PartnerLoginReq request)
+        {
+            return await userBusiness.PartnerLogin(HttpContext.Request, request);
+        }
     }
 }

+ 2 - 2
EsimLao/Esim.Apis/Esim.Apis.csproj

@@ -1,14 +1,14 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
-    <TargetFramework>net9.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
   </PropertyGroup>
 	<PropertyGroup Condition=" '$(RunConfiguration)' == 'https' " />
 	<PropertyGroup Condition=" '$(RunConfiguration)' == 'http' " />
 	<ItemGroup>
-		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
+		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" />
 		<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.7" />
 		<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
 		<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />

+ 10 - 4
EsimLao/Esim.Apis/Program.cs

@@ -1,4 +1,3 @@
-using System.Text;
 using Common.Global;
 using Database.Database;
 using Esim.Apis.Business;
@@ -7,6 +6,8 @@ using log4net.Config;
 using Microsoft.AspNetCore.Authentication.JwtBearer;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.IdentityModel.Tokens;
+using System.Security.Cryptography;
+using System.Text;
 
 var builder = WebApplication.CreateBuilder(args);
 
@@ -108,7 +109,12 @@ using (var scope = app.Services.CreateScope())
         logger.LogInformation("Initializing ConfigManager...");
         Esim.Apis.Singleton.ConfigManager.Instance.Initialize();
         logger.LogInformation("ConfigManager initialized successfully");
+        byte[] keyBytes = Encoding.UTF8.GetBytes("9kw1wLMLG2u1j2zX");
+        var encrypt = DotnetLib.Secure.SecureLogic.EncryptAes("{\"username\": \"10014331\",\"fullName\": \"User6066871357\",\"phoneNumber\": \"8560010014331\",\"email\": \"abc@gmail.com\",}", "9kw1wLMLG2u1j2zX");
+        logger.LogInformation($"encrypt: {encrypt}");
 
+        var decrypt = DotnetLib.Secure.SecureLogic.DecryptAes(encrypt, "9kw1wLMLG2u1j2zX");
+        logger.LogInformation($"decrypt: {decrypt}");
         // Start background refresh task
         Task.Run(() => Esim.Apis.Singleton.ConfigManager.Instance.RefreshConfigs());
     }
@@ -147,10 +153,10 @@ app.UseCors("AllowFrontend");
 app.UseAuthentication();
 app.UseAuthorization();
 
-app.MapStaticAssets();
+//app.MapStaticAssets();
 
-app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}")
-    .WithStaticAssets();
+app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
+    //.WithStaticAssets();
 
 app.MapControllers();
 

+ 1 - 6
EsimLao/Esim.Apis/Properties/PublishProfiles/FolderProfile.pubxml

@@ -8,13 +8,8 @@
     <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
     <LastUsedPlatform>Any CPU</LastUsedPlatform>
     <PublishProvider>FileSystem</PublishProvider>
-    <PublishUrl>bin\Release\net9.0\publish\</PublishUrl>
+    <PublishUrl>bin\Release\net7.0\publish\</PublishUrl>
     <WebPublishMethod>FileSystem</WebPublishMethod>
     <_TargetId>Folder</_TargetId>
-    <SiteUrlToLaunchAfterPublish />
-    <TargetFramework>net9.0</TargetFramework>
-    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
-    <ProjectGuid>f7de4e65-3ca0-47dc-89fc-727cdf7558b8</ProjectGuid>
-    <SelfContained>true</SelfContained>
   </PropertyGroup>
 </Project>

+ 3 - 0
EsimLao/Esim.Apis/appsettings.json

@@ -30,5 +30,8 @@
     "ClientId": "510004579352-p8brlhu92qesea636ae2c1k96bq30u0j.apps.googleusercontent.com",
     "ClientSecret": "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",
     "RedirectUri": "https://simgetgo.vn"
+  },
+  "Partner": {
+    "EncryptionKey": "9kw1wLMLG2u1j2zX"
   }
 }

BIN
EsimLao/esim-vite/dist.zip


+ 7 - 0
EsimLao/esim-vite/package-lock.json

@@ -13,6 +13,7 @@
         "@tailwindcss/vite": "^4.1.18",
         "@tanstack/react-query": "^5.90.16",
         "axios": "^1.13.2",
+        "crypto-js": "^4.2.0",
         "i18next": "^25.7.4",
         "meta": "^2.2.25",
         "react": "^19.2.3",
@@ -2041,6 +2042,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT"
+    },
     "node_modules/data-uri-to-buffer": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",

+ 1 - 0
EsimLao/esim-vite/package.json

@@ -14,6 +14,7 @@
     "@tailwindcss/vite": "^4.1.18",
     "@tanstack/react-query": "^5.90.16",
     "axios": "^1.13.2",
+    "crypto-js": "^4.2.0",
     "i18next": "^25.7.4",
     "meta": "^2.2.25",
     "react": "^19.2.3",

+ 7 - 0
EsimLao/esim-vite/src/apis/authApi.ts

@@ -33,6 +33,13 @@ class AuthApi extends BaseApi {
       code,
     });
   }
+
+  async partnerLogin({ partner, data }) {
+    return this.authPost<AccountInfo>("/partner-login", {
+      partner,
+      data,
+    });
+  }
 }
 
 export const authApi = new AuthApi();

+ 5 - 3
EsimLao/esim-vite/src/apis/axios.ts

@@ -1,4 +1,5 @@
 import axios, { AxiosInstance } from "axios";
+import { getWithExpiry } from "../logic/loigicUtils";
 
 export const createAxiosInstance = (baseURL: string): AxiosInstance => {
   const instance = axios.create({
@@ -10,8 +11,9 @@ export const createAxiosInstance = (baseURL: string): AxiosInstance => {
   });
 
   instance.interceptors.request.use((config) => {
-    const token = localStorage.getItem("token");
-    const lang = localStorage.getItem("selectedLanguage") || "en";
+    const token = getWithExpiry("token");
+    const lang = getWithExpiry("selectedLanguage") || "en";
+    config.headers["Content-Type"] = "application/json";
     if (lang) {
       config.headers["Accept-Language"] = lang;
     }
@@ -31,7 +33,7 @@ export const createAxiosInstance = (baseURL: string): AxiosInstance => {
         window.location.href = "/login";
       }
       return Promise.reject(error);
-    }
+    },
   );
 
   return instance;

+ 8 - 1
EsimLao/esim-vite/src/components/Footer.tsx

@@ -64,7 +64,14 @@ const Footer: React.FC = () => {
               Công ty TNHH Phát triển toàn cầu VIETTECH
             </p>
             <p className="text-sm font-medium text-slate-600">
-              Tax Code: 0901210362
+              Mã số thuế: 0901210362
+            </p>
+            <p className="text-sm font-medium text-slate-600">
+              Ngày cấp: 09/01/2026
+            </p>
+            <p className="text-sm font-medium text-slate-600">
+              Nơi cấp: Phòng Đăng Ký Kinh Doanh Tỉnh Hưng Yên - Sở Tài Chính
+              Tỉnh Hưng Yên
             </p>
             <div className="space-y-3 text-sm text-slate-500">
               <p className="flex items-start">

+ 117 - 101
EsimLao/esim-vite/src/components/Header.tsx

@@ -39,7 +39,7 @@ const Header: React.FC = () => {
   const [isBuySimMegaVisible, setIsBuySimMegaVisible] = useState(false);
   const [isGuideMegaVisible, setIsGuideMegaVisible] = useState(false);
   const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
-  const lang = localStorage.getItem("lang") || "en";
+  const lang = getWithExpiry("selectedLanguage") || "en";
 
   const [selectedLang, setSelectedLang] = useState<"en" | "vi">(lang);
   const [activeDesktopTab, setActiveDesktopTab] = useState<
@@ -50,7 +50,8 @@ const Header: React.FC = () => {
   const dispatch = useAppDispatch();
   const langMenuRef = useRef<HTMLDivElement>(null);
   const userMenuRef = useRef<HTMLDivElement>(null);
-  const account = localStorage.getItem("accountInfo");
+  const account = getWithExpiry("accountInfo");
+  const partner = getWithExpiry("selectedPartner") as string | null;
 
   const guideItems = [
     { label: "Giới thiệu cơ bản về eSim", path: "/support" },
@@ -66,10 +67,21 @@ const Header: React.FC = () => {
   const [searchQuery, setSearchQuery] = useState("");
   const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false);
 
-  const languages = [
-    // { code: "en", label: t("english"), flag: "us" },
-    { code: "vi", label: t("vietnamese"), flag: "vn" },
-  ];
+  const languages =
+    partner === "laotravel"
+      ? lang === "la"
+        ? [
+            { code: "la", label: t("lao"), flag: "la" },
+            { code: "vi", label: t("vietnamese"), flag: "vn" },
+          ]
+        : [
+            { code: "vi", label: t("vietnamese"), flag: "vn" },
+            { code: "la", label: t("lao"), flag: "la" },
+          ]
+      : [
+          // { code: "en", label: t("english"), flag: "us" },
+          { code: "vi", label: t("vietnamese"), flag: "vn" },
+        ];
 
   // load product by country/region or popularity
   const getProductMutation = useMutation({
@@ -127,7 +139,7 @@ const Header: React.FC = () => {
     return () => window.removeEventListener("resize", handleResize);
   }, []);
 
-  const langNow = localStorage.getItem("lang") || "en";
+  const langNow = getWithExpiry("selectedLanguage") || "en";
   const currentLangObj =
     languages.find((l) => l.code === (selectedLang || langNow)) || languages[0];
 
@@ -466,56 +478,58 @@ const Header: React.FC = () => {
                 )}
               </div>
 
-              <div
-                className="relative h-full flex items-center"
-                onMouseEnter={() => setIsGuideMegaVisible(true)}
-                onMouseLeave={() => setIsGuideMegaVisible(false)}
-              >
-                <Link
-                  to="/support"
-                  className={`flex items-center text-[17px] font-bold transition-colors ${
-                    isActive("/support")
-                      ? "text-[#EE0434]"
-                      : "text-slate-700 hover:text-[#EE0434]"
-                  }`}
+              {partner !== "laotravel" && (
+                <div
+                  className="relative h-full flex items-center"
+                  onMouseEnter={() => setIsGuideMegaVisible(true)}
+                  onMouseLeave={() => setIsGuideMegaVisible(false)}
                 >
-                  {t("guide")}{" "}
-                  <svg
-                    className={`ml-1 w-4 h-4 transition-transform ${
-                      isGuideMegaVisible ? "rotate-180" : ""
+                  <Link
+                    to="/support"
+                    className={`flex items-center text-[17px] font-bold transition-colors ${
+                      isActive("/support")
+                        ? "text-[#EE0434]"
+                        : "text-slate-700 hover:text-[#EE0434]"
                     }`}
-                    fill="none"
-                    stroke="currentColor"
-                    viewBox="0 0 24 24"
                   >
-                    <path
-                      strokeLinecap="round"
-                      strokeLinejoin="round"
-                      strokeWidth={2}
-                      d="M19 9l-7 7-7-7"
-                    />
-                  </svg>
-                </Link>
+                    {t("guide")}{" "}
+                    <svg
+                      className={`ml-1 w-4 h-4 transition-transform ${
+                        isGuideMegaVisible ? "rotate-180" : ""
+                      }`}
+                      fill="none"
+                      stroke="currentColor"
+                      viewBox="0 0 24 24"
+                    >
+                      <path
+                        strokeLinecap="round"
+                        strokeLinejoin="round"
+                        strokeWidth={2}
+                        d="M19 9l-7 7-7-7"
+                      />
+                    </svg>
+                  </Link>
 
-                {isGuideMegaVisible && (
-                  <div className="absolute top-full left-0 w-[600px] bg-white rounded-[32px] shadow-2xl border border-slate-100 mt-0 overflow-hidden flex animate-in fade-in slide-in-from-top-2 duration-300">
-                    <div className="flex-1 py-10 px-6 flex flex-col">
-                      {guideItems.map((item, idx) => (
-                        <button
-                          key={item.label}
-                          onClick={() => {
-                            navigate("/support");
-                            setIsGuideMegaVisible(false);
-                          }}
-                          className={`w-full text-left px-6 py-4 rounded-2xl text-base font-medium transition-all text-slate-600 hover:bg-slate-50 hover:text-[#EE0434]`}
-                        >
-                          {item.label}
-                        </button>
-                      ))}
+                  {isGuideMegaVisible && (
+                    <div className="absolute top-full left-0 w-[600px] bg-white rounded-[32px] shadow-2xl border border-slate-100 mt-0 overflow-hidden flex animate-in fade-in slide-in-from-top-2 duration-300">
+                      <div className="flex-1 py-10 px-6 flex flex-col">
+                        {guideItems.map((item, idx) => (
+                          <button
+                            key={item.label}
+                            onClick={() => {
+                              navigate("/support");
+                              setIsGuideMegaVisible(false);
+                            }}
+                            className={`w-full text-left px-6 py-4 rounded-2xl text-base font-medium transition-all text-slate-600 hover:bg-slate-50 hover:text-[#EE0434]`}
+                          >
+                            {item.label}
+                          </button>
+                        ))}
+                      </div>
                     </div>
-                  </div>
-                )}
-              </div>
+                  )}
+                </div>
+              )}
 
               <Link
                 to="/news"
@@ -693,10 +707,10 @@ const Header: React.FC = () => {
                         <button
                           key={lang.code}
                           onClick={() => {
-                            setSelectedLang(lang.code as "en" | "vi");
+                            setSelectedLang(lang.code as "en" | "vi" | "la");
                             setIsLangMenuOpen(false);
                             i18n.changeLanguage(lang.code);
-                            localStorage.setItem("lang", lang.code);
+                            setWithExpiry("selectedLanguage", lang.code);
                           }}
                           className={`flex items-center space-x-3 px-5 py-4 w-full text-left transition-colors ${
                             selectedLang === lang.code
@@ -880,55 +894,57 @@ const Header: React.FC = () => {
                 </div>
               </div>
 
-              <div className="w-full">
-                <button
-                  onClick={() => setIsGuideExpanded(!isGuideExpanded)}
-                  className={`w-full flex items-center justify-center space-x-3 py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
-                    isActive("/support")
-                      ? "bg-red-50 text-[#EE0434]"
-                      : "text-slate-800 hover:bg-slate-50"
-                  }`}
-                >
-                  <span>{t("guide")}</span>
-                  <svg
-                    className={`w-6 h-6 transition-transform duration-300 ${
-                      isGuideExpanded ? "rotate-180" : ""
+              {partner !== "laotravel" && (
+                <div className="w-full">
+                  <button
+                    onClick={() => setIsGuideExpanded(!isGuideExpanded)}
+                    className={`w-full flex items-center justify-center space-x-3 py-5 px-6 rounded-3xl text-2xl font-black transition-all ${
+                      isActive("/support")
+                        ? "bg-red-50 text-[#EE0434]"
+                        : "text-slate-800 hover:bg-slate-50"
                     }`}
-                    fill="none"
-                    stroke="currentColor"
-                    viewBox="0 0 24 24"
                   >
-                    <path
-                      strokeLinecap="round"
-                      strokeLinejoin="round"
-                      strokeWidth={2.5}
-                      d="M19 9l-7 7-7-7"
-                    />
-                  </svg>
-                </button>
-                <div
-                  className={`overflow-hidden transition-all duration-300 ease-in-out ${
-                    isGuideExpanded
-                      ? "max-h-[400px] opacity-100 mt-4"
-                      : "max-h-0 opacity-0"
-                  }`}
-                >
-                  <div className="flex flex-col space-y-2 px-2">
-                    {guideItems.map((item) => (
-                      <button
-                        key={item.label}
-                        onClick={() => {
-                          navigate(item.path);
-                          setIsMenuOpen(false);
-                        }}
-                        className="w-full text-center py-4 rounded-2xl bg-slate-50 text-slate-600 font-bold hover:text-[#EE0434] active:bg-red-50"
-                      >
-                        {item.label}
-                      </button>
-                    ))}
+                    <span>{t("guide")}</span>
+                    <svg
+                      className={`w-6 h-6 transition-transform duration-300 ${
+                        isGuideExpanded ? "rotate-180" : ""
+                      }`}
+                      fill="none"
+                      stroke="currentColor"
+                      viewBox="0 0 24 24"
+                    >
+                      <path
+                        strokeLinecap="round"
+                        strokeLinejoin="round"
+                        strokeWidth={2.5}
+                        d="M19 9l-7 7-7-7"
+                      />
+                    </svg>
+                  </button>
+                  <div
+                    className={`overflow-hidden transition-all duration-300 ease-in-out ${
+                      isGuideExpanded
+                        ? "max-h-[400px] opacity-100 mt-4"
+                        : "max-h-0 opacity-0"
+                    }`}
+                  >
+                    <div className="flex flex-col space-y-2 px-2">
+                      {guideItems.map((item) => (
+                        <button
+                          key={item.label}
+                          onClick={() => {
+                            navigate(item.path);
+                            setIsMenuOpen(false);
+                          }}
+                          className="w-full text-center py-4 rounded-2xl bg-slate-50 text-slate-600 font-bold hover:text-[#EE0434] active:bg-red-50"
+                        >
+                          {item.label}
+                        </button>
+                      ))}
+                    </div>
                   </div>
                 </div>
-              </div>
+              )}
 
               <Link
                 to="/news"
@@ -979,10 +995,10 @@ const Header: React.FC = () => {
                     <button
                       key={lang.code}
                       onClick={() => {
-                        setSelectedLang(lang.code as "en" | "vi");
+                        setSelectedLang(lang.code as "en" | "vi" | "la");
                         setIsLangMenuOpen(false);
                         i18n.changeLanguage(lang.code);
-                        localStorage.setItem("lang", lang.code);
+                        setWithExpiry("selectedLanguage", lang.code);
                       }}
                       className={`flex items-center space-x-2 px-4 py-2 rounded-2xl transition-all border ${
                         selectedLang === lang.code

+ 6 - 3
EsimLao/esim-vite/src/features/account/accuntSlice.ts

@@ -1,3 +1,4 @@
+import { setWithExpiry } from "../../logic/loigicUtils";
 import { AccountInfo } from "../../services/auth/types";
 import { createSlice, PayloadAction } from "@reduxjs/toolkit";
 
@@ -8,9 +9,9 @@ const accountSlice = createSlice({
   },
   reducers: {
     accountLogin: (state, action: PayloadAction<AccountInfo>) => {
-      localStorage.setItem("token", action.payload.accessToken);
-      localStorage.setItem("refreshToken", action.payload.refreshToken);
-      localStorage.setItem("accountInfo", JSON.stringify(action.payload));
+      setWithExpiry("token", action.payload.accessToken);
+      setWithExpiry("refreshToken", action.payload.refreshToken);
+      setWithExpiry("accountInfo", action.payload);
       return {
         ...state,
         account: action.payload,
@@ -20,6 +21,8 @@ const accountSlice = createSlice({
       localStorage.removeItem("token");
       localStorage.removeItem("refreshToken");
       localStorage.removeItem("accountInfo");
+      localStorage.removeItem("selectedPartner");
+      localStorage.removeItem("selectedLanguage");
       return {
         ...state,
         account: null,

+ 2 - 1
EsimLao/esim-vite/src/i18n/index.ts

@@ -3,10 +3,11 @@ import { initReactI18next } from "react-i18next";
 
 import en from "./locales/en.json";
 import vi from "./locales/vi.json";
+import la from "./locales/la.json";
 
 i18n.use(initReactI18next).init({
   resources: {
-    // en: { translation: en },
+    la: { translation: la },
     vi: { translation: vi },
   },
   lng: "vi", // default

+ 231 - 0
EsimLao/esim-vite/src/i18n/locales/la.json

@@ -0,0 +1,231 @@
+{
+  "buySim": "ຊື້ຊີມ",
+  "viewAll": "ເບີ່ງທັ້ງໝົດ",
+  "mostPopular": "ຍອດນີຍົມທີ່ສຸດ",
+  "region": "ພື້ນທີ່",
+  "guide": "ນຳພາ",
+  "whatEsim": "eSim ແມ່ນຫຍັງ?",
+  "installationInstructions": "ແນະນຳການຕີດຕັ້ງ",
+  "supportSupport": "ການຊ່ວຍເຫຼືອ",
+  "orderSearch": "ກວດສອບການສັ່ງຊື້",
+  "seeMore": "ເບີ່ີ່ງເພີ່ມເຕີມ",
+  "whatUs": "ລູກຄ້າເວົ້າຫຍັງກ່ຽວກັບພວກເຮົາ",
+  "chooseGetgo": "ເປັນຫຍັງຈື່ງເລືອກ SkySimHub",
+  "fastCost": "ວ່ອງໄວ - ສະດວກ - ລາຄາສູດຄຸ້ມ",
+  "globalAnywhere": "ເຊື່ອມຕໍ່ທົ່ວໂລກ - ໝັ້ນຄົງທຸກທີ່",
+  "customerSupport": "ຊ່ວຍເຫຼືອລູກຄ້າ 24 ຊົ່ວໂມງ",
+  "customerPolicy": "ນະໂຍບາຍຄວາມສຳຄັນລູກຄ້າ",
+  "getgoWith": "ການຮ່ວມມືກັນ",
+  "buyExplore": "ຊື້ື້ SIM ງ່າຍດາຍ​ , ພ້ອມທີ່ຈະຄົ້ນຫາ",
+  "compatibleDevice": "ອຸປະກອນທີ່ເຂົ້າກັນໄດ້",
+  "pickPlan": "ເລືອກແພັກເກັດສີນຄ້າ",
+  "instantActivation": "ເປີດໃຊ້ທັນທີ",
+  "frequentlyQuestions": "ຄຳຖາມທີ່ມັກພົບ",
+  "refundDefective": "ຄືນເງີນ 100% ຖ້າສິນຄ້າມີບັນຫາ",
+  "returnGuide": "ຊ່ວຍເຫຼືອ ການປ່ຽນ ແລະ ສົ່ງຄືນເງີນ",
+  "chooseTo": "ເລືືອກປະເທດທີ່ທ່ານຈະໄປ",
+  "doesEsim?": "ໂທລະສັບຂອງຂ້ອຍມີຮອງຮັບ eSIM ບໍ?",
+  "orders": "ຂໍ້ມູນສິນຄ້າ",
+  "logout": "ອອກຈາກລະບົບ",
+  "viettechLimited": "ບໍໍລີສັດ TNHH ພັດທະນາທົ່ວໂລກ VIETTECH",
+  "taxCode": "ເລກພາສີ ; 0901210362",
+  "address": "218 ດາວເຊືອ 1 , ເຂດຕົວເມືອງ Vinhomes Ocean park 2,ຊຸມຊົນ ເງຍຈຸ , ແຂວງ ຮຶງອ່ຽນ, ຫວຽດນາມ",
+  "getgoGetgo": "SkySimHub",
+  "aboutUs": "ແນະນຳ",
+  "termsService": "ເງື່ອນໄຂການໃຫ້ບໍລິການ",
+  "privacyPolicy": "ນະໂຍບາຍຄວາມເປັນສ່ວນຕັວ",
+  "travelSim": "SIM ທ່ອງທ່ຽວ",
+  "chinaSim": "SIM ຈີນ",
+  "thailandSim": "SIM ໄທ",
+  "japanSim": "SIM ຍີ່ປຸ່ນ",
+  "emailAsia": "ອີເມວ: klinhnguyen@viettech.asia",
+  "zaloWhatsapp": "Zalo/Whatsapp: +84336548007",
+  "verifiedSpeed": "ໄດ້ກວດສອບ : ຄວາມໄວສູງ",
+  "numberDays": "ຈຳນວນມື້",
+  "dataData": "ຂໍ້ມູູນ",
+  "unlimitedUnlimited": "ບໍ່ຈຳກັດ",
+  "simType": "ປະເພດຊີມ",
+  "quantity": "ຈຳນວນ",
+  "esimEsim": "eSim",
+  "days": "ວັນທີ່",
+  "discountDiscount": "ຫຼຸດລາຄາ",
+  "pricePrice": "ລາຄາ",
+  "vnd": "ດົງ (VND) ຫວຽດນາມດົງ",
+  "continueShopping": "ສືບຕໍ່ຊື້ເຄື່ອງ",
+  "chooseProduct": "ເລືອກສິນຄ້າ",
+  "orderInformation": "ຂໍ້ມູນສິນຄ້າ",
+  "paymentPayment": "ຊຳລະເງີນ",
+  "customerInformation": "ຂໍ້ມູນລູກຄ້າ",
+  "lastName": "ນາມສະກຸນ",
+  "firstName": "ຊື່",
+  "emailEmail": "ອີເມວ",
+  "confirmEmail": "ຢືືນຢັນ ອີເມວ",
+  "phoneNumber": "ເບີໂທລະສັບ",
+  "orderInfo": "ຂໍ້ມູນການສັ່ງຊື້",
+  "esim": "eSIM",
+  "paymentMethod": "ວິທີການຊຳລະເງີນ",
+  "onepayOnepay": "OnePay",
+  "iService": "ຂ້ອຍໄດ້ອ່ານ ແລະ ເຫັນດີກັບເງື່ອນໄຂບໍລິການ",
+  "iUnlocked": "ຂ້ອຍຢືນຢັນວ່າອຸປະກອນຂອງຂ້ອຍຮອງຮັບ eSIM ແລະ ເຄື່ອຂ່າຍປົດລ໊ອກແລ້ວ",
+  "iInvoice": "ຂ້ອຍຕ້ອງການຮັບໃບແຈ້ງໝີ້ພາສີ",
+  "overviewOverview": "ພາບລວມ",
+  "totalTotal": "ລວມທັງໝົດ",
+  "paymentStatus": "ສະຖານະການຊຳລະເງີນ",
+  "paymentSuccessful": "ຊຳລະເງີີນສຳເລັດ!",
+  "viewOrder": "ເບິ່ງການສັ່ງຊື້",
+  "close": "ປິດ",
+  "transactionHistory": "ປະຫວັດການເຮັດທຸລະກຳ",
+  "enterCode": "ໃສ່ລະຫັດການສັ່ງຊື້",
+  "fromDate": "ຕັ້ງແຕ່ວັນທີ່",
+  "toDate": "ຮອດວັນທີ່",
+  "statusStatus": "ສະຖານະ",
+  "successSuccess": "ສຳເລັດ",
+  "pendingPayment": "ລໍຖ້າຊຳລະເງີນ",
+  "transactionNo.": "ລະຫັັດທຸລະກຳ / ລະຫັດການສັ່ງຊື້",
+  "dateTime": "ວັນທີ່ີ່ ແລະ ເວລາ",
+  "amountAmount": "ຈຳນວນເງີນ",
+  "detailDetail": "ລາຍລະອຽດ",
+  "manageManage": "ການຈັດການ",
+  "fullName": "ຊື່ ແລະ ນາມສະກຸນ",
+  "simNam": "SIM ຫວຽດນາມ",
+  "validityPeriod": "ໄລຍະການນຳໃຊ້",
+  "daysDays": "ວັນທີ່",
+  "qrCode": "ລະຫັດ QR",
+  "getUs": "ຕິດຕໍ່ຫາພວກເຮົາ",
+  "weArePossible": "ພວກເຮົາພ້ອມຈະຮັບຟັງ ແລະ ຊ່ວຍເຫຼືອທ່ານທຸກຢ່າງທີ່ສົງໄສ.ກະລຸນາຝາກຂໍ້ມູນໄວ້ ,ທີມງານພວກເຮົາຈະຕິດຕໍ່ກັບຫາທ່ານໄວທີ່ສຸດ",
+  "enterName": "ໃສ່ຊື່ ແລະ ນາມສະກຸນ ຂອງທ່ານ",
+  "specificRequirements": "ຈູດປະສົງຫຼັກ",
+  "describeRequirements": "ອະທີບາຍຄວາມຕ້ອງການຂອງທ່ານ",
+  "submitSubmit": "ສົ່ງ",
+  "from": "ຈາກ",
+  "noMatchesFound": "ບໍ່ພົບຜົນລັບ",
+  "viewAllDestinations": "ເບີ່ງຈຸດໝາຍປາຍທາງທັ້ງໝົດ",
+  "selectLanguage": "ເລືອກພາສາ",
+  "weAreAlwaysHereToSupportYou": "ພວກເຮົົາຢູ່ນີ້ສະເໝີເພື່ອຊ່ວຍເຫຼືອທ່ານ",
+  "frequently": "ສີ່ງທີ່ມັກພົບເຫັນ",
+  "askedQuestions": "ຄຳຖາມທີ່ີ່ໄດ້ຖາມ",
+  "fastestData": "ຂໍ້ມູນທີ່ໄວທີ່ສຸດ",
+  "bestPrices": "ລາຄາດີທີ່ສຸດ",
+  "connectInstantlyInOver200Destinations": "ການເຊື່ອມຕໍ່ທັນທີທີ່ຫລາຍກວ່າ 200 ຈຸດໝາຍປາຍທາງ",
+  "countryList": "ລາຍຊື່ແຕ່ລະປະເທດ",
+  "regionList": "ພື້້ນທີ່",
+  "noCountryMatchesFound": "ບໍ່ມີປະເທດໃດທີ່ເໝາະກັບ \" {searchQuery}\"",
+  "doesEsimWorkWithMyDevice": "ໂທລະສັັບຂອງຂ້ອຍມີການຮອງຮັບ eSIM ບໍ?",
+  "sayAboutUs": "ເວົ້າກ່ຽວກັບພວກເຮົາ",
+  "overMillionSatisfiedCustomers": "ຫຼາຍກວ່າ 1.000.000 ລູກຄ້າພໍໃຈທີ່ໄດ້ໃຊ້ບໍລີການພວກເຮົາ",
+  "simCountries": "ປະເທດ SIM",
+  "apply": "ນຳໃຊ້",
+  "subtotal": "ໂດຍປະມານ",
+  "checkout": "ການຊຳລະເງີນ",
+  "getInTouch": "ຕິດຕໍ່ໍ່",
+  "withUs": "ກັບພວກເຮົາ",
+  "backToNews": "ກັບຄືນໜ້າຫຼັກ",
+  "myEsim": "eSIM ຂອງຂ້ອຍ",
+  "back": "ກັບຄືືນ",
+  "failure": "ລົ້ມເຫຼວ",
+  "guideAndHelp": "ຄູ່ມື ແລະ ການຊ່ວນເຫຼືອ",
+  "selectTopicInstructions": "ເລືອກຫົວຂໍ້ແຕ່ເບື້ອງຊ້າຍ ເພື່ອເບິ່ງຄຳແນະນຳລາຍລະອຽດໃຫ້ SIM ທ່ອງທ່ຽວ ຫລື eSIM ຂອງທ່ານ.ທີມງານພວກເຮົາພ້ອມໃຫ້ຄຳປຶກສາ 24 ຊົ່ວໂມງ",
+  "importantNote": "",
+  "someDualPhysicalSimModelsDoNotSupportEsim": "ໂທລະສັບບາງລຸ້ນມີ 2 ຊີມ ບໍ່ຕ້ອງການ eSim",
+  "carrierLockedDevicesMayNotSupportEsim": "ອຸປະກອນທີ່ລ໊ອກໂດຍຜູ້ໃຫ້ບໍລິການ ( ອຸປະກອນທີ່ລົງທະບຽນພາຍໃຕ້ຕາມສັນຍາ) ອາດບໍ່ຮອງຮັບ eSIM.",
+  "someModelsPurchasedInCertainCountriesMayNotSupportEsim": "ບາງລຸ້ນທີ່ຊື່ໃນຕ່າງປະເທດອາດບໍ່ຮອງຮັບ eSIM ( ເບີ່ງຕາຕະລາງດ້ານລຸ່ມ)",
+  "toMakeSureYourDeviceSupportsEsim": "ເພື່ອແນ່ນອນວ່າອຸປະກອນຂອງທ່ານຮອງຮັບ eSIM, ກະລູນາຕິດຕໍ່ຫາທາງຮ້ານ/ບ່ອນທີ່ໃຫ້ທ່ານຊື້ອຸປະກອນເພື່ອກການຢືນຢັນທີ່ຖືກຕ້ອງທີ່ສຸດ.",
+  "noteTheFollowingAppleDevicesDoNotSupportEsim": "ໝາຍເຫດ : ອຸປະກອນ Apple ຕໍ່ໄປນີ້ບໍ່ຮອງຮັບ eSIM:",
+  "allIphonesPurchasedFromChinaHongKongAndMacauDoNotSupportEsim": "iPhone ທີ່ຊື້ແຕ່ຈີນ, ຮອງກົງ ແລະ ມາເກົາ ບໍ່ຮອງຮັບ eSIM.",
+  "method2CheckDirectlyOnYourDevice": "ວິທີທີ່ 2 : ກວດສອບອຸປະກອນຂອງທ່ານໂດຍຕົງ",
+  "checkIfYourDeviceSupportsEsimAndWhetherItIsCarrierLockedByFollowing": "ກວດສອບດ້ວຍວິທີການກົດ *#06# ແລະ ໂທອອກ ,ຖ້າສະແດງ EID (eSIM ID) ແມ່ນອຸປະກອນໃຊ້ eSIM ໄດ້.",
+  "theStepsBelow": "ຂັ້ນຕອນດັ່ງລຸ່ມນີ້",
+  "yourDeviceDoesNotSupportEsim": "ອຸຸປະກອນຂອງທ່ານບໍ່ຮອງຮັບ eSIM ?",
+  "tryFindingAPhysicalSimThatIsCompatibleWithYourDevice": "ກະລຸນາລອງຄົ້ນຫາ SIM ກາດທີ່ເຂົ້າກັບອຸປະກອນທ່ານໄດ້",
+  "here": "ທີ່ນີ້",
+  "insightEngine": "ຄວາມເຂົ້າໃຈສຳລັບເຄື່ອງມື",
+  "realTimeWebSearchPoweredByGoogleGrounding": "ຄົ້ນຫາ Web ເວລານີ້ທີ່ຮອງຮັບຕົວຈິງໂດຍ Google Grounding",
+  "scouringTheWebForRealTimeInformation": "ກຳລັງຄົ້ນຫາຂໍ້ມູນແບບສົດໆໃນ Web...",
+  "news": "ຂ່າວສານ",
+  "contact": "ຕິດຕໍ່",
+  "home": "ໜ້າຫຼັກ",
+  "productInformation": "ຂໍ້ມູນສີນຄ້າ",
+  "networkProvider": "ຜູ້ໃຫ້ບໍລິການເຄືອຂ່າຍ",
+  "infoRegistration": "ລົງທະບຽນຂໍ້ມູນ",
+  "required": "ບໍ່ັງຄັບ",
+  "notRequired": "ບໍ່ບັງຄັບ",
+  "packageStartTime": "ເວລາເລີ່ມຕົ້ນແພັກເກັດ",
+  "usageTimeCalculatedFromSignal": "ການເປີດນຳໃຊ້ງານອັດຕະໂນມັດ, ການລົງຈອດແມ່ນໃຊ້ ( Plug and Play)",
+  "usageTimeCalculatedFromPurchase": "ເວລານຳໃຊ້ໄລ່ຕັ້ງແຕ່ຕອນຊື້ສິນຄ້າ",
+  "coverageArea": "ພື້ີນທີ່ຄວບຄຸມໃຫ້ບໍລິການ",
+  "deliveryMethod": "ວິທີການຈັດສົ່ງ",
+  "youWillReceiveAnEmailImmediatelyAfterPaymentWithEsimWaitForShippingAndReceiveYourPhysicalSim": "ທ່ານຈະໄດ້ຮັບຂໍ້ຄວາມທັນທີ່ພາຍຫຼັງຊຳລະເງີນກັບ eSIM. ລໍຖ້າຂົນສົ່ງແລະຮັບ SIM ທຳມະດາຂອງທ່ານ",
+  "physicalSimWillBeShippedToYourAddressWithin3-5BusinessDays": "SIM ທຳມະດາ ຈະໄດ້ສົ່ງໃຫ້ທ່ານໃນ 3 - 5 ມື້ ເຮັດວຽກ",
+  "wifiHotspot": "ຈຸດເຂົ້າເຖິງ Wi - Fi",
+  "yesSupported": "ມີ ( ຊ່ວຍເຫຼືອ )",
+  "no": "ບໍ່",
+  "networkTechnology": "ເຄືອຂ່າຍເທັກໂນໂລຢີ",
+  "dataResetTime": "ເວລາຕັ້ງຄ່າຂໍ້ມູນຄືນໃໝ່",
+  "timeResetInfo": "ໝື່ງມື້ໄລ່ຮອດ 23:59 ໂມງ ທ້ອງຖິ່ນ",
+  "unlimitedPolicy": "ແພັກເກັດບໍ່ມີກຳນົດ",
+  "appSupport": "ຊ່ວຍເຫຼືອ ແອັບພິເຄຊັນ",
+  "specifications": "ລາຍລະອຽດ",
+  "notes": "ໝາຍເຫດ",
+  "note1": "ຄຳແນະນຳການຕັ້ງຄ່າ eSIM ກ່ອນທີ່ຈະອອກເດີນທາງ (ຕ້ອງການເຊື່ອມຕໍ່ ອິນເຕີເນັດ ໃນການຕັ້ງຄ່າ), ເປີດອິນເຕີເນັດມືຖື ແລະ ປ່ຽນຈາກໃຊ້ອິນເຕີເນັດເປັນ eSIM SkySimHub.",
+  "note2": "SkySimHub ແນະນຳໃຫ້ຊື້ຢ່າງໜ່ອຍ 1 ມື້ ນອກເວລາຈູດປະສົງການນຳໃຊ້ຂອງທ່ານ ເພື່ອຫຼີກເວັ້ນວັນໝົດອາຍຸຂອງການບໍລິການນເນື່ອງຈາກຄວາມແຕກຕ່າງຂອງເຂດເວລາລະຫວ່າງປະເທດ",
+  "note3": "ແຕ່ລະ eSIM ສາມາດເປີດໃຊ້ງານໄດ້ພຽງເທື່ອດຽວ.",
+  "note4": "ເວລາທີ່ລໍຖ້າເປີດໃຊ້ງານ eSIM ແມ່ນ 30 ມື້ ເລີ່ມຕັ້ງແຕ່ການສັ່ງຊື້ສຳເລັດ.",
+  "note5": "ນະໂຍບາຍການຮັບປະກັນ : <br />+ ຮັບປະກັນການປ່ຽນແປງ ຫຼື ຄືນເງີນ 100% ໃນກໍລະນີມີບັນຫາທີ່ກ່ຽວຂ້ອງກັບຜູ້ໃຫ້ບໍລິການ <br / > ປ່ຽນແປງໃນເວລາ 1 ຊົ່ວໂມງ ຖ້າມັນເກີດຫຍັງຂື້ນໃນລະຫວ່າງການເປີດໃຊ້ງານ ຫຼື ນຳໃຊ້",
+  "note6": "ເຂົ້າເຖິງ TikTok ແລະ ChatGPT ອາດຈະມີຂໍ້ຈຳກັດຂຶ້ນກັບອຸປະກອນ, ລະບົດເຂົ້າກັນໄດ້ ແລະ ນະໂຍບາຍການຄວບຄຸມອິນເຕີເນັດທີ່ມີຢູ່ແລ້ວໃນປະດທດຈີນ",
+  "internationalTravelStarts": "ການທ່ອງທ່ຽວສາກົນເລີ່ມຕົ້ນພຽງແຕ່ 25.000​ VND, ປະຢັດໄປ 80% ທຽບກັບ Roaming ດັ້ງເດີມ.",
+  "easilyChooseSuitableSim": "ເລືອກ SIM ທີ່ເໝາະກັບຫຼາຍອຸປະກອນຢ່າງງ່າຍດາຍ",
+  "eSIMReceiveQR": "eSIM: ຮັບລະຫັດ QR ຜ່ານ Email, ສະແກນ ແລະ ຕັ້ງຄ່າ ພາຍໃນ 2 ນາທີ ເພື່ອເລີ່ມໃຊ້ທັນທີ່.",
+  "physicalSIMDelivery": "SIM ກາດ : ຈັດສົ່ງວ່ອງໄວທົ່ວໂລກ, ເຂົ້າກັນໄດ້ກັບທຸກອຸປະກອນ.",
+  "worldLeadingNetwork": "ໂຄງສ້າງພື້ນຖານເຄືອຂ່າຍຊັ້ນນຳຂອງໂລກ ຮັບປະກັນການເຊື່ອມຕໍ່ທີ່ໝັ້ນຄົງ ທຸກທີ່ ທູກເວລາ.",
+  "flexibleDiversePackages": "ແພັັກເກັດທີ່ມີຄວາມຢືດຫຍຸ໋ນ ແລະ ຫຼາກຫຼາຍ ຕອບສະໜອງຄວາມຕ້ອງການຂອງທ່ານ.",
+  "customerServiceAvailable": "ທີມງານດູແລລູກຄ້າຂອງພວກເຮົາພ້ອມໃຫ້ບໍລິການ 24 ຊົ່ວໂມງ ຕະຫຼອດການເດີນທາງຂອງທ່ານ.",
+  "supportChannels": "ຊ່ວຍໄດ້ຫລາຍຊ່ອງທາງ ເຊັ່ນ Hotline,Zalo OA ແລະ WhatsApp",
+  "commitmentPeaceOfMind": "ພວກເຮົົາສັນຍາທີ່ຈະໃຫ້ຄວາມເຊື່ອໝັ້ນ ແລະ ປະສົບການການບໍລິການຢ່າງໂປ່ງໃສ.",
+  "partnershipNetwork": "ຄູ່ຮ່ວມງານຢ່າງເປັນທາງການຂອງບໍລິສັດທົ່ວໂລກ",
+  "physicalSim": "SIM ທຳມະດາ",
+  "welcomeBack": "ຍັີນດີຕ້ອນຮັບທ່ານກັບມາ !",
+  "stayConnected": "ເຊື່ື່ອມໄດ້ທຸກທີ່ທຸກເວລາກັບ SkySimHub.",
+  "emailAddress": "ທີ່ຢູ່ Email",
+  "login": "ລົງທະບຽນ",
+  "orContinueWith": "ຫລື ສືບຕໍ່ດ້ວຍ...",
+  "checkYourEmail": "ກວດສອບ email ຂອງທ່ານ",
+  "weveSentVerificationCode": "ພວກເຮົາໄດ້ສົ່ງລະຫັດຢືນຢັນລວມ 6 ໂຕຈົນຮອດ..",
+  "verificationCode": "ລະຫັດຢືນຢັນ",
+  "codeExpiredPleaseRequestNewOne": "ລະຫັັດໄດ້ໝົດອາຍຸ. ກະລຸນາຂໍລະຫັດໃໝ່",
+  "fastSimpleRed": "ວ່ອງໄວ ງ່າຍດາຍ",
+  "globalConnectivityForTheModernTraveler": "ເຊື່ອມຕໍ່ທົ່ວໂລກໃຫ້ນັກທ່ອງທ່ຽວທັນສະໄໝ",
+  "codeExpiresIn": "ລະຫັດຈະໝົດອາຍູຫລັງຈາກນີ້",
+  "enterYourEmail": "ໃສ່ email ຂອງທ່ານ",
+  "searchCountry": "ຄົ້ນຫາປະເທດ...",
+  "suggestionsEsim": "ແນະນຳ eSIM ສຳລັບເສັ້ນທາງທ່ອງທ່ຽວຍອດນິຍົມ",
+  "support": "ຊ່ວຍເຫຼືອ",
+  "travelEsimSim": "eSIM / SIM ທ່ອງທ່ຽວ ແມ່ນຫຍັງ?",
+  "physicalTravelSim": "SIM ທ່ອງທ່ຽວທຳມະດາ",
+  "howToBuyTravelEsimSim": "ວິິທີຊື້ eSIM/SIM ທ່ອງທ່ຽວ",
+  "installationGuide": "ແນະນຳການຕັ້ັ້ງຄ່າ ແລະ ເປີດໃຊ້ງານ eSIM",
+  "emailAndEsimQrCode": "Email ແລະ ລະຫັດ QR eSIM",
+  "installationGuideForIphoneIos": "ແນະນຳຳການຕັ້ງຄ່າ iPhone ( iOS)",
+  "installationGuideForAndroid": "ແນະນຳການຕັ້ງຄ່າ Android",
+  "buyNow": "ຊື້ເລີຍ",
+  "expiredAt": "ໝົົດເວລາເຂົ້າ",
+  "notActive": "ຍັງບໍ່ເປີດນຳໃຊ້",
+  "active": "ກຳລັງເຄື່ອນໄຫວ",
+  "finished": "ນຳໃຊ້ໝົດແລ້ວ",
+  "expired": "ໝົດອາຍຸການນຳໃຊ້",
+  "unknown": "ບໍ່ກຳນົດ",
+  "paymentSuccess": "ການຊຳລະສຳເລັດ",
+  "paymentFailed": "ການຊຳລະລົ້ມເຫຼວ",
+  "activationMethod": "ວິທີການເປີດໃຊ້ງານ",
+  "productDetailsMissing": "ກະລຸນາດາວໂຫຼດໜ້າເວັບຄືນໃໝ່",
+  "goToShop": "ໄປທີ່ຮ້ານ",
+  "english": "ພາສາອັງກິດ",
+  "vietnamese": "ພາສາຫວຽດ",
+  "numberOfDays": "ຈຳນວນມື້",
+  "verified": "ຢືນຢັນແລ້ວ",
+  "highSpeed": "ຄວາມໄວສູງ",
+  "selectADataPackageForYourDestination": "ເລືອກແພັກເກັດອິນເຕີເນັດທີ່ເໝາະກັບປາຍທາງຂອງທ່ານ",
+  "checkIfYourPhoneSupportsESIMTechnology": "ກວດກາເບິ່ງໂທລະສັບຂອງທ່ານມີການຊ່ວຍເຫຼືອເທັກໂນໂລຢີ",
+  "scanTheQRCodeAndConnectToHighSpeedData": "ສະແກນ QR ແລະ ເຊື່ອມຕໍ່ກັບອິນເຕີເນັດຄວາມໄວສູງ",
+  "register": "ລົງທະບຽນ",
+  "lao": "ພາສາລາວ"
+}

+ 2 - 1
EsimLao/esim-vite/src/i18n/locales/vi.json

@@ -226,5 +226,6 @@
   "selectADataPackageForYourDestination": "Sau khi xác nhận thiết bị, bạn có thể dễ dàng lựa chọn gói dữ liệu phù hợp với điểm đến và thời gian sử dụng. Các gói cước được thiết kế đa dạng về dung lượng, thời hạn và khu vực phủ sóng, đáp ứng mọi nhu cầu từ lướt web cơ bản, làm việc online đến sử dụng mạng xã hội và xem video.",
   "checkIfYourPhoneSupportsESIMTechnology": "Trước khi mua, bạn chỉ cần kiểm tra xem điện thoại của mình có hỗ trợ eSIM. Hệ thống sẽ tự động xác định khả năng tương thích của thiết bị để đảm bảo bạn lựa chọn đúng sản phẩm phù hợp.  Cú pháp kiểm tra: *#06#",
   "scanTheQRCodeAndConnectToHighSpeedData": "Sau khi hoàn tất thanh toán, eSIM sẽ được gửi trực tiếp về email hoặc tài khoản của bạn dưới dạng mã QR. Chỉ cần quét mã và thực hiện vài bước cài đặt đơn giản, eSIM sẽ được kích hoạt ngay lập tức. Đối với SIM vật lý, bạn chỉ cần lắp SIM vào thiết bị và bật dữ liệu di động để bắt đầu sử dụng. Không cần đăng ký phức tạp, không cần chờ đợi xác minh thủ công.",
-  "register": "Đăng ký"
+  "register": "Đăng ký",
+  "lao": "Tiếng Lào"
 }

+ 1 - 1
EsimLao/esim-vite/src/logic/loigicUtils.ts

@@ -16,7 +16,7 @@ export const setWithExpiry = <T>(
 
   const item = {
     value,
-    expiry: ttlMs ? now + ttlMs : now + 10 * 60 * 1000, // Default 10 minutes
+    expiry: ttlMs ? now + ttlMs : now + 60 * 60 * 1000, // Default 1 hour
   };
 
   localStorage.setItem(key, JSON.stringify(item));

+ 15 - 7
EsimLao/esim-vite/src/pages/checkout/CheckoutView.tsx

@@ -18,23 +18,31 @@ import {
 import { useAppDispatch, useAppSelector } from "../../hooks/useRedux";
 import { productApi } from "../../apis/productApi";
 import { openPopup } from "../../features/popup/popupSlice";
-import { formatCurrency, formatNumber } from "../../logic/loigicUtils";
+import {
+  formatCurrency,
+  formatNumber,
+  getWithExpiry,
+} from "../../logic/loigicUtils";
 import { useTranslation } from "react-i18next";
 import { format } from "path";
+import { AccountInfo } from "../../services/auth/types";
 
 const CheckoutView = () => {
   const navigate = useNavigate();
   const dispatch = useAppDispatch();
   const { t } = useTranslation();
   const loading = useAppSelector((state) => state.loading);
-  const accountInfo = localStorage.getItem("accountInfo");
+  const accountInfo = getWithExpiry("accountInfo") as AccountInfo | null;
   const [paymentMethod, setPaymentMethod] = useState("card");
   const [form, setForm] = useState({
-    firstName: "",
-    lastName: "",
-    email: accountInfo != null ? JSON.parse(accountInfo).email : "",
-    confirmEmail: accountInfo != null ? JSON.parse(accountInfo).email : "",
-    phone: "",
+    firstName: accountInfo != null ? accountInfo.fullName?.split(" ")[0] : "",
+    lastName:
+      accountInfo != null
+        ? accountInfo.fullName?.split(" ").slice(1).join(" ")
+        : "",
+    email: accountInfo != null ? accountInfo.email : "",
+    confirmEmail: accountInfo != null ? accountInfo.email : "",
+    phone: accountInfo != null ? accountInfo.msisdn : "",
   });
   const [agreements, setAgreements] = useState({
     terms: false,

+ 36 - 2
EsimLao/esim-vite/src/pages/home/HomeView.tsx

@@ -14,6 +14,8 @@ import { authApi } from "../../apis/authApi";
 import { accountLogin } from "../../features/account/accuntSlice";
 import { useTranslation } from "react-i18next";
 import i18n from "../../i18n";
+import CryptoJS from "crypto-js";
+import { getWithExpiry, setWithExpiry } from "../../logic/loigicUtils";
 
 const HomeView: React.FC = () => {
   const [simType, setSimType] = useState<"eSIM" | "Physical">("eSIM");
@@ -21,12 +23,12 @@ const HomeView: React.FC = () => {
   const navigate = useNavigate();
   const dispatch = useAppDispatch();
   const { t } = useTranslation();
-  const langNow = localStorage.getItem("lang") || "en";
+  const langNow = (getWithExpiry("selectedLanguage") as string) || "en";
 
   useEffect(() => {
     // set language in i18n
     i18n.changeLanguage(langNow);
-    localStorage.setItem("lang", langNow);
+    setWithExpiry("selectedLanguage", langNow);
   }, [langNow]);
 
   useEffect(() => {
@@ -39,6 +41,15 @@ const HomeView: React.FC = () => {
 
     // google callback
     const code = searchParams.get("code");
+    const partner = searchParams.get("partner");
+    const data = searchParams.get("data");
+    const lang = searchParams.get("lang");
+
+    if (lang) {
+      i18n.changeLanguage(lang);
+      setWithExpiry("selectedLanguage", lang);
+    }
+
     if (status) {
       console.log("URL Params:", params);
       if (status === "0") {
@@ -66,9 +77,32 @@ const HomeView: React.FC = () => {
     } else if (code) {
       console.log("Handling Google callback with code:", code);
       handleGoogleCallback(code);
+    } else if (partner && data) {
+      console.log("Handling partner callback with partner:", partner);
+      // You can implement partner callback handling here
+      // aes decryption can be done in the backend for security reasons but for test, we do it here
+      setWithExpiry("selectedPartner", partner);
+      handlePartnerCallback(partner, data);
     }
   }, []);
 
+  const handlePartnerCallback = async (partner: string, data: string) => {
+    try {
+      const response = await authApi.partnerLogin({
+        partner,
+        data: data,
+      });
+      if (response.errorCode === "0") {
+        dispatch(accountLogin(response.data));
+        navigate("/");
+      } else {
+        console.error("Google callback failed:", response.message);
+      }
+    } catch (error) {
+      console.error("Partner callback error:", error);
+    }
+  };
+
   const handleGoogleCallback = async (code: string) => {
     try {
       const response = await authApi.googleCallback({ code });

+ 72 - 63
EsimLao/esim-vite/src/pages/home/components/HomeFaq.tsx

@@ -9,11 +9,13 @@ import {
 } from "../../../features/loading/loadingSlice";
 import { useAppDispatch } from "../../../hooks/useRedux";
 import { useTranslation } from "react-i18next";
+import { getWithExpiry } from "../../../logic/loigicUtils";
 
 const HomeFaq = () => {
   const [openFaqIndex, setOpenFaqIndex] = useState<number | null>(0);
   const dispatch = useAppDispatch();
   const { t } = useTranslation();
+  const partner = getWithExpiry("selectedPartner") as string | null;
   const { data: loadFaqsData = [] } = useQuery<Faq[]>({
     queryKey: [DataCacheKey.FAQS],
     queryFn: async (): Promise<Faq[]> => {
@@ -35,86 +37,93 @@ const HomeFaq = () => {
     staleTime: staleTime,
   });
 
+  useEffect(() => {
+    window.scrollTo({ top: 0, behavior: "smooth" });
+    console.log("parter: ", partner);
+  }, []);
+
   const toggleFaq = (index: number) => {
     setOpenFaqIndex(openFaqIndex === index ? null : index);
   };
 
   return (
-    <>
-      <section className="py-16 md:py-24 bg-white px-4 border-t border-slate-50">
-        <div className="max-w-4xl mx-auto">
-          <div className="text-center mb-12 md:mb-20">
-            <h2 className="text-3xl md:text-6xl font-black tracking-tight text-[#EE0434] mb-4">
-              {t("frequently")}{" "}
-              <span className="text-slate-900">{t("askedQuestions")}</span>
-            </h2>
-            <p className="text-slate-500 text-sm md:text-xl font-medium">
-              {t("weAreAlwaysHereToSupportYou")}
-            </p>
-          </div>
+    partner !== "laotravel" && (
+      <>
+        <section className="py-16 md:py-24 bg-white px-4 border-t border-slate-50">
+          <div className="max-w-4xl mx-auto">
+            <div className="text-center mb-12 md:mb-20">
+              <h2 className="text-3xl md:text-6xl font-black tracking-tight text-[#EE0434] mb-4">
+                {t("frequently")}{" "}
+                <span className="text-slate-900">{t("askedQuestions")}</span>
+              </h2>
+              <p className="text-slate-500 text-sm md:text-xl font-medium">
+                {t("weAreAlwaysHereToSupportYou")}
+              </p>
+            </div>
 
-          <div className="space-y-4">
-            {loadFaqsData.map((faq, index) => (
-              <div key={index} className="border-b border-slate-100">
-                <button
-                  onClick={() => toggleFaq(index)}
-                  className="w-full flex justify-between items-center py-6 text-left group transition-all"
-                >
-                  <span
-                    className={`text-base md:text-2xl font-black transition-colors ${
-                      openFaqIndex === index
-                        ? "text-[#EE0434]"
-                        : "text-slate-800 group-hover:text-[#EE0434]"
-                    }`}
+            <div className="space-y-4">
+              {loadFaqsData.map((faq, index) => (
+                <div key={index} className="border-b border-slate-100">
+                  <button
+                    onClick={() => toggleFaq(index)}
+                    className="w-full flex justify-between items-center py-6 text-left group transition-all"
                   >
-                    {faq.question}
-                  </span>
+                    <span
+                      className={`text-base md:text-2xl font-black transition-colors ${
+                        openFaqIndex === index
+                          ? "text-[#EE0434]"
+                          : "text-slate-800 group-hover:text-[#EE0434]"
+                      }`}
+                    >
+                      {faq.question}
+                    </span>
+                    <div
+                      className={`shrink-0 w-6 h-6 md:w-8 md:h-8 flex items-center justify-center transition-all duration-500 ${
+                        openFaqIndex === index
+                          ? "text-[#EE0434] rotate-180"
+                          : "text-slate-300"
+                      }`}
+                    >
+                      <svg
+                        className="w-5 h-5 md:w-7 md:h-7"
+                        fill="none"
+                        stroke="currentColor"
+                        viewBox="0 0 24 24"
+                      >
+                        <path
+                          strokeLinecap="round"
+                          strokeLinejoin="round"
+                          strokeWidth={3}
+                          d="M19 9l-7 7-7-7"
+                        />
+                      </svg>
+                    </div>
+                  </button>
                   <div
-                    className={`shrink-0 w-6 h-6 md:w-8 md:h-8 flex items-center justify-center transition-all duration-500 ${
+                    className={`grid transition-all duration-500 ease-in-out ${
                       openFaqIndex === index
-                        ? "text-[#EE0434] rotate-180"
-                        : "text-slate-300"
+                        ? "grid-rows-[1fr] opacity-100"
+                        : "grid-rows-[0fr] opacity-0"
                     }`}
                   >
-                    <svg
-                      className="w-5 h-5 md:w-7 md:h-7"
-                      fill="none"
-                      stroke="currentColor"
-                      viewBox="0 0 24 24"
-                    >
-                      <path
-                        strokeLinecap="round"
-                        strokeLinejoin="round"
-                        strokeWidth={3}
-                        d="M19 9l-7 7-7-7"
+                    <div className="overflow-hidden">
+                      {/* show html */}
+                      <div
+                        dangerouslySetInnerHTML={{ __html: faq.answer }}
+                        className="text-slate-600 text-sm md:text-xl leading-relaxed font-medium pb-8"
                       />
-                    </svg>
-                  </div>
-                </button>
-                <div
-                  className={`grid transition-all duration-500 ease-in-out ${
-                    openFaqIndex === index
-                      ? "grid-rows-[1fr] opacity-100"
-                      : "grid-rows-[0fr] opacity-0"
-                  }`}
-                >
-                  <div className="overflow-hidden">
-                    {/* show html */}
-                    <div
-                      dangerouslySetInnerHTML={{ __html: faq.answer }}
-                      className="text-slate-600 text-sm md:text-xl leading-relaxed font-medium pb-8"
-                    />
-                    {/* <p className="text-slate-600 text-sm md:text-xl leading-relaxed font-medium pb-8">
+                      {/* <p className="text-slate-600 text-sm md:text-xl leading-relaxed font-medium pb-8">
                       {faq.answer}
                     </p> */}
+                    </div>
                   </div>
                 </div>
-              </div>
-            ))}
+              ))}
+            </div>
           </div>
-        </div>
-      </section>
-    </>
+        </section>
+      </>
+    )
   );
 };
 

+ 0 - 1
EsimLao/esim-vite/src/pages/home/components/HomeProduct.tsx

@@ -1,5 +1,4 @@
 import { useAppDispatch, useAppSelector } from "../../../hooks/useRedux";
-import { SimProduct } from "@/src/services/types";
 import { useMutation, useQuery } from "@tanstack/react-query";
 import React, { useState, useEffect, useCallback } from "react";
 import { useNavigate } from "react-router-dom";

+ 1 - 1
EsimLao/esim-vite/src/pages/news/NewsView.tsx

@@ -3,7 +3,7 @@ import { useNavigate, Link } from "react-router-dom";
 import { NewsArticle } from "../../services/types";
 import { DataCacheKey, staleTime } from "../../global/constants";
 import { useQuery } from "@tanstack/react-query";
-import { Article, Category } from "@/src/services/article/types";
+import { Article, Category } from "../../services/article/types";
 import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
 import { useAppDispatch } from "../../hooks/useRedux";
 import { articleApi } from "../../apis/articleApi";

+ 7 - 7
EsimLao/esim-vite/src/pages/support/SupportView.tsx

@@ -200,7 +200,7 @@ const SupportView: React.FC = () => {
         {
           childId: 2.1,
           title: "Hướng dẫn cài đặt eSIM trên các thiết bị",
-          content: `<p>C&agrave;i đặt eSIM tr&ecirc;n c&aacute;c thiết bị tương th&iacute;ch (iPhone, Samsung, Google Pixel,&hellip;) rất đơn giản v&agrave; nhanh ch&oacute;ng. Sau khi thanh to&aacute;n, bạn sẽ nhận được email từ nh&agrave; cung cấp (như InfiGate) chứa th&ocirc;ng tin g&oacute;i eSIM v&agrave; m&atilde; QR để k&iacute;ch hoạt.</p>
+          content: `<p>C&agrave;i đặt eSIM tr&ecirc;n c&aacute;c thiết bị tương th&iacute;ch (iPhone, Samsung, Google Pixel,&hellip;) rất đơn giản v&agrave; nhanh ch&oacute;ng. Sau khi thanh to&aacute;n, bạn sẽ nhận được email từ nh&agrave; cung cấp (như Skysim) chứa th&ocirc;ng tin g&oacute;i eSIM v&agrave; m&atilde; QR để k&iacute;ch hoạt.</p>
 <p>To&agrave;n bộ qu&aacute; tr&igrave;nh c&agrave;i đặt v&agrave; k&iacute;ch hoạt eSIM được thực hiện <strong>ho&agrave;n to&agrave;n trực tuyến</strong>, chỉ mất khoảng <strong>3&ndash;5 ph&uacute;t</strong>.</p>`,
         },
         {
@@ -251,9 +251,9 @@ const SupportView: React.FC = () => {
 <li>7. Tại m&agrave;n<strong> Cellular Setup Complete</strong>/<strong>Ho&agrave;n th&agrave;nh c&agrave;i đặt di động,</strong> chọn <strong>Done.</strong>
 <img src="https://skysimhub.vn/assets/guide/2_2.png" alt="Activate eSIM on iOS"/></li>
 </ol>
-<h4><strong>2.2.1.3. C&aacute;ch 3: Sử dụng n&uacute;t &ldquo;Install Now/C&agrave;i đặt ngay&rdquo; tr&ecirc;n m&agrave;n h&igrave;nh My eSIM tr&ecirc;n app hoặc email InfiGate gửi.</strong></h4>
+<h4><strong>2.2.1.3. C&aacute;ch 3: Sử dụng n&uacute;t &ldquo;Install Now/C&agrave;i đặt ngay&rdquo; tr&ecirc;n m&agrave;n h&igrave;nh My eSIM tr&ecirc;n app hoặc email Skysim gửi.</strong></h4>
 <ol>
-<li>Truy cập app InfiGate &gt; My eSIM hoặc mở email eSIM đ&atilde; được InfiGate gửi.</li>
+<li>Truy cập app Skysim &gt; My eSIM hoặc mở email eSIM đ&atilde; được Skysim gửi.</li>
 <li>Bấm n&uacute;t Instal Now/C&agrave;i đặt ngay trong m&agrave;n h&igrave;nh hiển thị chi tiết</li>
 <li>Tại m&agrave;n<strong> Activate eSIM/K&iacute;ch hoạt eSIM</strong>, chọn<strong> Tiếp tục</strong>/<strong>Continue</strong>.</li>
 <li>Tại m&agrave;n <strong>Cellular Setup Complete</strong>/<strong>Ho&agrave;n th&agrave;nh c&agrave;i đặt di động</strong>, chọn <strong>Done</strong>.</li>
@@ -261,8 +261,8 @@ const SupportView: React.FC = () => {
 <h4><strong>2.2.1.4. K&iacute;ch hoạt eSIM du lịch tr&ecirc;n iPhone</strong></h4>
 <ol>
 <li>Sau khi đến quốc gia sử dụng eSIM, v&agrave;o <strong>Settings</strong>/<strong>C&agrave;i đặt</strong> &gt; <strong>Cellular</strong>/<strong>Di động</strong>.</li>
-<li>Bật <strong>Turn on this line </strong>/ <strong>Bật d&ograve;ng n&agrave;y </strong>v&agrave; <strong>Data roaming </strong>/ <strong>Chuyển v&ugrave;ng dữ liệu</strong> cho d&ograve;ng eSIM InfiGate.</li>
-<li>V&agrave;o <strong>Cellular</strong>/<strong>Di động</strong>, chọn <strong>Cellular Data</strong>/<strong>Dữ liệu di động</strong> &gt; chọn <strong>InfiGate eSIM</strong> l&agrave; k&ecirc;nh sử dụng dữ liệu di động ch&iacute;nh.</li>
+<li>Bật <strong>Turn on this line </strong>/ <strong>Bật d&ograve;ng n&agrave;y </strong>v&agrave; <strong>Data roaming </strong>/ <strong>Chuyển v&ugrave;ng dữ liệu</strong> cho d&ograve;ng eSIM Skysim.</li>
+<li>V&agrave;o <strong>Cellular</strong>/<strong>Di động</strong>, chọn <strong>Cellular Data</strong>/<strong>Dữ liệu di động</strong> &gt; chọn <strong>Skysim eSIM</strong> l&agrave; k&ecirc;nh sử dụng dữ liệu di động ch&iacute;nh.</li>
 </ol>
 <p>&nbsp;<em>Bạn đ&atilde; c&oacute; thể sử dụng eSIM để truy cập Internet.<br /> </em></p>
 <h4><strong>2.2.1.5. X&oacute;a eSIM tr&ecirc;n iPhone</strong></h4>
@@ -293,8 +293,8 @@ const SupportView: React.FC = () => {
 <h4><strong>2.2.2.3. K&iacute;ch hoạt eSIM du lịch tr&ecirc;n Samsung</strong></h4>
 <ol>
 <li>Sau khi đến nơi, v&agrave;o <strong>Settings</strong>/<strong>C&agrave;i đặt</strong> &gt; <strong>Connections/Kết nối</strong>&gt;<strong> Mạng di động</strong>&gt; bật <strong>Chuyển v&ugrave;ng dữ liệu/Data Roaming</strong></li>
-<li>Quay trở lại m&agrave;n <strong>Connection/Kết nối</strong>&gt; V&agrave;o <strong>SIM Manager/Quản l&yacute; SIM&gt; </strong>&nbsp;bật <strong>eSIM InfiGate</strong></li>
-<li>V&agrave;o <strong>Mobile Data/Dữ liệu di dộng</strong>, chọn <strong>InfiGate eSIM</strong> l&agrave; mạng ch&iacute;nh</li>
+<li>Quay trở lại m&agrave;n <strong>Connection/Kết nối</strong>&gt; V&agrave;o <strong>SIM Manager/Quản l&yacute; SIM&gt; </strong>&nbsp;bật <strong>eSIM Skysim</strong></li>
+<li>V&agrave;o <strong>Mobile Data/Dữ liệu di dộng</strong>, chọn <strong>Skysim eSIM</strong> l&agrave; mạng ch&iacute;nh</li>
 </ol>
 <p><em>eSIM sẵn s&agrave;ng để d&ugrave;ng Internet.</em></p>
 <h4><strong>2.2.2.4. X&oacute;a eSIM tr&ecirc;n Android</strong></h4>

+ 1 - 0
EsimLao/esim-vite/src/services/auth/types.ts

@@ -6,6 +6,7 @@ export interface AccountInfo {
   accessToken: string;
   refreshToken: string;
   expiresAt: string;
+  msisdn?: string;
 }
 
 export interface RequestOtpResponse {

+ 8 - 1
EsimLao/esim-vite/src/services/product/type.ts

@@ -172,5 +172,12 @@ export interface DataUsage {
   isUnlimited: number;
   status: number;
   dataUnit: string;
-  usageData: number
+  usageData: number;
+}
+
+export interface LaoTravelAuthResponse {
+  username: string;
+  fullName: string;
+  phoneNumber: string;
+  email: string;
 }

BIN
EsimLao/lib/DotnetLib.dll