student 4 tygodni temu
rodzic
commit
45e82d9420

+ 21 - 5
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/Business/User/UserBusinessImpl.cs

@@ -321,6 +321,7 @@ namespace Kitty_Fall.Apis.Business.User
                         x => x.Msisdn == msisdn
                             && x.Status == true
                             && x.Renew == 1
+                            && x.ExpireTime >= DateTime.Now
                     )
                     .OrderByDescending(x => x.RegisterTime)
                     .ThenByDescending(x => x.RegisterId)
@@ -332,7 +333,21 @@ namespace Kitty_Fall.Apis.Business.User
                 }
                 var now = DateTime.Now;
                 var startOfMonth = new DateTime(now.Year, now.Month, 1);
-                var endOfMonth = startOfMonth.AddMonths(1).AddDays(-1);
+                var startOfNextMonth = startOfMonth.AddMonths(1);
+                var monthKey = now.Year * 100 + now.Month;
+                var totalTurn = await dbContext.LuckySpins.AsNoTracking()
+                    .Where(x => x.Msisdn == msisdn)
+                    .SumAsync(x => (int?)x.Added) ?? 0;
+                var totalPoint = await dbContext.GameMonthlyRankings.AsNoTracking()
+                    .Where(x => x.Msisdn == msisdn && x.MonthKey == monthKey)
+                    .Select(x => (long?)x.TotalScore)
+                    .FirstOrDefaultAsync();
+
+                totalPoint ??= await dbContext.GamePlaySessions.AsNoTracking()
+                    .Where(x => x.Msisdn == msisdn
+                        && x.StartTime >= startOfMonth
+                        && x.StartTime < startOfNextMonth)
+                    .SumAsync(x => (long?)x.TotalScore) ?? 0;
 
                 return CommonLogic.BuildResponse(
                     url,
@@ -344,8 +359,9 @@ namespace Kitty_Fall.Apis.Business.User
                         accountUser = userAccounts,
                         regInfos = userPackage,
                         msisdn = userAccounts.Msisdn,
-                        totalTurn = 0,
-                        totalpoin = 0
+                        totalTurn,
+                        totalPoint,
+                        totalpoin = totalPoint
                     }
                   );
             }
@@ -754,7 +770,7 @@ namespace Kitty_Fall.Apis.Business.User
 
                 await dbContext.SaveChangesAsync();
                 var messOTp = @Lang.messOtp.Replace("%OTP%", mOtp);
-                await CommonLogic.SendMtToUserAsync(dbContext, msisdn, messOTp);
+                //await CommonLogic.SendMtToUserAsync(dbContext, msisdn, messOTp);
                 return CommonLogic.BuildResponse(
                     url,
                     json,
@@ -877,7 +893,7 @@ namespace Kitty_Fall.Apis.Business.User
                     transactionId = (isTrans == true ? otpSender.TransactionId :"")
                 };
                 var messOTp = @Lang.messOtp.Replace("%OTP%", mOtp);
-                await CommonLogic.SendMtToUserAsync(dbContext, msisdn, messOTp);
+                //await CommonLogic.SendMtToUserAsync(dbContext, msisdn, messOTp);
 
                 return CommonLogic.BuildResponse(
                     url,

+ 4 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/Kitty_Fall.Apis.csproj

@@ -13,6 +13,10 @@
 	<ItemGroup>
 		<ProjectReference Include="..\Common\Common.csproj" />
 	</ItemGroup>
+	<ItemGroup>
+		<Watch Remove="bin\**\*.log" />
+		<Watch Remove="logs\**\*" />
+	</ItemGroup>
 	<ItemGroup>
 		<Reference Include="DotnetLib">
 			<HintPath>..\..\..\..\lib\DotnetLib.dll</HintPath>

+ 1 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/log4net.config

@@ -14,7 +14,7 @@
 	<appender name="file" type="Common.DailyRollingFileAppender, Common">
 		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
 		<!-- Thư mục chứa log (phải kết thúc bằng dấu \) -->
-		<file value="../../logs/" />
+		<file value="C:\AppLogs\KittyFall\apis\log.txt" />
 		<appendToFile value="true" />
 		<!-- Rolling theo NGÀY: mỗi ngày 1 file riêng, vd 21.05.2026.log -->
 		<rollingStyle value="Date" />

+ 178 - 2
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Controllers/HomeController.cs

@@ -1,21 +1,32 @@
+using Common;
 using Common.Extension;
+using Common.Http.Apis.Request;
+using Kitty_Fall.Website.Helpers;
 using Kitty_Fall.Website.Models;
 using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
 using System.Diagnostics;
+using System.Net.Http.Headers;
+using System.Text;
 
 namespace Kitty_Fall.Website.Controllers
 {
     public class HomeController : Controller
     {
         private readonly ILogger<HomeController> _logger;
+        private readonly IConfiguration _configuration;
+        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(HomeController));
 
-        public HomeController(ILogger<HomeController> logger)
+        public HomeController(ILogger<HomeController> logger, IConfiguration configuration)
         {
             _logger = logger;
+            _configuration = configuration;
         }
 
-        public IActionResult Index()
+        public async Task<IActionResult> Index()
         {
+            await LoadHomeDataAsync();
             return View();
         }
 
@@ -37,6 +48,171 @@ namespace Kitty_Fall.Website.Controllers
             return Json(new { success = true });
         }
 
+        private async Task LoadHomeDataAsync()
+        {
+            var msisdn = HttpContext.Session.GetComplexData<string>("msisdn") ?? "";
+            var profilePicture = HttpContext.Session.GetComplexData<string>("profilePicture") ?? "";
+            var totalTurn = HttpContext.Session.GetComplexData<long?>("totalTurn") ?? 0;
+            var totalPoint = HttpContext.Session.GetComplexData<long?>("totalPoint") ?? 0;
+
+            ViewBag.Msisdn = msisdn;
+            ViewBag.ProfilePicture = profilePicture;
+            ViewBag.TotalTurn = totalTurn;
+            ViewBag.TotalPoint = totalPoint;
+
+            await LoadPackageDataAsync();
+
+            if (string.IsNullOrWhiteSpace(msisdn))
+            {
+                return;
+            }
+
+            var baseUrl = GetParameter("url");
+            var apiToken = HttpContext.Session.GetString("apiToken");
+            if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(apiToken))
+            {
+                return;
+            }
+
+            try
+            {
+                var language = GetCurrentLanguage();
+                using var client = new HttpClient();
+                using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl + UrlConstant.GetProfileInfo)
+                {
+                    Content = new StringContent(
+                        JsonConvert.SerializeObject(new GetProfileInfoReq
+                        {
+                            msisdn = msisdn,
+                            language = language
+                        }),
+                        Encoding.UTF8,
+                        "application/json")
+                };
+                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
+
+                using var response = await client.SendAsync(request);
+                var responseText = await response.Content.ReadAsStringAsync();
+                var jObject = JObject.Parse(responseText);
+                if (ReadErrorCode(jObject) != CommonErrorCode.SUCCESS)
+                {
+                    log.Warn("Load home profile failed: " + responseText);
+                    return;
+                }
+
+                var data = jObject["data"];
+                var accountUser = data?["accountUser"] ?? data?["AccountUser"];
+                var loadedMsisdn = data?["msisdn"]?.ToString()
+                    ?? data?["Msisdn"]?.ToString()
+                    ?? accountUser?["msisdn"]?.ToString()
+                    ?? accountUser?["Msisdn"]?.ToString()
+                    ?? msisdn;
+                var loadedPicture = accountUser?["picture"]?.ToString()
+                    ?? accountUser?["Picture"]?.ToString()
+                    ?? profilePicture;
+                var loadedTurn = ReadLong(data?["totalTurn"] ?? data?["TotalTurn"]);
+                var loadedPoint = ReadLong(
+                    data?["totalPoint"]
+                    ?? data?["TotalPoint"]
+                    ?? data?["totalpoin"]
+                    ?? data?["Totalpoin"]
+                    ?? data?["totalPointsThisMonth"]
+                    ?? data?["TotalPointsThisMonth"]);
+
+                HttpContext.Session.SetComplexData("msisdn", loadedMsisdn);
+                HttpContext.Session.SetComplexData("profilePicture", loadedPicture);
+                HttpContext.Session.SetComplexData("totalTurn", loadedTurn);
+                HttpContext.Session.SetComplexData("totalPoint", loadedPoint);
+
+                ViewBag.Msisdn = loadedMsisdn;
+                ViewBag.ProfilePicture = loadedPicture;
+                ViewBag.TotalTurn = loadedTurn;
+                ViewBag.TotalPoint = loadedPoint;
+            }
+            catch (Exception ex)
+            {
+                log.Error("LoadHomeDataAsync error: ", ex);
+            }
+        }
+
+        private async Task LoadPackageDataAsync()
+        {
+            var baseUrl = GetParameter("url");
+            if (string.IsNullOrWhiteSpace(baseUrl))
+            {
+                return;
+            }
+
+            try
+            {
+                var productName = GetParameter("productName");
+                var language = GetCurrentLanguage();
+                var packages = await PackagesCacheHelper.GetOrLoadAsync(log, HttpContext.Session, baseUrl, language);
+                var selectedPackage = packages.FirstOrDefault(p =>
+                    !string.IsNullOrWhiteSpace(productName)
+                    && p.TryGetValue("productName", out var value)
+                    && string.Equals(value?.ToString(), productName, StringComparison.OrdinalIgnoreCase))
+                    ?? packages.FirstOrDefault();
+
+                if (selectedPackage == null)
+                {
+                    return;
+                }
+
+                ViewBag.PackageProductName = ReadPackageString(selectedPackage, "productName");
+                ViewBag.PackageFee = ReadPackageDecimal(selectedPackage, "fee");
+                ViewBag.PackageTurns = ReadPackageInt(selectedPackage, "numberSpin");
+                ViewBag.PackageExpireDays = ReadPackageInt(selectedPackage, "expireDays");
+            }
+            catch (Exception ex)
+            {
+                log.Error("LoadPackageDataAsync error: ", ex);
+            }
+        }
+
+        private string GetParameter(string key)
+        {
+            return _configuration.GetSection(key).Value
+                ?? _configuration.GetSection("Url").Value
+                ?? "";
+        }
+
+        private string GetCurrentLanguage()
+        {
+            var sessionLang = HttpContext.Session.GetComplexData<string>("culture");
+            if (!string.IsNullOrWhiteSpace(sessionLang)) return sessionLang;
+            return System.Globalization.CultureInfo.CurrentUICulture.Name.StartsWith("vi", StringComparison.OrdinalIgnoreCase) ? "vi" : "en";
+        }
+
+        private static int ReadErrorCode(JObject jObject)
+        {
+            var value = jObject["errorCode"]?.ToString();
+            return int.TryParse(value, out var errorCode) ? errorCode : -1;
+        }
+
+        private static long ReadLong(JToken? token)
+        {
+            if (token == null) return 0;
+            return long.TryParse(token.ToString(), out var value) ? value : 0;
+        }
+
+        private static string ReadPackageString(Dictionary<string, object?> package, string key)
+        {
+            return package.TryGetValue(key, out var value) ? value?.ToString() ?? "" : "";
+        }
+
+        private static decimal ReadPackageDecimal(Dictionary<string, object?> package, string key)
+        {
+            if (!package.TryGetValue(key, out var value) || value == null) return 0;
+            return decimal.TryParse(value.ToString(), out var number) ? number : 0;
+        }
+
+        private static int ReadPackageInt(Dictionary<string, object?> package, string key)
+        {
+            if (!package.TryGetValue(key, out var value) || value == null) return 0;
+            return int.TryParse(value.ToString(), out var number) ? number : 0;
+        }
+
         [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
         public IActionResult Error()
         {

+ 136 - 32
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Controllers/LoginController.cs

@@ -31,23 +31,89 @@ namespace Kitty_Fall.Website.Controllers
 
             ViewBag.ReturnUrl = NormalizeReturnUrl(returnUrl);
             ViewBag.CurrentMsisdn = currentMsisdn;
-            ViewBag.PendingOtpMsisdn = HttpContext.Session.GetComplexData<string>(PendingOtpMsisdnKey);
-            ViewBag.PendingOtpTransactionId = HttpContext.Session.GetComplexData<string>(PendingOtpTransactionKey);
+            ViewBag.PendingOtpMsisdn = HttpContext.Session.GetString(PendingOtpMsisdnKey);
+            ViewBag.PendingOtpTransactionId = HttpContext.Session.GetString(PendingOtpTransactionKey);
+            ViewBag.LoginError = TempData["LoginError"]?.ToString();
+            ViewBag.LoginInfo = TempData["LoginInfo"]?.ToString();
             return View();
         }
 
+        [HttpPost]
+        [ValidateAntiForgeryToken]
+        public async Task<IActionResult> SendOtpPage(string msisdn, string? returnUrl = null)
+        {
+            var result = await SendOtpCore(msisdn);
+            if (result.ErrorCode == CommonErrorCode.SUCCESS)
+            {
+                PrepareLoginView(returnUrl, result.Msisdn, result.TransactionId, null, result.Message);
+            }
+            else
+            {
+                PrepareLoginView(returnUrl, msisdn?.Trim(), "", result.Message, null);
+            }
+
+            return View("Index");
+        }
+
+        [HttpPost]
+        [ValidateAntiForgeryToken]
+        public async Task<IActionResult> ConfirmOtpPage(string msisdn, string transactionId, string otp, string? returnUrl = null)
+        {
+            var result = await ConfirmOtpCore(msisdn, transactionId, otp);
+            if (result.ErrorCode == CommonErrorCode.SUCCESS)
+            {
+                return Redirect(NormalizeReturnUrl(returnUrl));
+            }
+
+            PrepareLoginView(returnUrl, msisdn?.Trim(), transactionId?.Trim(), result.Message, null);
+            return View("Index");
+        }
+
         [HttpPost]
         public async Task<IActionResult> SendOtpAction(string msisdn)
+        {
+            var result = await SendOtpCore(msisdn);
+            return Json(new
+            {
+                errorCode = result.ErrorCode,
+                message = result.Message,
+                msisdn = result.Msisdn,
+                transactionId = result.TransactionId
+            });
+        }
+
+        [HttpPost]
+        public async Task<IActionResult> ConfirmOtpAction(string msisdn, string transactionId, string otp, string? returnUrl = null)
+        {
+            var result = await ConfirmOtpCore(msisdn, transactionId, otp);
+            return Json(new
+            {
+                errorCode = result.ErrorCode,
+                message = result.Message,
+                redirectUrl = result.ErrorCode == CommonErrorCode.SUCCESS ? Url.Action("Index", "Home") ?? "/" : null
+            });
+        }
+
+        private async Task<OtpResult> SendOtpCore(string msisdn)
         {
             if (string.IsNullOrWhiteSpace(msisdn))
             {
-                return Json(new { errorCode = -1, message = "Please enter MSISDN." });
+                return OtpResult.Fail("Please enter MSISDN.");
+            }
+
+            var requestMsisdn = msisdn.Trim();
+            var pendingMsisdn = HttpContext.Session.GetString(PendingOtpMsisdnKey);
+            var pendingTransactionId = HttpContext.Session.GetString(PendingOtpTransactionKey);
+            if (string.Equals(pendingMsisdn, requestMsisdn, StringComparison.Ordinal)
+                && !string.IsNullOrWhiteSpace(pendingTransactionId))
+            {
+                return OtpResult.Success("Enter OTP from message.", pendingMsisdn ?? requestMsisdn, pendingTransactionId);
             }
 
             var baseUrl = GetParameter("url");
             if (string.IsNullOrWhiteSpace(baseUrl))
             {
-                return Json(new { errorCode = -1, message = "Missing API base URL." });
+                return OtpResult.Fail("Missing API base URL.");
             }
 
             try
@@ -58,7 +124,7 @@ namespace Kitty_Fall.Website.Controllers
                     baseUrl + UrlConstant.UserSendOtp,
                     new UserSendOtpReq
                     {
-                        msisdn = msisdn.Trim(),
+                        msisdn = requestMsisdn,
                         serviceName = TestServiceName,
                         language = language
                     },
@@ -67,7 +133,7 @@ namespace Kitty_Fall.Website.Controllers
 
                 if (res?.data == null)
                 {
-                    return Json(new { errorCode = -1, message = "Send OTP API returned empty response." });
+                    return OtpResult.Fail("Send OTP API returned empty response.");
                 }
 
                 var jObject = JObject.Parse(res.data);
@@ -75,49 +141,43 @@ namespace Kitty_Fall.Website.Controllers
                 var message = jObject["message"]?.ToString() ?? jObject["msg"]?.ToString() ?? "";
                 if (errorCode != CommonErrorCode.SUCCESS)
                 {
-                    return Json(new { errorCode, message = string.IsNullOrWhiteSpace(message) ? "Send OTP failed." : message });
+                    return OtpResult.From(errorCode, string.IsNullOrWhiteSpace(message) ? "Send OTP failed." : message);
                 }
 
                 var data = jObject["data"];
-                var normalizedMsisdn = data?["msisdn"]?.ToString() ?? msisdn.Trim();
+                var normalizedMsisdn = data?["msisdn"]?.ToString() ?? requestMsisdn;
                 var transactionId = data?["transactionId"]?.ToString();
                 if (string.IsNullOrWhiteSpace(transactionId))
                 {
-                    return Json(new { errorCode = -1, message = "Send OTP succeeded but transactionId is empty." });
+                    return OtpResult.Fail("Send OTP succeeded but transactionId is empty.");
                 }
 
-                SessionExtension.SetComplexData(HttpContext.Session, PendingOtpMsisdnKey, normalizedMsisdn.ToString());
-                SessionExtension.SetComplexData(HttpContext.Session, PendingOtpTransactionKey, transactionId);
+                Microsoft.AspNetCore.Http.SessionExtensions.SetString(HttpContext.Session, PendingOtpMsisdnKey, normalizedMsisdn.ToString());
+                Microsoft.AspNetCore.Http.SessionExtensions.SetString(HttpContext.Session, PendingOtpTransactionKey, transactionId);
+                await HttpContext.Session.CommitAsync();
 
-                return Json(new
-                {
-                    errorCode,
-                    message = string.IsNullOrWhiteSpace(message) ? "Enter OTP from message." : message,
-                    msisdn = normalizedMsisdn,
-                    transactionId
-                });
+                return OtpResult.Success(string.IsNullOrWhiteSpace(message) ? "Enter OTP from message." : message, normalizedMsisdn, transactionId);
             }
             catch (Exception ex)
             {
                 log.Error("Test send OTP error: ", ex);
-                return Json(new { errorCode = -1, message = "Send OTP failed. Please check API service." });
+                return OtpResult.Fail("Send OTP failed. Please check API service.");
             }
         }
 
-        [HttpPost]
-        public async Task<IActionResult> ConfirmOtpAction(string msisdn, string transactionId, string otp, string? returnUrl = null)
+        private async Task<OtpResult> ConfirmOtpCore(string msisdn, string transactionId, string otp)
         {
             if (string.IsNullOrWhiteSpace(msisdn)
                 || string.IsNullOrWhiteSpace(transactionId)
                 || string.IsNullOrWhiteSpace(otp))
             {
-                return Json(new { errorCode = -1, message = "Please enter OTP." });
+                return OtpResult.Fail("Please enter OTP.");
             }
 
             var baseUrl = GetParameter("url");
             if (string.IsNullOrWhiteSpace(baseUrl))
             {
-                return Json(new { errorCode = -1, message = "Missing API base URL." });
+                return OtpResult.Fail("Missing API base URL.");
             }
 
             try
@@ -139,7 +199,7 @@ namespace Kitty_Fall.Website.Controllers
 
                 if (res?.data == null)
                 {
-                    return Json(new { errorCode = -1, message = "Confirm OTP API returned empty response." });
+                    return OtpResult.Fail("Confirm OTP API returned empty response.");
                 }
 
                 var jObject = JObject.Parse(res.data);
@@ -147,7 +207,7 @@ namespace Kitty_Fall.Website.Controllers
                 var message = jObject["message"]?.ToString() ?? jObject["msg"]?.ToString() ?? "";
                 if (errorCode != CommonErrorCode.SUCCESS)
                 {
-                    return Json(new { errorCode, message = string.IsNullOrWhiteSpace(message) ? "Confirm OTP failed." : message });
+                    return OtpResult.From(errorCode, string.IsNullOrWhiteSpace(message) ? "Confirm OTP failed." : message);
                 }
 
                 var loggedMsisdn = jObject["data"]?["accountUser"]?["msisdn"]?.ToString()
@@ -157,18 +217,14 @@ namespace Kitty_Fall.Website.Controllers
                 LoginSessionHelper.SaveLoginSession(HttpContext, loggedMsisdn, jObject);
                 ClearPendingOtp();
                 await PackagesCacheHelper.LoadAndCacheAsync(log, HttpContext.Session, baseUrl, language);
+                await HttpContext.Session.CommitAsync();
 
-                return Json(new
-                {
-                    errorCode,
-                    message = string.IsNullOrWhiteSpace(message) ? "Success" : message,
-                    redirectUrl = Url.Action("Index", "Home") ?? "/"
-                });
+                return OtpResult.Success(string.IsNullOrWhiteSpace(message) ? "Success" : message, loggedMsisdn, "");
             }
             catch (Exception ex)
             {
                 log.Error("Test confirm OTP error: ", ex);
-                return Json(new { errorCode = -1, message = "Confirm OTP failed. Please check API service and OTP." });
+                return OtpResult.Fail("Confirm OTP failed. Please check API service and OTP.");
             }
         }
 
@@ -195,6 +251,21 @@ namespace Kitty_Fall.Website.Controllers
             HttpContext.Session.Remove(PendingOtpTransactionKey);
         }
 
+        private void PrepareLoginView(
+            string? returnUrl,
+            string? pendingMsisdn,
+            string? pendingTransactionId,
+            string? error,
+            string? info)
+        {
+            ViewBag.ReturnUrl = NormalizeReturnUrl(returnUrl);
+            ViewBag.CurrentMsisdn = HttpContext.Session.GetComplexData<string>("msisdn");
+            ViewBag.PendingOtpMsisdn = pendingMsisdn;
+            ViewBag.PendingOtpTransactionId = pendingTransactionId;
+            ViewBag.LoginError = error;
+            ViewBag.LoginInfo = info;
+        }
+
         private static int ReadErrorCode(JObject jObject)
         {
             var value = jObject["errorCode"]?.ToString();
@@ -224,5 +295,38 @@ namespace Kitty_Fall.Website.Controllers
 
             return Url.Action("Index", "Home") ?? "/";
         }
+
+        private sealed class OtpResult
+        {
+            public int ErrorCode { get; init; }
+            public string Message { get; init; } = "";
+            public string Msisdn { get; init; } = "";
+            public string TransactionId { get; init; } = "";
+
+            public static OtpResult Success(string message, string msisdn, string transactionId)
+            {
+                return new OtpResult
+                {
+                    ErrorCode = CommonErrorCode.SUCCESS,
+                    Message = message,
+                    Msisdn = msisdn,
+                    TransactionId = transactionId
+                };
+            }
+
+            public static OtpResult Fail(string message)
+            {
+                return From(-1, message);
+            }
+
+            public static OtpResult From(int errorCode, string message)
+            {
+                return new OtpResult
+                {
+                    ErrorCode = errorCode,
+                    Message = message
+                };
+            }
+        }
     }
 }

+ 6 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Kitty_Fall.Website.csproj

@@ -10,6 +10,12 @@
 		<ProjectReference Include="..\Common\Common.csproj" />
 	</ItemGroup>
 
+	<ItemGroup>
+		<!-- Gợi ý dotnet watch bỏ qua toàn bộ file log nằm trong bin hoặc build -->
+		<Watch Remove="bin\**\*.log" />
+		<Watch Remove="logs\**\*" />
+	</ItemGroup>
+
 	<ItemGroup>
 		<PackageReference Include="log4net" Version="2.0.15" />
 	</ItemGroup>

+ 126 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Language/Lang.Designer.cs

@@ -60,6 +60,15 @@ namespace Kitty_Fall.Website.Language {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to auto-renewed daily..
+        /// </summary>
+        public static string AutoRenewedDaily {
+            get {
+                return ResourceManager.GetString("AutoRenewedDaily", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Back.
         /// </summary>
@@ -87,6 +96,24 @@ namespace Kitty_Fall.Website.Language {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Confirm.
+        /// </summary>
+        public static string Confirm {
+            get {
+                return ResourceManager.GetString("Confirm", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Confirm Subscription.
+        /// </summary>
+        public static string ConfirmSubscription {
+            get {
+                return ResourceManager.GetString("ConfirmSubscription", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Daily Package.
         /// </summary>
@@ -114,6 +141,15 @@ namespace Kitty_Fall.Website.Language {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Enjoy more rewards and exciting gameplay!.
+        /// </summary>
+        public static string EnjoyMoreRewards {
+            get {
+                return ResourceManager.GetString("EnjoyMoreRewards", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Fewer obstacles.
         /// </summary>
@@ -240,6 +276,15 @@ namespace Kitty_Fall.Website.Language {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to More turns and more fun!.
+        /// </summary>
+        public static string MoreTurnsAndMoreFun {
+            get {
+                return ResourceManager.GetString("MoreTurnsAndMoreFun", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Most challenging levels.
         /// </summary>
@@ -276,6 +321,15 @@ namespace Kitty_Fall.Website.Language {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Please try again..
+        /// </summary>
+        public static string PleaseTryAgain {
+            get {
+                return ResourceManager.GetString("PleaseTryAgain", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Points.
         /// </summary>
@@ -339,6 +393,60 @@ namespace Kitty_Fall.Website.Language {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Subscription Failed.
+        /// </summary>
+        public static string SubscriptionFailed {
+            get {
+                return ResourceManager.GetString("SubscriptionFailed", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to We couldn&apos;t complete your subscription..
+        /// </summary>
+        public static string SubscriptionFailedCopy {
+            get {
+                return ResourceManager.GetString("SubscriptionFailedCopy", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Opps! Something went wrong.
+        /// </summary>
+        public static string SubscriptionFailedTitle {
+            get {
+                return ResourceManager.GetString("SubscriptionFailedTitle", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Subscribe to the package below and enjoy more turns and rewards!.
+        /// </summary>
+        public static string SubscriptionIntro {
+            get {
+                return ResourceManager.GetString("SubscriptionIntro", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to You have successfully subscribed to the package!.
+        /// </summary>
+        public static string SubscriptionSuccessMessage {
+            get {
+                return ResourceManager.GetString("SubscriptionSuccessMessage", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Successful!.
+        /// </summary>
+        public static string Successful {
+            get {
+                return ResourceManager.GetString("Successful", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Suitable for beginners.
         /// </summary>
@@ -348,6 +456,24 @@ namespace Kitty_Fall.Website.Language {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to This package will be.
+        /// </summary>
+        public static string ThisPackageWillBe {
+            get {
+                return ResourceManager.GetString("ThisPackageWillBe", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to turns have been added!.
+        /// </summary>
+        public static string TurnsHaveBeenAdded {
+            get {
+                return ResourceManager.GetString("TurnsHaveBeenAdded", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Winner List.
         /// </summary>

+ 42 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Language/Lang.resx

@@ -216,4 +216,46 @@
   <data name="Mode" xml:space="preserve">
     <value>mode</value>
   </data>
+  <data name="ConfirmSubscription" xml:space="preserve">
+    <value>Confirm Subscription</value>
+  </data>
+  <data name="SubscriptionIntro" xml:space="preserve">
+    <value>Subscribe to the package below and enjoy more turns and rewards!</value>
+  </data>
+  <data name="MoreTurnsAndMoreFun" xml:space="preserve">
+    <value>More turns and more fun!</value>
+  </data>
+  <data name="ThisPackageWillBe" xml:space="preserve">
+    <value>This package will be</value>
+  </data>
+  <data name="AutoRenewedDaily" xml:space="preserve">
+    <value>auto-renewed daily.</value>
+  </data>
+  <data name="Confirm" xml:space="preserve">
+    <value>Confirm</value>
+  </data>
+  <data name="Successful" xml:space="preserve">
+    <value>Successful!</value>
+  </data>
+  <data name="SubscriptionSuccessMessage" xml:space="preserve">
+    <value>You have successfully subscribed to the package!</value>
+  </data>
+  <data name="TurnsHaveBeenAdded" xml:space="preserve">
+    <value>turns have been added!</value>
+  </data>
+  <data name="EnjoyMoreRewards" xml:space="preserve">
+    <value>Enjoy more rewards and exciting gameplay!</value>
+  </data>
+  <data name="SubscriptionFailed" xml:space="preserve">
+    <value>Subscription Failed</value>
+  </data>
+  <data name="SubscriptionFailedTitle" xml:space="preserve">
+    <value>Opps! Something went wrong</value>
+  </data>
+  <data name="SubscriptionFailedCopy" xml:space="preserve">
+    <value>We couldn't complete your subscription.</value>
+  </data>
+  <data name="PleaseTryAgain" xml:space="preserve">
+    <value>Please try again.</value>
+  </data>
 </root>

+ 42 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Language/Lang.vi.resx

@@ -216,4 +216,46 @@
   <data name="Mode" xml:space="preserve">
     <value>mode</value>
   </data>
+  <data name="ConfirmSubscription" xml:space="preserve">
+    <value>Confirm Subscription</value>
+  </data>
+  <data name="SubscriptionIntro" xml:space="preserve">
+    <value>Subscribe to the package below and enjoy more turns and rewards!</value>
+  </data>
+  <data name="MoreTurnsAndMoreFun" xml:space="preserve">
+    <value>More turns and more fun!</value>
+  </data>
+  <data name="ThisPackageWillBe" xml:space="preserve">
+    <value>This package will be</value>
+  </data>
+  <data name="AutoRenewedDaily" xml:space="preserve">
+    <value>auto-renewed daily.</value>
+  </data>
+  <data name="Confirm" xml:space="preserve">
+    <value>Confirm</value>
+  </data>
+  <data name="Successful" xml:space="preserve">
+    <value>Successful!</value>
+  </data>
+  <data name="SubscriptionSuccessMessage" xml:space="preserve">
+    <value>You have successfully subscribed to the package!</value>
+  </data>
+  <data name="TurnsHaveBeenAdded" xml:space="preserve">
+    <value>turns have been added!</value>
+  </data>
+  <data name="EnjoyMoreRewards" xml:space="preserve">
+    <value>Enjoy more rewards and exciting gameplay!</value>
+  </data>
+  <data name="SubscriptionFailed" xml:space="preserve">
+    <value>Subscription Failed</value>
+  </data>
+  <data name="SubscriptionFailedTitle" xml:space="preserve">
+    <value>Opps! Something went wrong</value>
+  </data>
+  <data name="SubscriptionFailedCopy" xml:space="preserve">
+    <value>We couldn't complete your subscription.</value>
+  </data>
+  <data name="PleaseTryAgain" xml:space="preserve">
+    <value>Please try again.</value>
+  </data>
 </root>

+ 10 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Program.cs

@@ -16,12 +16,21 @@ var builder = WebApplication.CreateBuilder(args);
 builder.Services.AddControllersWithViews();
 builder.Services.AddMemoryCache();
 
+builder.Services.Configure<Microsoft.AspNetCore.Builder.ForwardedHeadersOptions>(options =>
+{
+    options.ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto;
+    options.KnownNetworks.Clear();
+    options.KnownProxies.Clear();
+});
+
 builder.Services.AddSession(options =>
 {
     options.IdleTimeout = TimeSpan.FromMinutes(15);
     options.Cookie.HttpOnly = true;
     options.Cookie.IsEssential = true;
     options.Cookie.Name = "Kitty_Fall.Website.Session";
+    options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
+    options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax;
 });
 
 builder.Services.Configure<RequestLocalizationOptions>(options =>
@@ -42,6 +51,7 @@ if (!app.Environment.IsDevelopment())
     app.UseHsts();
 }
 
+app.UseForwardedHeaders();
 app.UseHttpsRedirection();
 app.UseStaticFiles();
 

+ 46 - 36
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Views/Home/Index.cshtml

@@ -1,5 +1,16 @@
 @{
     ViewData["Title"] = "Kitty Fall";
+    var msisdn = ViewBag.Msisdn as string;
+    var profilePicture = ViewBag.ProfilePicture as string;
+    var avatarUrl = string.IsNullOrWhiteSpace(profilePicture) ? Url.Content("~/kitty/assets/kitty-fall/avatar.png") : profilePicture;
+    var totalPoint = ViewBag.TotalPoint is null ? 0L : Convert.ToInt64(ViewBag.TotalPoint);
+    var totalTurn = ViewBag.TotalTurn is null ? 0L : Convert.ToInt64(ViewBag.TotalTurn);
+    var packageFee = ViewBag.PackageFee is null ? 179M : Convert.ToDecimal(ViewBag.PackageFee);
+    var packageTurns = ViewBag.PackageTurns is null ? 5 : Convert.ToInt32(ViewBag.PackageTurns);
+    var packageExpireDays = ViewBag.PackageExpireDays is null ? 1 : Convert.ToInt32(ViewBag.PackageExpireDays);
+    var packageProductName = ViewBag.PackageProductName as string ?? "";
+    var packagePeriodText = packageExpireDays <= 1 ? "Day" : $"{packageExpireDays} Days";
+    var packageTurnText = packageTurns <= 1 ? "Turn" : "Turns";
 }
 
 <main class="kitty-app" aria-label="Kitty Fall home screen">
@@ -8,10 +19,10 @@
 
     <header class="topbar">
         <div class="profile">
-            <img class="profile-avatar" src="~/kitty/assets/kitty-fall/avatar.png" alt="Player avatar">
+            <img class="profile-avatar" src="@avatarUrl" alt="Player avatar">
             <div class="profile-copy">
                 <strong>Hello!</strong>
-                <span>0912 345 678</span>
+                <span>@(string.IsNullOrWhiteSpace(msisdn) ? "-" : msisdn)</span>
             </div>
         </div>
         <div class="topbar-actions">
@@ -32,7 +43,7 @@
                 <img class="stat-icon coin" src="~/kitty/assets/kitty-fall/coin.png" alt="">
                 <div>
                     <p>@Lang.Points</p>
-                    <strong class="points">2,450</strong>
+                    <strong class="points">@totalPoint.ToString("N0")</strong>
                 </div>
             </div>
             <span class="stat-divider"></span>
@@ -40,7 +51,7 @@
                 <img class="stat-icon ticket" src="~/kitty/assets/kitty-fall/ticket.png" alt="">
                 <div>
                     <p>@Lang.PlayTurns</p>
-                    <strong class="turns">24</strong>
+                    <strong class="turns">@totalTurn.ToString("N0")</strong>
                 </div>
             </div>
         </div>
@@ -69,15 +80,15 @@
                 <p>@Lang.SubscriptionAutoRenewDaily</p>
             </div>
             <div class="package-action">
-                <p><strong>179</strong> MMK/Day</p>
-                <button class="subscribe-trigger" type="button" data-open-subscription>@Lang.SubscribeNow</button>
+                <p><strong>@packageFee.ToString("N0")</strong> MMK/@packagePeriodText</p>
+                <button class="subscribe-trigger" type="button" data-open-subscription data-product-name="@packageProductName">@Lang.SubscribeNow</button>
             </div>
         </div>
     </section>
 
     <div class="subscription-modal" data-subscription-modal aria-hidden="true">
-        <button class="subscription-backdrop" type="button" data-close-subscription aria-label="Dismiss subscription confirmation"></button>
-        <section class="confirm-dialog-shell scale-floor modal-panel is-active" data-modal-panel="confirm" data-scale-floor="361" data-scale-height="540" role="dialog" aria-modal="true" aria-label="Confirm subscription">
+        <button class="subscription-backdrop" type="button" data-close-subscription aria-label="@Lang.ConfirmSubscription"></button>
+        <section class="confirm-dialog-shell scale-floor modal-panel is-active" data-modal-panel="confirm" data-scale-floor="361" data-scale-height="540" role="dialog" aria-modal="true" aria-label="@Lang.ConfirmSubscription">
             <div class="confirm-dialog scale-floor-inner">
                 <img class="confirm-cat" src="~/kitty/assets/kitty-fall/confirm-cat.png" alt="">
                 <div class="confirm-card">
@@ -88,40 +99,40 @@
                                 <path id="confirm-ribbon-title-path" d="M 20 55 Q 160 18 300 55" />
                             </defs>
                             <text font-size="30" font-weight="900" fill="#fff" text-anchor="middle" textLength="258" lengthAdjust="spacingAndGlyphs">
-                                <textPath href="#confirm-ribbon-title-path" startOffset="50%">Confirm Subscription</textPath>
+                                <textPath href="#confirm-ribbon-title-path" startOffset="50%">@Lang.ConfirmSubscription</textPath>
                             </text>
                         </svg>
-                        <span id="subscription-title" class="sr-only">Confirm Subscription</span>
+                        <span id="subscription-title" class="sr-only">@Lang.ConfirmSubscription</span>
                     </div>
-                    <button class="confirm-close" type="button" data-close-subscription aria-label="Close subscription confirmation">
+                    <button class="confirm-close" type="button" data-close-subscription aria-label="@Lang.ConfirmSubscription">
                         <img src="~/kitty/assets/kitty-fall/confirm-close.png" alt="">
                     </button>
 
-                    <p class="confirm-intro">Subscribe to the package below and enjoy more turns and rewards!</p>
+                    <p class="confirm-intro">@Lang.SubscriptionIntro</p>
 
                     <div class="confirm-price">
-                        <strong>179</strong>
-                        <span>MMK/5 Turns</span>
+                        <strong>@packageFee.ToString("N0")</strong>
+                        <span>MMK/@packageTurns @packageTurnText</span>
                     </div>
 
                     <img class="confirm-tickets" src="~/kitty/assets/kitty-fall/confirm-tickets.png" alt="">
 
-                    <p class="confirm-note">More turns and more fun!</p>
+                    <p class="confirm-note">@Lang.MoreTurnsAndMoreFun</p>
 
                     <div class="renewal-note">
                         <span class="renewal-icon" aria-hidden="true"></span>
-                        <p>This package will be <strong>auto-renewed daily.</strong></p>
+                        <p>@Lang.ThisPackageWillBe <strong>@Lang.AutoRenewedDaily</strong></p>
                     </div>
 
-                    <button class="confirm-button" type="button" data-confirm-subscription>
+                    <button class="confirm-button" type="button" data-confirm-subscription data-product-name="@packageProductName">
                         <span class="confirm-button-icon" aria-hidden="true"></span>
-                        <span>Confirm</span>
+                        <span>@Lang.Confirm</span>
                     </button>
                 </div>
             </div>
         </section>
 
-        <section class="result-dialog-shell result-dialog-shell-success scale-floor modal-panel" data-modal-panel="success" data-scale-floor="361" data-scale-height="608" role="dialog" aria-modal="true" aria-label="Subscription successful">
+        <section class="result-dialog-shell result-dialog-shell-success scale-floor modal-panel" data-modal-panel="success" data-scale-floor="361" data-scale-height="608" role="dialog" aria-modal="true" aria-label="@Lang.Successful">
             <div class="result-dialog scale-floor-inner">
                 <img class="result-cat result-cat-success" src="~/kitty/assets/kitty-fall/result-success-cat.png" alt="">
                 <div class="result-card result-card-success">
@@ -132,45 +143,45 @@
                                 <path id="result-success-title-path" d="M 20 55 Q 160 18 300 55" />
                             </defs>
                             <text font-size="34" font-weight="900" fill="#fff" text-anchor="middle" textLength="210" lengthAdjust="spacingAndGlyphs">
-                                <textPath href="#result-success-title-path" startOffset="50%">Successful!</textPath>
+                                <textPath href="#result-success-title-path" startOffset="50%">@Lang.Successful</textPath>
                             </text>
                         </svg>
-                        <span class="sr-only">Successful!</span>
+                        <span class="sr-only">@Lang.Successful</span>
                     </div>
-                    <button class="confirm-close" type="button" data-close-subscription aria-label="Close subscription result">
+                    <button class="confirm-close" type="button" data-close-subscription aria-label="@Lang.Successful">
                         <img src="~/kitty/assets/kitty-fall/confirm-close.png" alt="">
                     </button>
 
                     <img class="result-status-icon result-status-success" src="~/kitty/assets/kitty-fall/result-success-icon.png" alt="">
-                    <p class="result-title result-title-success">You have successfully subscribed to the package!</p>
+                    <p class="result-title result-title-success">@Lang.SubscriptionSuccessMessage</p>
 
                     <div class="result-benefits">
                         <div class="result-benefit">
                             <span class="result-ticket-icon">
                                 <img src="~/kitty/assets/kitty-fall/ticket.png" alt="">
-                                <strong>x5</strong>
+                                <strong>x@packageTurns</strong>
                             </span>
-                            <p><strong>5</strong> turns have been added!</p>
+                            <p><strong>@packageTurns</strong> @Lang.TurnsHaveBeenAdded</p>
                         </div>
                         <div class="result-benefit">
                             <span class="renewal-icon" aria-hidden="true"></span>
-                            <p>Enjoy more rewards and exciting gameplay!</p>
+                            <p>@Lang.EnjoyMoreRewards</p>
                         </div>
                         <div class="result-benefit">
                             <span class="renewal-icon" aria-hidden="true"></span>
-                            <p>This package will be <strong>auto-renewed daily.</strong></p>
+                            <p>@Lang.ThisPackageWillBe <strong>@Lang.AutoRenewedDaily</strong></p>
                         </div>
                     </div>
 
                     <button class="result-button confirm-button" type="button" data-close-subscription>
                         <span class="confirm-button-icon" aria-hidden="true"></span>
-                        <span>Confirm</span>
+                        <span>@Lang.Confirm</span>
                     </button>
                 </div>
             </div>
         </section>
 
-        <section class="result-dialog-shell result-dialog-shell-fail scale-floor modal-panel" data-modal-panel="fail" data-scale-floor="361" data-scale-height="452" role="dialog" aria-modal="true" aria-label="Subscription failed">
+        <section class="result-dialog-shell result-dialog-shell-fail scale-floor modal-panel" data-modal-panel="fail" data-scale-floor="361" data-scale-height="452" role="dialog" aria-modal="true" aria-label="@Lang.SubscriptionFailed">
             <div class="result-dialog scale-floor-inner">
                 <img class="result-cat result-cat-fail" src="~/kitty/assets/kitty-fall/result-fail-cat.png" alt="">
                 <div class="result-card result-card-fail">
@@ -181,22 +192,22 @@
                                 <path id="result-fail-title-path" d="M 20 55 Q 160 18 300 55" />
                             </defs>
                             <text font-size="30" font-weight="900" fill="#fff" text-anchor="middle" textLength="258" lengthAdjust="spacingAndGlyphs">
-                                <textPath href="#result-fail-title-path" startOffset="50%">Subscription Failed</textPath>
+                                <textPath href="#result-fail-title-path" startOffset="50%">@Lang.SubscriptionFailed</textPath>
                             </text>
                         </svg>
-                        <span class="sr-only">Subscription Failed</span>
+                        <span class="sr-only">@Lang.SubscriptionFailed</span>
                     </div>
-                    <button class="confirm-close" type="button" data-close-subscription aria-label="Close subscription result">
+                    <button class="confirm-close" type="button" data-close-subscription aria-label="@Lang.SubscriptionFailed">
                         <img src="~/kitty/assets/kitty-fall/confirm-close.png" alt="">
                     </button>
 
                     <img class="result-status-icon result-status-fail" src="~/kitty/assets/kitty-fall/result-fail-icon.png" alt="">
-                    <p class="result-title result-title-fail">Opps! Something went wrong</p>
-                    <p class="result-copy-fail">We coundn’t complete your subscription.<br>Please try again.</p>
+                    <p class="result-title result-title-fail">@Lang.SubscriptionFailedTitle</p>
+                    <p class="result-copy-fail">@Lang.SubscriptionFailedCopy<br>@Lang.PleaseTryAgain</p>
 
                     <button class="result-button result-button-fail confirm-button" type="button" data-close-subscription>
                         <span class="confirm-button-icon" aria-hidden="true"></span>
-                        <span>Confirm</span>
+                        <span>@Lang.Confirm</span>
                     </button>
                 </div>
             </div>
@@ -217,4 +228,3 @@
         }
     </script>
 }
-

+ 181 - 105
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Views/Login/Index.cshtml

@@ -1,13 +1,9 @@
 @{
-    ViewData["Title"] = "Test Login";
-    var returnUrl = ViewBag.ReturnUrl as string ?? "/";
+    ViewData["Title"] = "Login";
     var currentMsisdn = ViewBag.CurrentMsisdn as string;
-    var pendingOtpMsisdn = ViewBag.PendingOtpMsisdn as string;
-    var pendingOtpTransactionId = ViewBag.PendingOtpTransactionId as string;
-    var isOtpPending = !string.IsNullOrWhiteSpace(pendingOtpTransactionId);
 }
 
-<section class="kitty-app login-screen" aria-label="Test login">
+<section class="kitty-app login-screen" aria-label="login">
     <img class="kitty-bg" src="~/kitty/assets/kitty-fall/bg.png" alt="">
     <div class="kitty-bg-fade"></div>
 
@@ -15,7 +11,7 @@
         <img class="login-logo" src="~/kitty/assets/kitty-fall/logo.png" alt="Kitty Fall">
         <img class="login-cat" src="~/kitty/assets/kitty-fall/cat.png" alt="">
 
-        <div class="login-card" data-login-card>
+        <div class="login-card" data-login-card data-login-step="msisdn">
             <div class="login-card-head">
                 <strong>Login</strong>
             </div>
@@ -32,150 +28,230 @@
                 </div>
             }
 
-            <div class="login-alert login-alert-info @(isOtpPending ? "" : "login-hidden")" data-login-message>
-                @(isOtpPending ? "Enter OTP from message." : "")
-            </div>
+            <div id="loginMessage" class="login-alert login-hidden" data-login-message></div>
 
-            <div class="login-form">
-                <input type="hidden" data-return-url value="@returnUrl">
-                <input type="hidden" data-transaction-id value="@pendingOtpTransactionId">
+            <div class="login-form" data-login-form>
+                <div id="loginSendPanel" class="login-step-panel login-send-step">
+                    <label class="login-field">
+                        <span>MSISDN</span>
+                        <input id="phonenumber" type="text" autocomplete="tel" placeholder="959xxxxxxxx">
+                    </label>
+                </div>
 
-                <label class="login-field">
-                    <span>MSISDN</span>
-                    <input data-msisdn type="text" autocomplete="tel" placeholder="959xxxxxxxx" value="@pendingOtpMsisdn" readonly="@isOtpPending">
-                </label>
+                <div id="loginOtpPanel" class="login-step-panel login-otp-step" style="display: none;">
+                    <label class="login-field">
+                        <span>MSISDN</span>
+                        <input id="otp_phonenumber" type="text" autocomplete="tel" placeholder="959xxxxxxxx" readonly>
+                    </label>
 
-                <label class="login-field @(isOtpPending ? "" : "login-hidden")" data-otp-field>
-                    <span>OTP</span>
-                    <input data-otp type="text" inputmode="numeric" autocomplete="one-time-code" placeholder="Enter OTP">
-                </label>
+                    <label class="login-field">
+                        <span>OTP</span>
+                        <input id="otp" type="text" inputmode="numeric" autocomplete="one-time-code" placeholder="Enter OTP">
+                    </label>
+                </div>
 
-                <button class="login-submit @(isOtpPending ? "login-hidden" : "")" type="button" data-send-otp onclick="sendOtp()">
+                <button type="button" id="btnLoginConfirm" class="login-submit">
                     <img src="~/kitty/assets/kitty-fall/play-icon.svg" alt="">
                     <span>Send OTP</span>
                 </button>
 
-                <button class="login-submit @(isOtpPending ? "" : "login-hidden")" type="button" data-confirm-otp onclick="confirmOtp()">
-                    <img src="~/kitty/assets/kitty-fall/play-icon.svg" alt="">
-                    <span>Confirm OTP</span>
-                </button>
+                <div id="changePhoneWrapper" class="change-phone-wrapper" style="display: none; text-align: center; margin-top: 15px;">
+                    <button type="button" id="btnChangePhone" style="background: none; border: none; color: #5721ea; text-decoration: underline; cursor: pointer; font-size: 14px; font-weight: 800;">
+                        Change phone number
+                    </button>
+                </div>
+            </div>
+
+            <div class="login-loading login-hidden" data-login-loading>
+                <span class="login-spinner" aria-hidden="true"></span>
             </div>
         </div>
     </div>
 </section>
 
-@section Scripts {
-    <script>
-        function getLoginElements() {
-            const card = document.querySelector("[data-login-card]");
-            return {
-                card: card,
-                message: card?.querySelector("[data-login-message]"),
-                msisdnInput: card?.querySelector("[data-msisdn]"),
-                otpInput: card?.querySelector("[data-otp]"),
-                otpField: card?.querySelector("[data-otp-field]"),
-                transactionInput: card?.querySelector("[data-transaction-id]"),
-                returnUrlInput: card?.querySelector("[data-return-url]"),
-                sendButton: card?.querySelector("[data-send-otp]"),
-                confirmButton: card?.querySelector("[data-confirm-otp]")
-            };
-        }
+<script>
+    (function() {
+        var currentStep = "msisdn";
+        var transactionId = "";
+        var loginBusy = false;
 
-        function showLoginMessage(text, isInfo) {
-            const el = getLoginElements();
-            if (!el.message) return;
-            el.message.textContent = text || "";
-            el.message.classList.toggle("login-hidden", !text);
-            el.message.classList.toggle("login-alert-info", !!isInfo);
+        var btnConfirm = document.getElementById("btnLoginConfirm");
+        var btnChangePhone = document.getElementById("btnChangePhone");
+        var changePhoneWrapper = document.getElementById("changePhoneWrapper");
+        var inputPhone = document.getElementById("phonenumber");
+        var inputOtpPhone = document.getElementById("otp_phonenumber");
+        var inputOtp = document.getElementById("otp");
+        var panelSend = document.getElementById("loginSendPanel");
+        var panelOtp = document.getElementById("loginOtpPanel");
+        var alertMessage = document.getElementById("loginMessage");
+        var loadingSpinner = document.querySelector("[data-login-loading]");
+        var card = document.querySelector("[data-login-card]");
+
+        function showMessage(text, isInfo) {
+            if (!alertMessage) return;
+            if (text) {
+                alertMessage.textContent = text;
+                alertMessage.className = "login-alert" + (isInfo ? " login-alert-info" : "");
+                alertMessage.style.display = "block";
+            } else {
+                alertMessage.style.display = "none";
+            }
         }
 
-        function setLoginLoading(button, isLoading, text) {
-            button.disabled = isLoading;
-            button.querySelector("span").textContent = text;
+        function setLoading(isLoading, text) {
+            loginBusy = isLoading;
+            if (btnConfirm) {
+                btnConfirm.disabled = isLoading;
+                var span = btnConfirm.querySelector("span");
+                if (span && text) span.textContent = text;
+            }
+            if (loadingSpinner) {
+                loadingSpinner.classList.toggle("login-hidden", !isLoading);
+            }
         }
 
-        function showOtpStep(res) {
-            const el = getLoginElements();
-            el.transactionInput.value = res.transactionId || "";
-            el.msisdnInput.value = res.msisdn || el.msisdnInput.value.trim();
-            el.msisdnInput.readOnly = true;
-            el.otpField.classList.remove("login-hidden");
-            el.sendButton.classList.add("login-hidden");
-            el.confirmButton.classList.remove("login-hidden");
-            showLoginMessage(res.message || "Enter OTP from message.", true);
-            el.otpInput.focus();
+        function setStep(step) {
+            currentStep = step;
+            if (card) {
+                card.setAttribute("data-login-step", step);
+            }
+            if (panelSend) {
+                panelSend.style.display = step === "otp" ? "none" : "grid";
+            }
+            if (panelOtp) {
+                panelOtp.style.display = step === "otp" ? "grid" : "none";
+            }
+            if (changePhoneWrapper) {
+                changePhoneWrapper.style.display = step === "otp" ? "block" : "none";
+            }
+            if (btnConfirm) {
+                var span = btnConfirm.querySelector("span");
+                if (span) {
+                    span.textContent = step === "otp" ? "Confirm OTP" : "Send OTP";
+                }
+            }
+            if (step === "otp" && inputOtp) {
+                inputOtp.value = "";
+                setTimeout(function() { inputOtp.focus(); }, 50);
+            }
         }
 
-        function sendOtp() {
-            const el = getLoginElements();
-            if (!el.card || !window.jQuery) return;
+        function handleSendOtp() {
+            var msisdn = (inputPhone ? inputPhone.value : "").trim();
+            if (!msisdn) {
+                showMessage("Please enter MSISDN.", false);
+                if (inputPhone) inputPhone.focus();
+                return;
+            }
 
-            const msisdn = el.msisdnInput.value.trim();
-            showLoginMessage("", false);
-            setLoginLoading(el.sendButton, true, "Sending...");
+            showMessage("Sending OTP...", true);
+            setLoading(true, "Sending...");
 
             $.ajax({
                 url: "/Login/SendOtpAction",
                 type: "POST",
                 data: { msisdn: msisdn },
-                success: function (res) {
-                    console.log("GetOtp Response:", res);
-                    setLoginLoading(el.sendButton, false, "Send OTP");
-
-                    if (res && (res.errorCode === 0 || res.errorCode === 200)) {
-                        showOtpStep(res);
-                        return;
+                success: function(res) {
+                    setLoading(false);
+                    if (typeof res === "string") {
+                        try { res = JSON.parse(res); } catch(e) {}
+                    }
+                    
+                    var errorCode = res && (res.errorCode ?? res.ErrorCode);
+                    if (errorCode === 0 || errorCode === 200) {
+                        transactionId = res.transactionId || res.TransactionId;
+                        if (inputOtpPhone) inputOtpPhone.value = res.msisdn || res.Msisdn || msisdn;
+                        setStep("otp");
+                        showMessage(res.message || "Enter OTP from message.", true);
+                    } else {
+                        showMessage(res.message || "Send OTP failed.", false);
+                        setStep("msisdn");
                     }
-
-                    showLoginMessage((res && res.message) || "Send OTP failed.", false);
                 },
-                error: function (err) {
-                    console.error("GetOtp Error:", err);
-                    setLoginLoading(el.sendButton, false, "Send OTP");
-                    showLoginMessage("Send OTP failed. Please check API service.", false);
+                error: function() {
+                    setLoading(false);
+                    showMessage("Send OTP failed. Please check network connection.", false);
                 }
             });
         }
 
-        function confirmOtp() {
-            const el = getLoginElements();
-            if (!el.card || !window.jQuery) return;
+        function handleConfirmOtp() {
+            var msisdn = (inputOtpPhone ? inputOtpPhone.value : "").trim();
+            var otp = (inputOtp ? inputOtp.value : "").trim();
 
-            showLoginMessage("", false);
-            if (!el.otpInput.value.trim()) {
-                showLoginMessage("Please enter OTP.", false);
-                el.otpInput.focus();
+            if (!otp) {
+                showMessage("Please enter OTP.", false);
+                if (inputOtp) inputOtp.focus();
                 return;
             }
 
-            setLoginLoading(el.confirmButton, true, "Checking...");
+            showMessage("Checking OTP...", true);
+            setLoading(true, "Checking...");
 
             $.ajax({
                 url: "/Login/ConfirmOtpAction",
                 type: "POST",
                 data: {
-                    msisdn: el.msisdnInput.value.trim(),
-                    transactionId: el.transactionInput.value,
-                    otp: el.otpInput.value.trim(),
-                    returnUrl: el.returnUrlInput.value
+                    msisdn: msisdn,
+                    transactionId: transactionId,
+                    otp: otp
                 },
-                success: function (res) {
-                    console.log("ConfirmOtp Response:", res);
-                    setLoginLoading(el.confirmButton, false, "Confirm OTP");
+                success: function(res) {
+                    if (typeof res === "string") {
+                        try { res = JSON.parse(res); } catch(e) {}
+                    }
 
-                    if (res && (res.errorCode === 0 || res.errorCode === 200)) {
+                    var errorCode = res && (res.errorCode ?? res.ErrorCode);
+                    if (errorCode === 0 || errorCode === 200) {
+                        showMessage("Login successful! Redirecting...", true);
                         window.location.href = res.redirectUrl || "/";
-                        return;
+                    } else {
+                        setLoading(false);
+                        showMessage(res.message || "Confirm OTP failed.", false);
                     }
-
-                    showLoginMessage((res && res.message) || "Confirm OTP failed.", false);
                 },
-                error: function (err) {
-                    console.error("ConfirmOtp Error:", err);
-                    setLoginLoading(el.confirmButton, false, "Confirm OTP");
-                    showLoginMessage("Confirm OTP failed. Please check API service and OTP.", false);
+                error: function() {
+                    setLoading(false);
+                    showMessage("Confirm OTP failed. Please check network connection.", false);
                 }
             });
         }
-    </script>
-}
+
+        if (btnConfirm) {
+            btnConfirm.addEventListener("click", function(e) {
+                e.preventDefault();
+                if (loginBusy) return;
+                if (currentStep === "otp") {
+                    handleConfirmOtp();
+                } else {
+                    handleSendOtp();
+                }
+            });
+        }
+
+        if (btnChangePhone) {
+            btnChangePhone.addEventListener("click", function(e) {
+                e.preventDefault();
+                setStep("msisdn");
+                showMessage("");
+            });
+        }
+
+        if (inputPhone) {
+            inputPhone.addEventListener("keypress", function(e) {
+                if (e.key === "Enter") {
+                    e.preventDefault();
+                    if (!loginBusy) handleSendOtp();
+                }
+            });
+        }
+        if (inputOtp) {
+            inputOtp.addEventListener("keypress", function(e) {
+                if (e.key === "Enter") {
+                    e.preventDefault();
+                    if (!loginBusy) handleConfirmOtp();
+                }
+            });
+        }
+    })();
+</script>

+ 1 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/appsettings.json

@@ -1,6 +1,7 @@
 {
   "url": "http://127.0.0.1:8309",
   "CountryCode": "95",
+  "productName":"Kitty_Fall_DAYLY",
   "Logging": {
     "LogLevel": {
       "Default": "Information",

+ 1 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/log4net.config

@@ -14,7 +14,7 @@
 	<appender name="file" type="Common.DailyRollingFileAppender, Common">
 		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
 		<!-- Thư mục chứa log (phải kết thúc bằng dấu \) -->
-		<file value="../../logs/" />
+		<file value="C:\AppLogs\KittyFall\log.txt" />
 		<appendToFile value="true" />
 		<!-- Rolling theo NGÀY: mỗi ngày 1 file riêng, vd 21.05.2026.log -->
 		<rollingStyle value="Date" />

+ 70 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/wwwroot/kitty/css/all.min.css

@@ -3670,6 +3670,7 @@ button {
 }
 
 .login-card {
+  position: relative;
   width: min(100%, 382px);
   border: 3px solid #6b31da;
   border-radius: 28px;
@@ -3767,6 +3768,38 @@ button {
   gap: 14px;
 }
 
+.login-step-panel {
+  display: grid;
+  gap: 14px;
+}
+
+.login-step-panel.login-otp-step,
+.login-step-panel.login-send-step {
+  transition: opacity 0.12s ease;
+}
+
+.login-card[data-login-step="msisdn"] .login-step-panel.login-otp-step {
+  display: none;
+}
+
+.login-card[data-login-step="otp"] .login-step-panel.login-send-step {
+  display: none;
+}
+
+.login-card[data-login-step="msisdn"] .login-step-panel.login-otp-step,
+.login-card[data-login-step="otp"] .login-step-panel.login-send-step {
+  opacity: 0;
+  visibility: hidden;
+  pointer-events: none;
+}
+
+.login-card[data-login-step="otp"] .login-step-panel.login-otp-step,
+.login-card[data-login-step="msisdn"] .login-step-panel.login-send-step {
+  opacity: 1;
+  visibility: visible;
+  pointer-events: auto;
+}
+
 .login-field {
   display: grid;
   gap: 6px;
@@ -3818,6 +3851,43 @@ button {
      object-fit: contain;
 }
 
+.login-submit:disabled {
+  cursor: wait;
+  opacity: 0.82;
+  pointer-events: none;
+}
+
+.login-card.is-loading .login-form,
+.login-card.is-loading .login-session,
+.login-card.is-loading .login-alert {
+  pointer-events: none;
+}
+
+.login-loading {
+  position: absolute;
+  inset: 0;
+  z-index: 3;
+  border-radius: 25px;
+  display: grid;
+  place-items: center;
+  background: rgba(255, 246, 249, 0.5);
+}
+
+.login-spinner {
+  width: 38px;
+  height: 38px;
+  border: 5px solid rgba(87, 33, 234, 0.22);
+  border-top-color: var(--violet);
+  border-radius: 50%;
+  animation: login-spin 0.7s linear infinite;
+}
+
+@keyframes login-spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
 @media (max-width: 499px), (max-height: 719px) {
   .kitty-app {
     width: min(100vw, 500px);

+ 70 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/wwwroot/kitty/css/input.css

@@ -3044,6 +3044,7 @@ button {
 }
 
 .login-card {
+    position: relative;
     width: min(100%, 382px);
     border: 3px solid #6b31da;
     border-radius: 28px;
@@ -3141,6 +3142,38 @@ button {
     gap: 14px;
 }
 
+.login-step-panel {
+    display: grid;
+    gap: 14px;
+}
+
+.login-step-panel.login-otp-step,
+.login-step-panel.login-send-step {
+    transition: opacity 0.12s ease;
+}
+
+.login-card[data-login-step="msisdn"] .login-step-panel.login-otp-step {
+    display: none;
+}
+
+.login-card[data-login-step="otp"] .login-step-panel.login-send-step {
+    display: none;
+}
+
+.login-card[data-login-step="msisdn"] .login-step-panel.login-otp-step,
+.login-card[data-login-step="otp"] .login-step-panel.login-send-step {
+    opacity: 0;
+    visibility: hidden;
+    pointer-events: none;
+}
+
+.login-card[data-login-step="otp"] .login-step-panel.login-otp-step,
+.login-card[data-login-step="msisdn"] .login-step-panel.login-send-step {
+    opacity: 1;
+    visibility: visible;
+    pointer-events: auto;
+}
+
 .login-field {
     display: grid;
     gap: 6px;
@@ -3191,6 +3224,43 @@ button {
     object-fit: contain;
 }
 
+.login-submit:disabled {
+    cursor: wait;
+    opacity: 0.82;
+    pointer-events: none;
+}
+
+.login-card.is-loading .login-form,
+.login-card.is-loading .login-session,
+.login-card.is-loading .login-alert {
+    pointer-events: none;
+}
+
+.login-loading {
+    position: absolute;
+    inset: 0;
+    z-index: 3;
+    border-radius: 25px;
+    display: grid;
+    place-items: center;
+    background: rgba(255, 246, 249, 0.5);
+}
+
+.login-spinner {
+    width: 38px;
+    height: 38px;
+    border: 5px solid rgba(87, 33, 234, 0.22);
+    border-top-color: var(--violet);
+    border-radius: 50%;
+    animation: login-spin 0.7s linear infinite;
+}
+
+@keyframes login-spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
+
 @media (max-width: 499px), (max-height: 719px) {
     .kitty-app {
         width: min(100vw, 500px);