Pārlūkot izejas kodu

Merge branch 'websiteNoNatCash' into milian

# Conflicts:
#	website/Areas/LotteryV2/Views/Home/BuyTicket.cshtml
#	website/Areas/Millions/Views/Home/BuyTicket.cshtml
#	website/Controllers/HomeController.cs
student 3 nedēļas atpakaļ
vecāks
revīzija
ad94a511c5
30 mainītis faili ar 2603 papildinājumiem un 93 dzēšanām
  1. BIN
      API NATCOM LOTTERY.docx
  2. 140 0
      NATCOM_LOTO_WALLET.postman_collection.json
  3. 21 19
      website/Areas/LotteryV2/Controllers/HomeController.cs
  4. 149 14
      website/Areas/LotteryV2/Views/Home/BuyTicket.cshtml
  5. 152 18
      website/Areas/Millions/Controllers/HomeController.cs
  6. 263 27
      website/Areas/Millions/Views/Home/BuyTicket.cshtml
  7. 3 1
      website/Areas/Millions/Views/Home/GameHome.cshtml
  8. 21 10
      website/Areas/Millions/Views/Home/History.cshtml
  9. 370 0
      website/Areas/Millions/Views/Home/PopupFreeTickit.cshtml
  10. 1 1
      website/Areas/Millions/Views/Home/_TermUserTicketHistory.cshtml
  11. 1 0
      website/Common/Constants.cs
  12. 11 0
      website/Controllers/HomeController.cs
  13. 366 0
      website/Languages/Lang.Designer.cs
  14. 115 1
      website/Languages/Lang.fr.resx
  15. 115 1
      website/Languages/Lang.resx
  16. 56 0
      website/Service/ApiFunctions.cs
  17. 27 0
      website/Service/ApiModels.cs
  18. 1 1
      website/Views/Shared/_Layout.cshtml
  19. 6 0
      website/appsettings.json
  20. BIN
      website/wwwroot/Millions/assets/images/banner_free.jpg
  21. BIN
      website/wwwroot/Millions/assets/images/banner_free_transparent.png
  22. BIN
      website/wwwroot/Millions/assets/images/free-ticket-error-43-no-text.png
  23. BIN
      website/wwwroot/Millions/assets/images/free-ticket-error-43.png
  24. BIN
      website/wwwroot/Millions/assets/images/free-ticket-error-44-no-text.png
  25. BIN
      website/wwwroot/Millions/assets/images/free-ticket-error-44.png
  26. BIN
      website/wwwroot/Millions/assets/images/free-ticket-error-45-no-text.png
  27. BIN
      website/wwwroot/Millions/assets/images/free-ticket-floating-icon.png
  28. BIN
      website/wwwroot/Millions/assets/images/free-ticket-success-no-text.png
  29. BIN
      website/wwwroot/Millions/assets/images/free-ticket-success.png
  30. 785 0
      website/wwwroot/Millions/css/popup-free-ticket.css

BIN
API NATCOM LOTTERY.docx


+ 140 - 0
NATCOM_LOTO_WALLET.postman_collection.json

@@ -0,0 +1,140 @@
+{
+  "info": {
+    "_postman_id": "848020d3-bd8b-4f64-bb10-aa7f78073aea",
+    "name": "NATCOM_LOTO_WALLET",
+    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+    "_exporter_id": "19302593"
+  },
+  "item": [
+    {
+      "name": "buyTicket Pro",
+      "request": {
+        "method": "POST",
+        "header": [],
+        "body": {
+          "mode": "raw",
+          "raw": "{\"requestId\":\"123456789\",\"channel\":\"USSD\",\"channelPayment\":\"WALLET\",\"walletPin\":\"123456\",\"msisdn\":\"50935949762\",\"language\":\"0\",\"gameId\":\"30\",\"promotionCode\":\"12345\",\"token\":\"Q5VJq6IQ1O4OuT+y0RkfF9T/FK0FFrRlVHSkaHOPJywlqiqmNs1P5qClOTxdstKEMVtIVhUrGTsxDx41uDHoZvYz3YmINJtj9p+g58ehKVo=\",\r\n\"ticket\": [{\r\n            \"code\": \"03,10,17,21,23,24,28,29,63,70\",\r\n            \"money\": \"50\"\r\n        },\r\n        {\r\n            \"code\": \"03,10,17,21,23,24,28,29,63,70\",\r\n            \"money\": \"50\"\r\n        }\r\n    ]\r\n\r\n}\r\n",
+          "options": {
+            "raw": {
+              "language": "json"
+            }
+          }
+        },
+        "url": {
+          "raw": "http://localhost:41341/api/loto/buyTicketV2/data",
+          "protocol": "http",
+          "host": [
+            "localhost"
+          ],
+          "port": "41341",
+          "path": [
+            "api",
+            "loto",
+            "buyTicketV2",
+            "data"
+          ]
+        }
+      },
+      "response": []
+    },
+    {
+      "name": "buyTicketConfirmPro",
+      "request": {
+        "method": "POST",
+        "header": [],
+        "body": {
+          "mode": "raw",
+          "raw": "{\"requestId\":\"12345\",\"transIdByTicket\":\"5bc0f27b-2a5f-4d35-ab77-6f2de1044440\",\"paymentCode\":\"USSD\",\"channel\":\"USSD\",\"token\":\"Q5VJq6IQ1O4OuT+y0RkfF9T/FK0FFrRlVHSkaHOPJywlqiqmNs1P5qClOTxdstKEMVtIVhUrGTsxDx41uDHoZvYz3YmINJtj9p+g58ehKVo=\"}",
+          "options": {
+            "raw": {
+              "language": "json"
+            }
+          }
+        },
+        "url": {
+          "raw": "http://171.244.50.177:8900/api/loto/confirmTicketV2/data",
+          "protocol": "http",
+          "host": [
+            "171",
+            "244",
+            "50",
+            "177"
+          ],
+          "port": "8900",
+          "path": [
+            "api",
+            "loto",
+            "confirmTicketV2",
+            "data"
+          ]
+        }
+      },
+      "response": []
+    },
+    {
+      "name": "getRandomPromotion",
+      "request": {
+        "method": "POST",
+        "header": [],
+        "body": {
+          "mode": "raw",
+          "raw": "{\"requestId\":\"12345\",\"gameId\":\"30\",\"msisdn\":\"509123456\",\"channel\":\"USSD\",\"language\":\"1\",\"token\":\"Q5VJq6IQ1O4OuT+y0RkfF9T/FK0FFrRlVHSkaHOPJywlqiqmNs1P5qClOTxdstKEMVtIVhUrGTsxDx41uDHoZvYz3YmINJtj9p+g58ehKVo=\"}",
+          "options": {
+            "raw": {
+              "language": "json"
+            }
+          }
+        },
+        "url": {
+          "raw": "http://localhost:41341/api/loto/getPromotionCode/data",
+          "protocol": "http",
+          "host": [
+            "localhost"
+          ],
+          "port": "41341",
+          "path": [
+            "api",
+            "loto",
+            "getPromotionCode",
+            "data"
+          ]
+        }
+      },
+      "response": []
+    },
+    {
+      "name": "getJackport",
+      "request": {
+        "method": "POST",
+        "header": [],
+        "body": {
+          "mode": "raw",
+          "raw": "{\"gameId\":\"33\",\"channel\":\"USSD\",\"language\":\"1\",\"token\":\"Q5VJq6IQ1O4OuT+y0RkfF9T/FK0FFrRlVHSkaHOPJywlqiqmNs1P5qClOTxdstKEMVtIVhUrGTsxDx41uDHoZvYz3YmINJtj9p+g58ehKVo=\"}",
+          "options": {
+            "raw": {
+              "language": "json"
+            }
+          }
+        },
+        "url": {
+          "raw": "http://171.244.50.177:8900/api/loto/getJackportMilinons/data",
+          "protocol": "http",
+          "host": [
+            "171",
+            "244",
+            "50",
+            "177"
+          ],
+          "port": "8900",
+          "path": [
+            "api",
+            "loto",
+            "getJackportMilinons",
+            "data"
+          ]
+        }
+      },
+      "response": []
+    }
+  ]
+}

+ 21 - 19
website/Areas/LotteryV2/Controllers/HomeController.cs

@@ -701,8 +701,8 @@ namespace LotteryWebApp.Areas.LotteryV2.Controllers
                 request.language = CultureInfo.CurrentCulture.Name.StartsWith("en") ? "0" : "1";
                 request.requestId = Guid.NewGuid().ToString();
 
-                ConfirmTicketDataResponse response = api.ConfirmTicketDataApi(configuration, request);
-                log.Info($"[ConfirmTicketData] Call ConfirmTicketDataApi Response: {JsonConvert.SerializeObject(response)}");
+                ConfirmTicketDataResponse response = api.BuyTicketV2Api(configuration, request);
+                log.Info($"[ConfirmTicketData] Call BuyTicketV2Api Response: {JsonConvert.SerializeObject(response)}");
 
                 return Json(response);
             }
@@ -849,24 +849,24 @@ namespace LotteryWebApp.Areas.LotteryV2.Controllers
                     return Json(new { responseCode = Code.SESSION_EXPIRED, responseMessage = "Session expired" });
                 }
 
-                // Step 1: Verify OTP first
-                ConfirmOTPRequest otpRequest = new ConfirmOTPRequest
-                {
-                    otp = request.paymentCode,
-                    msisdn = msisdn,
-                    token = token,
-                    language = CultureInfo.CurrentCulture.Name.StartsWith("en") ? "0" : "1",
-                    channel = configuration.GetSection("channel").Value
-                };
-                log.Info($"[ConfirmBuyingTicketV2] Call ConfirmOTPApi Request: {JsonConvert.SerializeObject(otpRequest)}");
+                var isWalletPayment = request.channelPayment == Constants.NATCASH_WALLET_TICKET;
 
-                ConfirmOTPResponse otpResponse = api.ConfirmOTPApi(configuration, otpRequest);
-                log.Info($"[ConfirmBuyingTicketV2] Call ConfirmOTPApi Response: {JsonConvert.SerializeObject(otpResponse)}");
-
-                if (otpResponse.responseCode != Code.SUCCESS)
+                // The main-account flow keeps the existing OTP verification.
+                if (!isWalletPayment)
                 {
-                    log.Info($"[ConfirmBuyingTicketV2] OTP Verification Failed: {otpResponse.responseMessage}");
-                    return Json(new { responseCode = otpResponse.responseCode, responseMessage = otpResponse.responseMessage });
+                    ConfirmOTPRequest otpRequest = new ConfirmOTPRequest
+                    {
+                        otp = request.paymentCode,
+                        msisdn = msisdn,
+                        token = token,
+                        language = CultureInfo.CurrentCulture.Name.StartsWith("en") ? "0" : "1",
+                        channel = configuration.GetSection("channel").Value
+                    };
+                    ConfirmOTPResponse otpResponse = api.ConfirmOTPApi(configuration, otpRequest);
+                    if (otpResponse.responseCode != Code.SUCCESS)
+                    {
+                        return Json(new { responseCode = otpResponse.responseCode, responseMessage = otpResponse.responseMessage });
+                    }
                 }
 
                 // Step 2: Proceed to Confirm buying
@@ -878,7 +878,9 @@ namespace LotteryWebApp.Areas.LotteryV2.Controllers
 
                 log.Info($"[ConfirmBuyingTicketV2] Call ConfirmBuyingTicketApi Request: {JsonConvert.SerializeObject(request)}");
 
-                ConfirmBuyingTicketResponse response = api.ConfirmBuyingTicketApi(configuration, request);
+                ConfirmBuyingTicketResponse response = isWalletPayment
+                    ? api.ConfirmBuyingTicketV2Api(configuration, request, Constants.WALLET_PAYMENT_CHANNEL, request.walletPin)
+                    : api.ConfirmBuyingTicketApi(configuration, request);
                 log.Info($"[ConfirmBuyingTicketV2] Call ConfirmBuyingTicketApi Response: {JsonConvert.SerializeObject(response)}");
 
                 if (response.responseCode == Code.SUCCESS)

+ 149 - 14
website/Areas/LotteryV2/Views/Home/BuyTicket.cshtml

@@ -294,6 +294,38 @@ else
 </div>
 }
 
+<!-- Payment channel selection -->
+<div id="paymentChannelModal" class="hidden fixed inset-0 z-[140] bg-black/70 backdrop-blur-[3px] items-center justify-center p-4 font-bricolage">
+    <div class="relative w-full max-w-[370px] overflow-hidden rounded-[32px] bg-[#F8F9FC] shadow-[0_28px_70px_rgba(0,0,0,0.35)] animate__animated animate__zoomIn animate__faster">
+        <div class="relative overflow-hidden bg-gradient-to-br from-[#EE0033] to-[#B80028] px-6 pb-7 pt-6 text-white">
+            <div class="absolute -right-8 -top-10 h-32 w-32 rounded-full bg-white/10"></div>
+            <div class="absolute -bottom-12 -left-8 h-28 w-28 rounded-full bg-black/10"></div>
+            <button type="button" onclick="hidePaymentChannelModal()" class="absolute right-4 top-4 z-10 flex h-9 w-9 items-center justify-center rounded-full bg-white/15 text-white transition hover:bg-white/25 active:scale-90" aria-label="Close">
+                <i class="fa-solid fa-xmark"></i>
+            </button>
+            <div class="relative z-[1] flex items-center gap-4">
+                <div class="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-white text-[25px] text-[#EE0033] shadow-lg"><i class="fa-solid fa-wallet"></i></div>
+                <div><h2 class="text-[21px] font-black leading-tight">@Lang.payment_method_title</h2><p class="mt-1 text-[13px] font-semibold text-white/80">@Lang.payment_method_subtitle</p></div>
+            </div>
+        </div>
+        <div class="space-y-3 p-5">
+            <button type="button" onclick="selectPaymentChannel('@Constants.BASIC_WALLET_TICKET')" class="group flex w-full items-center gap-4 rounded-[20px] border-2 border-transparent bg-white p-4 text-left shadow-[0_5px_18px_rgba(31,41,55,0.07)] transition hover:-translate-y-0.5 hover:border-[#EE0033] active:scale-[0.98]">
+                <span class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[#FFE8ED] text-[21px] text-[#EE0033]"><i class="fa-solid fa-mobile-screen-button"></i></span>
+                <span class="min-w-0 flex-1"><span class="block text-[16px] font-black text-[#171A24]">@Lang.payment_main_account</span><span class="mt-0.5 block text-[12px] font-semibold text-gray-500">@Lang.payment_main_account_desc</span></span>
+                <span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition group-hover:bg-[#EE0033] group-hover:text-white"><i class="fa-solid fa-chevron-right text-[12px]"></i></span>
+            </button>
+            <!--
+            <button type="button" onclick="selectPaymentChannel('@Constants.NATCASH_WALLET_TICKET')" class="group flex w-full items-center gap-4 rounded-[20px] border-2 border-transparent bg-white p-4 text-left shadow-[0_5px_18px_rgba(31,41,55,0.07)] transition hover:-translate-y-0.5 hover:border-[#0A9800] active:scale-[0.98]">
+                <span class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[#E7F8E6] text-[21px] text-[#0A9800]"><i class="fa-solid fa-wallet"></i></span>
+                <span class="min-w-0 flex-1"><span class="flex items-center gap-2 text-[16px] font-black text-[#171A24]">@Lang.payment_wallet <span class="rounded-full bg-[#E7F8E6] px-2 py-0.5 text-[9px] font-black text-[#0A9800]">PIN</span></span><span class="mt-0.5 block text-[12px] font-semibold text-gray-500">@Lang.payment_wallet_desc</span></span>
+                <span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition group-hover:bg-[#0A9800] group-hover:text-white"><i class="fa-solid fa-chevron-right text-[12px]"></i></span>
+            </button>
+            -->
+            <p class="pt-1 text-center text-[11px] font-semibold text-gray-400"><i class="fa-solid fa-shield-halved mr-1 text-[#0A9800]"></i>@Lang.payment_security_message</p>
+        </div>
+    </div>
+</div>
+
 <!-- Order Summary Screen (from Figma) -->
 <div id="orderSummaryModal" class="hidden fixed inset-0 z-[150] bg-black/60 flex items-center justify-center p-0 font-bricolage backdrop-blur-[2px]">
     <div class="w-full h-full bg-white flex flex-col animate__animated animate__fadeInRight animate__faster shadow-2xl relative lg:max-w-[414px]">
@@ -451,7 +483,7 @@ else
                         </div>
                     </div>
         
-                    <p class="text-[14px] font-bold text-[#555] text-center px-4 leading-snug mt-0 font-bricolage">@Lang.v2_otp_instruction</p>
+                    <p id="paymentCodeInstruction" class="text-[14px] font-bold text-[#555] text-center px-4 leading-snug mt-0 font-bricolage">@Lang.v2_otp_instruction</p>
         
                     <!-- 6-Slots OTP Grid (Large Gray Blocks) -->
                     <div class="flex items-center justify-center gap-2 w-full mt-1 px-2" id="otpInputs">
@@ -463,6 +495,18 @@ else
                                    placeholder="-">
                         }
                     </div>
+                    <div id="walletPinContainer" class="hidden w-full mt-1 px-2">
+                        <div class="relative">
+                            <i class="fa-solid fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-[#0A9800]"></i>
+                            <input id="walletPinInput" type="password" inputmode="numeric" autocomplete="off"
+                                   oninput="this.value = this.value.replace(/[^0-9]/g, ''); updateWalletPinState()"
+                                   class="h-[52px] w-full rounded-[14px] border-2 border-gray-200 bg-gray-50 pl-11 pr-12 text-center text-[22px] font-black tracking-[0.3em] text-gray-800 outline-none transition focus:border-[#0A9800] focus:bg-white focus:ring-4 focus:ring-green-100"
+                                   placeholder="••••">
+                            <button type="button" onclick="toggleWalletPinVisibility()" class="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 active:scale-90" aria-label="Show PIN">
+                                <i id="walletPinEye" class="fa-solid fa-eye"></i>
+                            </button>
+                        </div>
+                    </div>
         
                     <!-- OTP Error Message -->
                     <p id="otpError" class="text-[#EE0033] font-bold text-[14px] text-center mb-1 hidden animate__animated animate__shakeX"></p>
@@ -846,7 +890,12 @@ else
 
         // --- Order Summary Modal Logic ---
         // --- Payment Preparation & Order Summary Logic ---
+<<<<<<< HEAD
         let pendingPaymentRequest = null;
+=======
+        let pendingTickets = [];
+        let currentChannelPayment = '@Constants.BASIC_WALLET_TICKET';
+>>>>>>> websiteNoNatCash
 
         function preparePayment(event) {
             const isBSMode = @(Model.termType == Constants.PIC10_BIGSMALL_CODE || Model.termType == Constants.PIC10_ODDEVEN_CODE ? "true" : "false");
@@ -889,12 +938,50 @@ else
                 }
             }
 
+<<<<<<< HEAD
             pendingPaymentRequest = {
+=======
+            pendingTickets = tickets;
+            $("#paymentChannelModal").removeClass("hidden").addClass("flex");
+        }
+
+        function hidePaymentChannelModal() {
+            $("#paymentChannelModal").removeClass("flex").addClass("hidden");
+        }
+
+        function selectPaymentChannel(channelPayment) {
+            currentChannelPayment = channelPayment;
+            hidePaymentChannelModal();
+
+            const requestData = {
+>>>>>>> websiteNoNatCash
                 gameId: "@Model.termType",
-                ticket: tickets
+                ticket: pendingTickets,
+                channelPayment: channelPayment
             };
 
+<<<<<<< HEAD
             showOrderSummary(null, tickets);
+=======
+            $.ajax({
+                url: subDomain + '@Url.Action("ConfirmTicketData", "Home")',
+                type: 'POST',
+                contentType: 'application/json',
+                data: JSON.stringify(requestData),
+                success: function(data) {
+
+                    if (data.responseCode === "0" || data.responseCode === "0000") {
+                        showOrderSummary(data, pendingTickets);
+                    } else {
+                        showNotification(data.responseMessage || "Confirmation failed", data.responseCode);
+                    }
+                },
+                error: function(err) {
+
+                    showNotification("Network error occurred.", "warning");
+                }
+            });
+>>>>>>> websiteNoNatCash
         }
 
         let currentTransId = null;
@@ -960,10 +1047,20 @@ else
         }
 
         function confirmCheckout(btn) {
+<<<<<<< HEAD
             if (!pendingPaymentRequest) {
                 showNotification("@Lang.v2_ticket_not_valid", "warning");
                 return;
             }
+=======
+            if (currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET') {
+                showOtpModal(true);
+                return;
+            }
+
+            const originalText = $(btn).html();
+            $(btn).prop('disabled', true).html('<i class="fa-solid fa-spinner fa-spin mr-2"></i>Processing...');
+>>>>>>> websiteNoNatCash
 
             $("#paymentSourceModal").removeClass("hidden").addClass("flex");
         }
@@ -1021,8 +1118,12 @@ else
                 type: 'POST',
                 success: function(data) {
                     if (data.responseCode === "0") {
+<<<<<<< HEAD
                         hidePaymentSourceModal();
                         showOtpModal();
+=======
+                        showOtpModal(false);
+>>>>>>> websiteNoNatCash
                     } else {
                         showNotification(data.responseMessage || "Failed to send OTP", data.responseCode);
                     }
@@ -1034,13 +1135,14 @@ else
         }
 
         let otpInterval;
-        function showOtpModal() {
+        function showOtpModal(isWalletPin) {
             hideOrderSummary();
             $("#otpModal").removeClass("hidden").addClass("flex");
             $("#otpInputs input").val("");
+            $("#walletPinInput").val("").attr("type", "password");
+            $("#walletPinEye").attr("class", "fa-solid fa-eye");
             $("#otpConfirmBtn").prop('disabled', true);
             $("#otpError").addClass("hidden").text("");
-            $("#otp1").focus();
             
             // Map game type to readable name & image
             let gameTypeStr = "@Model.termType";
@@ -1076,7 +1178,33 @@ else
             $("#otpTicketTypeContainer").append(`<span class='text-[17px] font-[900] ${typeColorClass} italic'>${displayName}</span>`);
             
             $("#otpTotalAmount").text($("#summaryTotalAmount").text());
-            startOtpTimer(60);
+            if (isWalletPin) {
+                $("#paymentCodeInstruction").text('@Html.Raw(Lang.payment_wallet_pin_instruction.Replace("'", "\\'"))');
+                $("#otpInputs").addClass("hidden");
+                $("#walletPinContainer").removeClass("hidden");
+                $("#otpTimer, #resendOtpBtn").addClass("hidden");
+                clearInterval(otpInterval);
+                $("#walletPinInput").focus();
+            } else {
+                $("#paymentCodeInstruction").text('@Html.Raw(Lang.v2_otp_instruction.Replace("'", "\\'"))');
+                $("#walletPinContainer").addClass("hidden");
+                $("#otpInputs").removeClass("hidden");
+                startOtpTimer(60);
+                $("#otp1").focus();
+            }
+        }
+
+        function updateWalletPinState() {
+            $("#otpConfirmBtn").prop('disabled', $("#walletPinInput").val().length === 0);
+            $("#otpError").addClass("hidden").text("");
+        }
+
+        function toggleWalletPinVisibility() {
+            const input = $("#walletPinInput");
+            const showPin = input.attr("type") === "password";
+            input.attr("type", showPin ? "text" : "password");
+            $("#walletPinEye").attr("class", showPin ? "fa-solid fa-eye-slash" : "fa-solid fa-eye");
+            input.focus();
         }
 
         // Add backspace support for OTP inputs
@@ -1176,13 +1304,14 @@ else
         }
 
         function finalizePurchase(btn) {
-            let otpCode = "";
-            $("#otpInputs input").each(function() {
-                otpCode += $(this).val();
-            });
+            const isWalletPayment = currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET';
+            let otpCode = isWalletPayment ? $("#walletPinInput").val().trim() : "";
+            if (!isWalletPayment) {
+                $("#otpInputs input").each(function() { otpCode += $(this).val(); });
+            }
             
-            if (otpCode.length < 6) {
-                showNotification("Please enter full 6-digit OTP", "warning");
+            if ((isWalletPayment && otpCode.length === 0) || (!isWalletPayment && otpCode.length < 6)) {
+                showNotification(isWalletPayment ? '@Html.Raw(Lang.payment_wallet_pin_instruction.Replace("'", "\\'"))' : "Please enter full 6-digit OTP", "warning");
                 return;
             }
 
@@ -1191,7 +1320,9 @@ else
 
             const finalData = {
                 transIdByTicket: currentTransId,
-                paymentCode: otpCode
+                paymentCode: currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET' ? null : otpCode,
+                walletPin: currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET' ? otpCode : null,
+                channelPayment: currentChannelPayment
             };
 
             // Call the unified backend action that verifies OTP and then buys the ticket
@@ -1212,8 +1343,12 @@ else
                              showNotification(res.responseMessage || "System is upgrading", res.responseCode);
                         } else {
                              $("#otpError").text(res.responseMessage || "Invalid OTP").removeClass("hidden");
-                             $("#otpInputs input").val("");
-                             $("#otp1").focus();
+                             if (isWalletPayment) {
+                                 $("#walletPinInput").val("").focus();
+                             } else {
+                                 $("#otpInputs input").val("");
+                                 $("#otp1").focus();
+                             }
                         }
                     }
                 },

+ 152 - 18
website/Areas/Millions/Controllers/HomeController.cs

@@ -23,6 +23,11 @@ namespace LotteryWebApp.Areas.Millions.Controllers
     public class HomeController : BaseController
     {
         private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(Program));
+        private const string FreeTicketAutoCheckSessionKey = "Millions.FreeTicket.AutoChecked";
+        private const string FreeTicketPromotionCodeSessionKey = "Millions.FreeTicket.PromotionCode";
+        private const string FreeTicketTransactionSessionKey = "Millions.FreeTicket.TransactionId";
+        private const string PendingTicketTransactionSessionKey = "Millions.PendingTicket.TransactionId";
+        private const string OtpVerifiedTransactionSessionKey = "Millions.OTP.VerifiedTransactionId";
 
         IConfiguration configuration;
         private readonly IWebHostEnvironment webHostEnvironment;
@@ -934,10 +939,67 @@ namespace LotteryWebApp.Areas.Millions.Controllers
             {
                 log.Error(ex);
             }
-
+            
             return model;
         }
 
+        [HttpPost]
+        [ValidateAntiForgeryToken]
+        public IActionResult GetFreeTicket(string type)
+        {
+            try
+            {
+                var token = HttpContext.Session.GetComplexData<string>("token");
+                var msisdn = HttpContext.Session.GetComplexData<string>("msisdn");
+                log.Info($"[Millions.GetFreeTicket] START - msisdn={msisdn}");
+
+                if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(msisdn))
+                {
+                    log.Info($"[Millions.GetFreeTicket] Session expired - msisdn={msisdn}");
+                    return Json(new { responseCode = Code.SESSION_EXPIRED, responseMessage = "Session expired" });
+                }
+
+                if (type == "1")
+                {
+                    if (HttpContext.Session.GetString(FreeTicketAutoCheckSessionKey) == "1")
+                    {
+                        log.Info($"[Millions.GetFreeTicket] Auto-check skipped for current session - msisdn={msisdn}");
+                        return Json(new { responseCode = "", responseMessage = "Auto-check already completed", skipped = true });
+                    }
+
+                    // Set before the downstream call to prevent concurrent Home requests
+                    // from triggering multiple automatic checks in the same login session.
+                    HttpContext.Session.SetString(FreeTicketAutoCheckSessionKey, "1");
+                }
+
+                string lang = CultureInfo.CurrentCulture.Name;
+                string langValue = (lang.StartsWith("en") || lang.StartsWith("fr")) ? "0" : "1";
+                GetFreeTicketRequest request = new GetFreeTicketRequest
+                {
+                    requestId = Guid.NewGuid().ToString(),
+                    gameId = Constants.Millions_CODE,
+                    type = type,
+                    msisdn = msisdn,
+                    language = langValue,
+                    token = token
+                };
+
+                GetFreeTicketResponse response = api.GetFreeTicketApi(configuration, request);
+                HttpContext.Session.Remove(FreeTicketPromotionCodeSessionKey);
+                if (!string.IsNullOrWhiteSpace(response?.code))
+                {
+                    HttpContext.Session.SetString(FreeTicketPromotionCodeSessionKey, response.code.Trim());
+                }
+                log.Info($"[Millions.GetFreeTicket] Response - code={response?.responseCode}, msg={response?.responseMessage}, voucher={response?.code}");
+                return Json(response);
+            }
+            catch (Exception ex)
+            {
+                log.Error("[Millions.GetFreeTicket] EXCEPTION", ex);
+                return Json(new { responseCode = Code.ERROR, responseMessage = ex.Message });
+            }
+        }
+
         [HttpPost]
         [ValidateAntiForgeryToken]
         public IActionResult TermResult(string termType)
@@ -993,14 +1055,38 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                 request.language = CultureInfo.CurrentCulture.Name.StartsWith("en") ? "0" : "1";
                 request.requestId = Guid.NewGuid().ToString();
 
+                var sessionPromotionCode = HttpContext.Session.GetString(FreeTicketPromotionCodeSessionKey);
+                var isValidPromotionCode = !string.IsNullOrWhiteSpace(request.promotionCode)
+                    && string.Equals(request.promotionCode, sessionPromotionCode, StringComparison.Ordinal);
+                if (isValidPromotionCode)
+                {
+                    request.gameId = Constants.Millions_CODE;
+                    request.channelPayment = Constants.BASIC_WALLET_TICKET;
+                }
+                else
+                {
+                    request.promotionCode = null;
+                }
+
                 // DEBUG: Log request data
                 var ticketDebug = request.ticket != null ? string.Join("; ", request.ticket.Select(t => $"code={t.code}, money={t.money}")) : "NULL";
-                log.Info($"[ConfirmTicketData] gameId={request.gameId}, msisdn={request.msisdn}, tickets=[{ticketDebug}]");
+                log.Info($"[BuyTicketV2.Prepare] gameId={request.gameId}, msisdn={request.msisdn}, tickets=[{ticketDebug}]");
 
-                ConfirmTicketDataResponse response = api.ConfirmTicketDataApi(configuration, request);
+                ConfirmTicketDataResponse response = api.BuyTicketV2Api(configuration, request);
+
+                if (response.responseCode == Code.SUCCESS && !string.IsNullOrWhiteSpace(response.transId))
+                {
+                    HttpContext.Session.SetString(PendingTicketTransactionSessionKey, response.transId);
+                    HttpContext.Session.Remove(OtpVerifiedTransactionSessionKey);
+                }
+
+                if (isValidPromotionCode && response.responseCode == Code.SUCCESS && !string.IsNullOrWhiteSpace(response.transId))
+                {
+                    HttpContext.Session.SetString(FreeTicketTransactionSessionKey, response.transId);
+                }
                 
                 // DEBUG: Log response
-                log.Info($"[ConfirmTicketData] Response: code={response.responseCode}, msg={response.responseMessage}, transId={response.transId}");
+                log.Info($"[BuyTicketV2.Prepare] Response: code={response.responseCode}, msg={response.responseMessage}, transId={response.transId}");
                 
                 return Json(response);
             }
@@ -1082,7 +1168,23 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                 request.token = token;
                 request.msisdn = msisdn;
 
+                var transactionId = request.transIdByTicket;
+                var pendingTransactionId = HttpContext.Session.GetString(PendingTicketTransactionSessionKey);
+                if (string.IsNullOrWhiteSpace(transactionId)
+                    || !string.Equals(transactionId, pendingTransactionId, StringComparison.Ordinal))
+                {
+                    return Json(new { responseCode = "4", responseMessage = GetLangFromCode("4") });
+                }
+
+                // This value is only used by the website to bind OTP verification
+                // to the prepared transaction; it must not be sent to the OTP API.
+                request.transIdByTicket = null;
+
                 ConfirmOTPResponse response = api.ConfirmOTPApi(configuration, request);
+                if (response.responseCode == Code.SUCCESS)
+                {
+                    HttpContext.Session.SetString(OtpVerifiedTransactionSessionKey, transactionId);
+                }
                 return Json(response);
             }
             catch (Exception ex)
@@ -1110,6 +1212,10 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                 };
 
                 SendOTPResponse response = api.SendOTPApi(configuration, request);
+                if (response.responseCode == Code.SUCCESS)
+                {
+                    HttpContext.Session.Remove(OtpVerifiedTransactionSessionKey);
+                }
                 return Json(response);
             }
             catch (Exception ex)
@@ -1128,20 +1234,23 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                 var msisdn = HttpContext.Session.GetComplexData<string>("msisdn");
                 if (string.IsNullOrEmpty(token)) return Json(new { responseCode = Code.SESSION_EXPIRED, responseMessage = "Session expired" });
 
-                // Step 1: Verify OTP first
-                ConfirmOTPRequest otpRequest = new ConfirmOTPRequest
-                {
-                    otp = request.paymentCode,
-                    msisdn = msisdn,
-                    token = token,
-                    language = CultureInfo.CurrentCulture.Name.StartsWith("en") ? "0" : "1",
-                    channel = configuration.GetSection("channel").Value
-                };
-                ConfirmOTPResponse otpResponse = api.ConfirmOTPApi(configuration, otpRequest);
+                var isWalletPayment = request.channelPayment == Constants.NATCASH_WALLET_TICKET;
+                var freeTicketTransactionId = HttpContext.Session.GetString(FreeTicketTransactionSessionKey);
+                var isFreeTicketTransaction = !string.IsNullOrWhiteSpace(request.transIdByTicket)
+                    && string.Equals(request.transIdByTicket, freeTicketTransactionId, StringComparison.Ordinal);
 
-                if (otpResponse.responseCode != Code.SUCCESS)
+                // Main-account and promotion purchases must have completed the
+                // separate ConfirmOTP request for this exact prepared transaction.
+                if (!isWalletPayment)
                 {
-                    return Json(new { responseCode = otpResponse.responseCode, responseMessage = otpResponse.responseMessage });
+                    var otpVerifiedTransactionId = HttpContext.Session.GetString(OtpVerifiedTransactionSessionKey);
+                    if (string.IsNullOrWhiteSpace(request.transIdByTicket)
+                        || !string.Equals(request.transIdByTicket, otpVerifiedTransactionId, StringComparison.Ordinal))
+                    {
+                        return Json(new { responseCode = "31", responseMessage = GetLangFromCode("31") });
+                    }
+
+                    HttpContext.Session.Remove(OtpVerifiedTransactionSessionKey);
                 }
 
                 // Step 2: Proceed to Confirm buying
@@ -1151,9 +1260,34 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                 request.language = CultureInfo.CurrentCulture.Name.StartsWith("en") ? "0" : "1";
                 request.channel = configuration.GetSection("channel").Value;
 
-                ConfirmBuyingTicketResponse response = api.ConfirmBuyingTicketApi(configuration, request);
+                request.gameId = Constants.Millions_CODE;
+
+                ConfirmBuyingTicketResponse response;
+                if (isWalletPayment)
+                {
+                    response = api.ConfirmBuyingTicketV2Api(
+                        configuration,
+                        request,
+                        Constants.WALLET_PAYMENT_CHANNEL,
+                        request.walletPin);
+                }
+                else if (isFreeTicketTransaction)
+                {
+                    // Promotion transactions are created by BuyTicketV2 and must
+                    // be completed by the matching V2 confirmation endpoint.
+                    response = api.ConfirmBuyingTicketV2Api(configuration, request);
+                }
+                else
+                {
+                    response = api.ConfirmBuyingTicketApi(configuration, request);
+                }
                 if (response.responseCode == Code.SUCCESS)
                 {
+                    if (isFreeTicketTransaction)
+                    {
+                        HttpContext.Session.Remove(FreeTicketPromotionCodeSessionKey);
+                        HttpContext.Session.Remove(FreeTicketTransactionSessionKey);
+                    }
                     UpdateUserStatus(msisdn, token);
                 }
                 return Json(response);
@@ -1212,7 +1346,7 @@ namespace LotteryWebApp.Areas.Millions.Controllers
                     log.Info($"[Millions.BuyTicket] Redirect login - missing token, termType={termType}, msisdn={msisdn}");
                     return RedirectToAction("Login", "Account", new { area = "" });
                 }
-
+                
                 HomeIndex_ViewModel model = new HomeIndex_ViewModel();
                 model.termType = termType ?? Constants.Millions_CODE;
                 log.Info($"[Millions.BuyTicket] Check active term - gameId={model.termType}, msisdn={msisdn}");

+ 263 - 27
website/Areas/Millions/Views/Home/BuyTicket.cshtml

@@ -6,6 +6,16 @@
 }
 @using LotteryWebApp.Languages;
 @using LotteryWebApp.Common;
+@using Microsoft.AspNetCore.Http;
+@{
+    var isFreeTicketMode = Context.Request.Query["freeTicketMode"] == "1"
+        && Model.termType == Constants.Millions_CODE;
+    var freeTicketPromotionCode = isFreeTicketMode
+        ? Context.Session.GetString("Millions.FreeTicket.PromotionCode")
+        : null;
+    var hasFreeTicketPromotion = isFreeTicketMode
+        && !string.IsNullOrWhiteSpace(freeTicketPromotionCode);
+}
 
 @section Styles {
     <link rel="stylesheet" href="/Millions/css/buy-ticket.css" asp-append-version="true" />
@@ -198,7 +208,7 @@
                         <span class="text-[13px] font-black" style="color: @themeColor">HTG</span>
                     </div>
                 </div>
-                <button id="bsPaymentBtn" onclick="bsShowPayment()" class="text-white text-[20px] font-black px-10 py-3.5 rounded-xl shadow-lg active:scale-95 transition-all" style="background: @themeColor;">
+                <button type="button" id="bsPaymentBtn" onpointerdown="armPaymentClick(event)" onkeydown="armPaymentKey(event)" onclick="bsShowPayment(event)" class="text-white text-[20px] font-black px-10 py-3.5 rounded-xl shadow-lg active:scale-95 transition-all" style="background: @themeColor;">
                     @Lang.millions_payment
                 </button>
             </div>
@@ -239,12 +249,15 @@ else
 
     <!-- Ticket Scrollable List Area -->
     <div id="ticketContainer" class="ticket-scroll-area p-4 flex flex-col gap-6 bg-[#F5F5F5] pb-6">
-        @for (int i = 1; i <= 2; i++)
+        @for (int i = 1; i <= (isFreeTicketMode ? 1 : 2); i++)
         {
             <div class="ticket-card relative group active:bg-gray-50 transition-colors cursor-pointer animate__animated animate__fadeIn">
-                <button onclick="removeEntry(this)" class="absolute -right-2 top-2 w-7 h-7 bg-[#FF0000] text-white rounded-full flex items-center justify-center text-[11px] shadow-sm z-[10] hover:bg-red-700 transition-all border-2 border-red-600">
-                    <i class="fa-solid fa-xmark"></i>
-                </button>
+                @if (!isFreeTicketMode)
+                {
+                    <button onclick="removeEntry(this)" class="absolute -right-2 top-2 w-7 h-7 bg-[#FF0000] text-white rounded-full flex items-center justify-center text-[11px] shadow-sm z-[10] hover:bg-red-700 transition-all border-2 border-red-600">
+                        <i class="fa-solid fa-xmark"></i>
+                    </button>
+                }
                 <div class="absolute left-2 top-4 w-9 h-9 rounded-full border-[3px] border-white flex items-center justify-center text-white font-black text-sm shadow-md ticket-index">@i</div>
                 <div class="flex flex-col gap-1 pl-8">
                     <span class="text-[16px] font-extrabold text-[#333]">@Lang.millions_select_10_lucky_numbers</span>
@@ -280,11 +293,14 @@ else
             </div>
         }
 
-        <div class="w-full pt-2" id="addButtonWrapper">
-            <button onclick="addNewTicket()" class="w-fit min-w-[220px] mx-auto bg-[#2B83F2] text-white font-black text-md py-2.5 px-6 rounded-xl shadow-md border-b-[4px] border-[#1F66D6] active:translate-y-[1px] active:border-b-[1px] transition-all flex items-center justify-center gap-2">
-                @Lang.millions_add_more_ticket
-            </button>
-        </div>
+        @if (!isFreeTicketMode)
+        {
+            <div class="w-full pt-2" id="addButtonWrapper">
+                <button onclick="addNewTicket()" class="w-fit min-w-[220px] mx-auto bg-[#2B83F2] text-white font-black text-md py-2.5 px-6 rounded-xl shadow-md border-b-[4px] border-[#1F66D6] active:translate-y-[1px] active:border-b-[1px] transition-all flex items-center justify-center gap-2">
+                    @Lang.millions_add_more_ticket
+                </button>
+            </div>
+        }
         <div class="h-4"></div>
     </div>
 
@@ -302,7 +318,7 @@ else
                     <span class="text-[13px] font-black text-[#0062FF]">HTG</span>
                 </div>
             </div>
-            <button onclick="preparePayment()" class="bg-[#0062FF] text-white text-[20px] font-black px-10 py-3.5 rounded-xl shadow-[0px_6px_15px_rgba(238,0,51,0.25)] active:scale-95 transition-all">
+            <button type="button" onpointerdown="armPaymentClick(event)" onkeydown="armPaymentKey(event)" onclick="preparePayment(event)" class="bg-[#0062FF] text-white text-[20px] font-black px-10 py-3.5 rounded-xl shadow-[0px_6px_15px_rgba(238,0,51,0.25)] active:scale-95 transition-all">
                 @Lang.millions_payment
             </button>
         </div>
@@ -311,6 +327,28 @@ else
 </div>
 }
 
+<!-- Payment channel selection -->
+<div id="paymentChannelModal" class="hidden fixed inset-0 z-[140] bg-black/70 backdrop-blur-[3px] items-center justify-center p-4 font-bricolage">
+    <div class="relative w-full max-w-[370px] overflow-hidden rounded-[32px] bg-[#F8F9FC] shadow-[0_28px_70px_rgba(0,0,0,0.35)] animate__animated animate__zoomIn animate__faster">
+        <div class="relative overflow-hidden bg-gradient-to-br from-[#0062FF] to-[#003FA8] px-6 pb-7 pt-6 text-white">
+            <div class="absolute -right-8 -top-10 h-32 w-32 rounded-full bg-white/10"></div><div class="absolute -bottom-12 -left-8 h-28 w-28 rounded-full bg-black/10"></div>
+            <button type="button" onclick="hidePaymentChannelModal()" class="absolute right-4 top-4 z-10 flex h-9 w-9 items-center justify-center rounded-full bg-white/15 text-white transition hover:bg-white/25 active:scale-90" aria-label="Close"><i class="fa-solid fa-xmark"></i></button>
+            <div class="relative z-[1] flex items-center gap-4"><div class="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-white text-[25px] text-[#0062FF] shadow-lg"><i class="fa-solid fa-wallet"></i></div><div><h2 class="text-[21px] font-black leading-tight">@Lang.payment_method_title</h2><p class="mt-1 text-[13px] font-semibold text-white/80">@Lang.payment_method_subtitle</p></div></div>
+        </div>
+        <div class="space-y-3 p-5">
+            <button type="button" onclick="selectPaymentChannel('@Constants.BASIC_WALLET_TICKET')" class="group flex w-full items-center gap-4 rounded-[20px] border-2 border-transparent bg-white p-4 text-left shadow-[0_5px_18px_rgba(31,41,55,0.07)] transition hover:-translate-y-0.5 hover:border-[#0062FF] active:scale-[0.98]">
+                <span class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[#E7F0FF] text-[21px] text-[#0062FF]"><i class="fa-solid fa-mobile-screen-button"></i></span><span class="min-w-0 flex-1"><span class="block text-[16px] font-black text-[#171A24]">@Lang.payment_main_account</span><span class="mt-0.5 block text-[12px] font-semibold text-gray-500">@Lang.payment_main_account_desc</span></span><span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition group-hover:bg-[#0062FF] group-hover:text-white"><i class="fa-solid fa-chevron-right text-[12px]"></i></span>
+            </button>
+            <!--
+            <button type="button" onclick="selectPaymentChannel('@Constants.NATCASH_WALLET_TICKET')" class="group flex w-full items-center gap-4 rounded-[20px] border-2 border-transparent bg-white p-4 text-left shadow-[0_5px_18px_rgba(31,41,55,0.07)] transition hover:-translate-y-0.5 hover:border-[#0A9800] active:scale-[0.98]">
+                <span class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[#E7F8E6] text-[21px] text-[#0A9800]"><i class="fa-solid fa-wallet"></i></span><span class="min-w-0 flex-1"><span class="flex items-center gap-2 text-[16px] font-black text-[#171A24]">@Lang.payment_wallet <span class="rounded-full bg-[#E7F8E6] px-2 py-0.5 text-[9px] font-black text-[#0A9800]">PIN</span></span><span class="mt-0.5 block text-[12px] font-semibold text-gray-500">@Lang.payment_wallet_desc</span></span><span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition group-hover:bg-[#0A9800] group-hover:text-white"><i class="fa-solid fa-chevron-right text-[12px]"></i></span>
+            </button>
+             -->
+            <p class="pt-1 text-center text-[11px] font-semibold text-gray-400"><i class="fa-solid fa-shield-halved mr-1 text-[#0A9800]"></i>@Lang.payment_security_message</p>
+        </div>
+    </div>
+</div>
+
 <!-- Order Summary Screen (from Figma) -->
 <div id="orderSummaryModal" class="hidden fixed inset-0 z-[150] bg-black/60 flex items-center justify-center p-0 font-bricolage backdrop-blur-[2px]">
         <div class="w-full h-full bg-white flex flex-col animate__animated animate__fadeInRight animate__faster shadow-2xl relative lg:max-w-[414px]">
@@ -468,7 +506,7 @@ else
                         </div>
                     </div>
         
-                    <p class="text-[14px] font-bold text-[#555] text-center px-4 leading-snug mt-0 font-bricolage">@Lang.millions_otp_instruction</p>
+                    <p id="paymentCodeInstruction" class="text-[14px] font-bold text-[#555] text-center px-4 leading-snug mt-0 font-bricolage">@Lang.millions_otp_instruction</p>
         
                     <!-- 6-Slots OTP Grid (Large Gray Blocks) -->
                     <div class="flex items-center justify-center gap-2 w-full mt-1 px-2" id="otpInputs">
@@ -480,6 +518,16 @@ else
                                    placeholder="-">
                         }
                     </div>
+                    <div id="walletPinContainer" class="hidden w-full mt-1 px-2">
+                        <div class="relative">
+                            <i class="fa-solid fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-[#0A9800]"></i>
+                            <input id="walletPinInput" type="password" inputmode="numeric" autocomplete="off"
+                                   oninput="this.value = this.value.replace(/[^0-9]/g, ''); updateWalletPinState()"
+                                   class="h-[52px] w-full rounded-[14px] border-2 border-gray-200 bg-gray-50 pl-11 pr-12 text-center text-[22px] font-black tracking-[0.3em] text-gray-800 outline-none transition focus:border-[#0A9800] focus:bg-white focus:ring-4 focus:ring-green-100"
+                                   placeholder="••••">
+                            <button type="button" onclick="toggleWalletPinVisibility()" class="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 active:scale-90" aria-label="Show PIN"><i id="walletPinEye" class="fa-solid fa-eye"></i></button>
+                        </div>
+                    </div>
         
                     <!-- OTP Error Message -->
                     <p id="otpError" class="text-[#0062FF] font-bold text-[14px] text-center mb-1 hidden animate__animated animate__shakeX"></p>
@@ -504,6 +552,9 @@ else
     <script>
 
         const maxBalls = @(Model.termType == Constants.PIC10_BASIC_CODE ? 10 : (Model.termType == Constants.Millions_CODE ? 6 : (Model.termType == Constants.PIC10_BIGSMALL_CODE ? 13 : 15)));
+        const isFreeTicketMode = @(isFreeTicketMode ? "true" : "false");
+        const hasFreeTicketPromotion = @(hasFreeTicketPromotion ? "true" : "false");
+        const freeTicketPromotionCode = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(freeTicketPromotionCode));
 
         function reindexTickets() {
             $(".ticket-card").each(function(index) {
@@ -928,12 +979,16 @@ else
                     filledTickets++;
                 }
             });
-            const pricePerTicket = @(Model.termType == Constants.PIC10_BASIC_CODE ? "10" : (Model.termType == Constants.Millions_CODE ? "20" : "50"));
+            const pricePerTicket = hasFreeTicketPromotion
+                ? 0
+                : @(Model.termType == Constants.PIC10_BASIC_CODE ? "10" : (Model.termType == Constants.Millions_CODE ? "20" : "50"));
             $("#totalPrice").text(formatMoneyV2(filledTickets * pricePerTicket));
             $("#totalTicketCount").text(filledTickets);
         }
 
         function addNewTicket() {
+            if (isFreeTicketMode) return;
+
             var container = $("#ticketContainer");
             var wrapper = $("#addButtonWrapper");
             var currentIndex = $(".ticket-card").length + 1;
@@ -982,9 +1037,35 @@ else
 
         // --- Order Summary Modal Logic ---
         // --- Payment Preparation & Order Summary Logic ---
+<<<<<<< HEAD
         let pendingPaymentRequest = null;
+=======
+        let pendingTickets = [];
+        let currentChannelPayment = '@Constants.BASIC_WALLET_TICKET';
+        let isPreparingCheckout = false;
+        let isSendingOtp = false;
+        let isFinalizingPurchase = false;
+        let paymentClickArmedAt = 0;
+
+        function armPaymentClick(event) {
+            if (event && event.isTrusted === true) paymentClickArmedAt = Date.now();
+        }
+
+        function armPaymentKey(event) {
+            if (event && event.isTrusted === true && (event.key === "Enter" || event.key === " ")) {
+                paymentClickArmedAt = Date.now();
+            }
+        }
+>>>>>>> websiteNoNatCash
 
         function preparePayment(event) {
+            // Never prepare or confirm a ticket during page initialization.
+            // A trusted pointer/key action on this button must immediately precede the click.
+            const hasFreshPaymentIntent = paymentClickArmedAt > 0
+                && Date.now() - paymentClickArmedAt <= 1500;
+            paymentClickArmedAt = 0;
+            if (!event || event.type !== "click" || event.isTrusted !== true || !hasFreshPaymentIntent) return;
+
             const isBSMode = @(Model.termType == Constants.PIC10_BIGSMALL_CODE || Model.termType == Constants.PIC10_ODDEVEN_CODE ? "true" : "false");
             const tickets = [];
 
@@ -1007,6 +1088,8 @@ else
                 });
             } else {
                 $(".ticket-card").each(function() {
+                    if (isFreeTicketMode && tickets.length >= 1) return false;
+
                     const balls = $(this).find(".ball-filled");
                     if (balls.length === maxBalls) {
                         const selectedNumbers = [];
@@ -1025,12 +1108,62 @@ else
                 }
             }
 
+<<<<<<< HEAD
             pendingPaymentRequest = {
                 gameId: "@Model.termType",
                 ticket: tickets
             };
 
             showOrderSummary(null, tickets);
+=======
+            pendingTickets = tickets;
+            if (hasFreeTicketPromotion) {
+                selectPaymentChannel('@Constants.BASIC_WALLET_TICKET');
+                return;
+            }
+            $("#paymentChannelModal").removeClass("hidden").addClass("flex");
+        }
+
+        function hidePaymentChannelModal() {
+            $("#paymentChannelModal").removeClass("flex").addClass("hidden");
+        }
+
+        function selectPaymentChannel(channelPayment) {
+            if (isPreparingCheckout) return;
+            isPreparingCheckout = true;
+
+            currentChannelPayment = channelPayment;
+            hidePaymentChannelModal();
+
+            const requestData = {
+                gameId: isFreeTicketMode ? "@Constants.Millions_CODE" : "@Model.termType",
+                ticket: pendingTickets,
+                channelPayment: channelPayment,
+                promotionCode: isFreeTicketMode ? freeTicketPromotionCode : null
+            };
+
+            $.ajax({
+                url: subDomain + '@Url.Action("ConfirmTicketData", "Home")',
+                type: 'POST',
+                contentType: 'application/json',
+                data: JSON.stringify(requestData),
+                success: function(data) {
+
+                    if (data.responseCode === "0" || data.responseCode === "0000") {
+                        showOrderSummary(data, pendingTickets);
+                    } else {
+                        showNotification(data.responseMessage || "Confirmation failed", data.responseCode);
+                    }
+                },
+                error: function(err) {
+
+                    showNotification("Network error occurred.", "warning");
+                },
+                complete: function() {
+                    isPreparingCheckout = false;
+                }
+            });
+>>>>>>> websiteNoNatCash
         }
 
         let currentTransId = null;
@@ -1079,7 +1212,7 @@ else
                         </div>
                         <div class="w-24 shrink-0 bg-white border border-gray-200 rounded-2xl p-2.5 flex items-center justify-center shadow-sm">
                             <div class="flex items-baseline gap-0.5">
-                                <span class="text-[20px] font-black text-black">${formatMoneyV2(t.money)}</span>
+                                <span class="text-[20px] font-black text-black">${formatMoneyV2(hasFreeTicketPromotion ? 0 : t.money)}</span>
                                 <span class="text-[12px] font-black text-[#0062FF]">HTG</span>
                             </div>
                         </div>
@@ -1089,9 +1222,15 @@ else
             });
 
             $("#summaryTotalCount").text(displayTickets.length);
+<<<<<<< HEAD
             const totalMoney = apiData
                 ? (apiData.totalMoneyPayment || apiData.totalMoney || "0")
                 : displayTickets.reduce((sum, ticket) => sum + (parseInt(ticket.money, 10) || 0), 0);
+=======
+            const totalMoney = hasFreeTicketPromotion
+                ? "0"
+                : (apiData.totalMoneyPayment || apiData.totalMoney || "0");
+>>>>>>> websiteNoNatCash
             $("#summaryTotalAmount").text(formatMoneyV2(totalMoney));
 
             summaryModal.removeClass("hidden").addClass("flex");
@@ -1102,10 +1241,23 @@ else
         }
 
         function confirmCheckout(btn) {
+<<<<<<< HEAD
             if (!pendingPaymentRequest) {
                 showNotification("@Lang.millions_ticket_not_valid", "warning");
                 return;
             }
+=======
+            if (isSendingOtp) return;
+
+            if (currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET') {
+                showOtpModal(true);
+                return;
+            }
+
+            isSendingOtp = true;
+            const originalText = $(btn).html();
+            $(btn).prop('disabled', true).html('<i class="fa-solid fa-spinner fa-spin mr-2"></i>Processing...');
+>>>>>>> websiteNoNatCash
 
             $("#paymentSourceModal").removeClass("hidden").addClass("flex");
         }
@@ -1163,26 +1315,34 @@ else
                 type: 'POST',
                 success: function(data) {
                     if (data.responseCode === "0") {
+<<<<<<< HEAD
                         hidePaymentSourceModal();
                         showOtpModal();
+=======
+                        showOtpModal(false);
+>>>>>>> websiteNoNatCash
                     } else {
                         showNotification(data.responseMessage || "Failed to send OTP", data.responseCode);
                     }
                 },
                 error: function() {
                     showNotification("Network error occurred.", "warning");
+                },
+                complete: function() {
+                    isSendingOtp = false;
                 }
             });
         }
 
         let otpInterval;
-        function showOtpModal() {
+        function showOtpModal(isWalletPin) {
             hideOrderSummary();
             $("#otpModal").removeClass("hidden").addClass("flex");
             $("#otpInputs input").val("");
+            $("#walletPinInput").val("").attr("type", "password");
+            $("#walletPinEye").attr("class", "fa-solid fa-eye");
             $("#otpConfirmBtn").prop('disabled', true);
             $("#otpError").addClass("hidden").text("");
-            $("#otp1").focus();
             
             // Map game type to readable name & image
             let gameTypeStr = "@Model.termType";
@@ -1219,7 +1379,33 @@ else
             $("#otpTicketTypeContainer").append(`<span class='text-[17px] font-[900] ${typeColorClass} italic'>${displayName}</span>`);
             
             $("#otpTotalAmount").text($("#summaryTotalAmount").text());
-            startOtpTimer(60);
+            if (isWalletPin) {
+                $("#paymentCodeInstruction").text('@Html.Raw(Lang.payment_wallet_pin_instruction.Replace("'", "\\'"))');
+                $("#otpInputs").addClass("hidden");
+                $("#walletPinContainer").removeClass("hidden");
+                $("#otpTimer, #resendOtpBtn").addClass("hidden");
+                clearInterval(otpInterval);
+                $("#walletPinInput").focus();
+            } else {
+                $("#paymentCodeInstruction").text('@Html.Raw(Lang.millions_otp_instruction.Replace("'", "\\'"))');
+                $("#walletPinContainer").addClass("hidden");
+                $("#otpInputs").removeClass("hidden");
+                startOtpTimer(60);
+                $("#otp1").focus();
+            }
+        }
+
+        function updateWalletPinState() {
+            $("#otpConfirmBtn").prop('disabled', $("#walletPinInput").val().length === 0);
+            $("#otpError").addClass("hidden").text("");
+        }
+
+        function toggleWalletPinVisibility() {
+            const input = $("#walletPinInput");
+            const showPin = input.attr("type") === "password";
+            input.attr("type", showPin ? "text" : "password");
+            $("#walletPinEye").attr("class", showPin ? "fa-solid fa-eye-slash" : "fa-solid fa-eye");
+            input.focus();
         }
 
         // Add backspace support for OTP inputs
@@ -1320,25 +1506,68 @@ else
         }
 
         function finalizePurchase(btn) {
-            let otpCode = "";
-            $("#otpInputs input").each(function() {
-                otpCode += $(this).val();
-            });
+            if (isFinalizingPurchase) return;
+
+            const isWalletPayment = currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET';
+            let otpCode = isWalletPayment ? $("#walletPinInput").val().trim() : "";
+            if (!isWalletPayment) {
+                $("#otpInputs input").each(function() { otpCode += $(this).val(); });
+            }
             
-            if (otpCode.length < 6) {
-                showNotification("Please enter full 6-digit OTP", "warning");
+            if ((isWalletPayment && otpCode.length === 0) || (!isWalletPayment && otpCode.length < 6)) {
+                showNotification(isWalletPayment ? '@Html.Raw(Lang.payment_wallet_pin_instruction.Replace("'", "\\'"))' : "Please enter full 6-digit OTP", "warning");
                 return;
             }
 
+            isFinalizingPurchase = true;
             const originalText = $(btn).html();
             $(btn).prop('disabled', true).html('<i class="fa-solid fa-spinner fa-spin mr-2"></i> ...');
 
+            if (isWalletPayment) {
+                submitFinalPurchase(btn, originalText, true, otpCode);
+                return;
+            }
+
+            // Confirm the OTP first. The final purchase API is called only after
+            // this request succeeds for the current prepared transaction.
+            $.ajax({
+                url: subDomain + '@Url.Action("ConfirmOTP", "Home")',
+                type: 'POST',
+                contentType: 'application/json',
+                data: JSON.stringify({
+                    otp: otpCode,
+                    transIdByTicket: currentTransId
+                }),
+                success: function(otpResult) {
+                    if (otpResult.responseCode === "0" || otpResult.responseCode === "0000") {
+                        submitFinalPurchase(btn, originalText, false, null);
+                        return;
+                    }
+
+                    $(btn).prop('disabled', false).html(originalText);
+                    isFinalizingPurchase = false;
+                    $("#otpError").text(otpResult.responseMessage || "Invalid OTP").removeClass("hidden");
+                    $("#otpInputs input").val("");
+                    $("#otp1").focus();
+                },
+                error: function() {
+                    $(btn).prop('disabled', false).html(originalText);
+                    isFinalizingPurchase = false;
+                    showNotification("Network error occurred during OTP verification.", "warning");
+                }
+            });
+        }
+
+        function submitFinalPurchase(btn, originalText, isWalletPayment, walletPin) {
             const finalData = {
                 transIdByTicket: currentTransId,
-                paymentCode: otpCode
+                gameId: isFreeTicketMode ? "@Constants.Millions_CODE" : "@Model.termType",
+                paymentCode: null,
+                walletPin: isWalletPayment ? walletPin : null,
+                channelPayment: currentChannelPayment
             };
 
-            // Call the unified backend action that verifies OTP and then buys the ticket
+            // This final API is unreachable until ConfirmOTP succeeds (or a wallet PIN is supplied).
             $.ajax({
                 url: subDomain + '@Url.Action("ConfirmBuyingTicketV2", "Home")',
                 type: 'POST',
@@ -1356,14 +1585,21 @@ else
                              showNotification(res.responseMessage || "System is upgrading", res.responseCode);
                         } else {
                              $("#otpError").text(res.responseMessage || "Invalid OTP").removeClass("hidden");
-                             $("#otpInputs input").val("");
-                             $("#otp1").focus();
+                             if (isWalletPayment) {
+                                 $("#walletPinInput").val("").focus();
+                             } else {
+                                 $("#otpInputs input").val("");
+                                 $("#otp1").focus();
+                             }
                         }
                     }
                 },
                 error: function() {
                     $(btn).prop('disabled', false).html(originalText);
                     showNotification("Network error occurred during payment completion.", "warning");
+                },
+                complete: function() {
+                    isFinalizingPurchase = false;
                 }
             });
         }

+ 3 - 1
website/Areas/Millions/Views/Home/GameHome.cshtml

@@ -177,7 +177,7 @@
         </div>
 
         <!-- Play now button + decorative balls -->
-        <div class="relative shrink-0 mt-4 z-10">
+        <div id="playNowSection" class="relative shrink-0 mt-4 z-10">
             <img src="/Millions/assets/images/play-decor.png" alt="" class="block w-full h-auto pointer-events-none"/>
 
             <a onclick="checkTerm('@Constants.Millions_CODE')" class="play-now-outer rounded-full animate-pulse-play" style="position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:155px; height:155px; padding:11px; cursor:pointer;">
@@ -218,6 +218,8 @@
 
 </div>
 
+@await Html.PartialAsync("~/Areas/Millions/Views/Home/PopupFreeTickit.cshtml")
+
 @section Scripts {
     <script src="/Millions/js/all.min.js"></script>
     <script>

+ 21 - 10
website/Areas/Millions/Views/Home/History.cshtml

@@ -168,7 +168,7 @@
             if (nextBtn) nextBtn.disabled = currentSeqPage >= totalPages;
         }
 
-        function showTicketDetail(billCode, money, moneyWin, date, channel, payMethod, ticketCode, gameId, drawTime, ticketId) {
+        function showTicketDetail(billCode, money, moneyWin, date, channel, payMethod, paymentMessage, ticketCode, gameId, drawTime, ticketId) {
             document.getElementById('detailBillCode').innerText = '#' + (billCode || '-');
             document.getElementById('detailTicketCode').innerText = '#' + (ticketId || '-');
             document.getElementById('detailMoney').innerText = formatMoneyV2(money || '0') + ' HTG';
@@ -178,24 +178,35 @@
             document.getElementById('detailChannel').innerText = channel || 'App';
             
             // Logic for Game Name and Color
-            let gameName = "@Lang.millions_classic_pick_10";
-            let gameColor = "#0062FF"; // Red
+            const normalizedGameId = String(gameId || '').trim();
+            let gameName = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.millions));
+            let gameColor = "#0062FF";
             
-            if (gameId === "31") {
+            if (normalizedGameId === "30") {
+                gameName = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.millions_classic_pick_10));
+            } else if (normalizedGameId === "31") {
                 gameName = "@Lang.millions_big_small";
-                gameColor = "#00A3FF"; // Blue
-            } else if (gameId === "32") {
+                gameColor = "#00A3FF";
+            } else if (normalizedGameId === "32") {
                 gameName = "@Lang.millions_odd_even";
-                gameColor = "#9333EA"; // Purple
+                gameColor = "#9333EA";
+            } else if (normalizedGameId === "33") {
+                gameName = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.millions));
             }
             
             const gameEl = document.getElementById('detailGameName');
             gameEl.innerText = gameName;
             gameEl.style.color = gameColor;
 
-            let payMethodText = "Main Account";
-            if(payMethod == "1") payMethodText = "Wallet";
-            else if(payMethod == "0") payMethodText = "Main Account";
+            const normalizedPayMethod = String(payMethod || '').trim().toUpperCase();
+            const normalizedPaymentMessage = String(paymentMessage || '').trim().toUpperCase();
+            const isWalletPayment = normalizedPaymentMessage.includes('WALLET')
+                || normalizedPaymentMessage.includes('NATCASH')
+                || normalizedPayMethod === '1'
+                || normalizedPayMethod === 'WALLET';
+            const payMethodText = isWalletPayment
+                ? @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.payment_wallet))
+                : @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.payment_main_account));
 
             document.getElementById('detailPaymentMethod').innerText = payMethodText;
 

+ 370 - 0
website/Areas/Millions/Views/Home/PopupFreeTickit.cshtml

@@ -0,0 +1,370 @@
+@using LotteryWebApp.Languages
+@using LotteryWebApp.Extensions
+@using Microsoft.AspNetCore.Http
+@using System.Globalization
+@{
+	var shouldAutoCheckFreeTicket = Context.Session.GetString("Millions.FreeTicket.AutoChecked") != "1";
+	var popupResourceCulture = CultureInfo.CurrentUICulture;
+	string PopupText(string key) => Lang.ResourceManager.GetString(key, popupResourceCulture) ?? key;
+}
+
+<link rel="stylesheet" href="~/Millions/css/popup-free-ticket.css" asp-append-version="true" />
+
+<aside class="free-ticket-popup" aria-labelledby="freeTicketTitle">
+	<a class="free-ticket-popup__card"
+	   href="#"
+	   id="freeTicketClaimButton"
+	   aria-label="Get your free ticket">
+		<img class="free-ticket-popup__image"
+			 src="~/Millions/assets/images/banner_free_transparent.png"
+			 alt="" aria-hidden="true" />
+		<strong class="free-ticket-popup__ticket-text">
+			@PopupText("millions_free")<br />@PopupText("millions_ticket")
+		</strong>
+		<strong class="free-ticket-popup__button-text" id="freeTicketTitle">
+			<span>@PopupText("free_ticket_banner_cta")</span>
+			<span class="free-ticket-popup__button-highlight">@PopupText("free_ticket_banner_highlight")</span>
+		</strong>
+	</a>
+</aside>
+
+<div class="free-ticket-result" id="freeTicketResultModal" role="dialog" aria-modal="true"
+	 aria-labelledby="freeTicketResultTitle" aria-describedby="freeTicketResultDescription" hidden>
+	<div class="free-ticket-result__backdrop" data-free-ticket-close></div>
+	<section class="free-ticket-result__panel">
+		<h2 class="free-ticket-visually-hidden" id="freeTicketResultTitle">@PopupText("free_ticket_waiting_title")</h2>
+		<p class="free-ticket-visually-hidden" id="freeTicketResultDescription">@PopupText("free_ticket_waiting_description")</p>
+		<img class="free-ticket-result__image"
+			 src="~/Millions/assets/images/free-ticket-success-no-text.png"
+			 alt="" aria-hidden="true" />
+		<strong class="free-ticket-result__copy free-ticket-result__copy--badge">@PopupText("free_ticket_success_badge")</strong>
+		<h2 class="free-ticket-result__copy free-ticket-result__copy--title">@PopupText("free_ticket_success_title")</h2>
+		<p class="free-ticket-result__copy free-ticket-result__copy--received">@PopupText("free_ticket_success_received")</p>
+		<p class="free-ticket-result__copy free-ticket-result__copy--ticket">@PopupText("free_ticket_success_ticket")</p>
+		<p class="free-ticket-result__copy free-ticket-result__copy--chance">@PopupText("free_ticket_success_chance")</p>
+		<strong class="free-ticket-result__copy free-ticket-result__copy--prize">30,000,000 HTG!</strong>
+		<span class="free-ticket-result__copy free-ticket-result__copy--use">@PopupText("free_ticket_success_use")</span>
+		<span class="free-ticket-result__copy free-ticket-result__copy--later">@PopupText("free_ticket_success_later")</span>
+		<p class="free-ticket-result__copy free-ticket-result__copy--validity">@PopupText("free_ticket_success_validity")</p>
+		<button type="button" class="free-ticket-result__action free-ticket-result__action--dismiss-icon"
+				data-free-ticket-close aria-label="@PopupText("millions_close")"></button>
+		<a class="free-ticket-result__action free-ticket-result__action--use"
+		   href="@Url.Action("BuyTicket", "Home", new { area = "Millions", freeTicketMode = "1" })"
+		   aria-label="@PopupText("free_ticket_buy_now")"></a>
+		<button type="button" class="free-ticket-result__action free-ticket-result__action--later"
+				data-free-ticket-close aria-label="@PopupText("millions_close")"></button>
+	</section>
+</div>
+
+<div class="free-ticket-error-43" id="freeTicketError43Modal" role="dialog" aria-modal="true"
+	 aria-labelledby="freeTicketError43Title" aria-describedby="freeTicketError43Description" hidden>
+	<div class="free-ticket-error-43__backdrop" data-free-ticket-error-43-close></div>
+	<section class="free-ticket-error-43__panel">
+		<h2 class="free-ticket-visually-hidden" id="freeTicketError43Title">@PopupText("free_ticket_error_43_title")</h2>
+		<p class="free-ticket-visually-hidden" id="freeTicketError43Description">
+			@PopupText("free_ticket_error_43_description")
+		</p>
+		<img class="free-ticket-error-43__image"
+			 src="~/Millions/assets/images/free-ticket-error-43-no-text.png"
+			 alt="" aria-hidden="true" />
+		<strong class="free-ticket-error-43__copy free-ticket-error-43__copy--badge">@PopupText("free_ticket_error_43_badge")</strong>
+		<h2 class="free-ticket-error-43__copy free-ticket-error-43__copy--title">@PopupText("free_ticket_error_43_title")</h2>
+		<p class="free-ticket-error-43__copy free-ticket-error-43__copy--description">@PopupText("free_ticket_error_43_description")</p>
+		<strong class="free-ticket-error-43__copy free-ticket-error-43__copy--daily-title">@PopupText("free_ticket_error_43_daily_title")</strong>
+		<p class="free-ticket-error-43__copy free-ticket-error-43__copy--daily-description">@PopupText("free_ticket_error_43_daily_description")</p>
+		<span class="free-ticket-error-43__copy free-ticket-error-43__copy--buy">@PopupText("millions_buy_ticket_label")</span>
+		<span class="free-ticket-error-43__copy free-ticket-error-43__copy--close">@PopupText("millions_close")</span>
+		<p class="free-ticket-error-43__copy free-ticket-error-43__copy--footer">@PopupText("free_ticket_error_43_footer")</p>
+		<button type="button" class="free-ticket-error-43__action free-ticket-error-43__action--dismiss-icon"
+				data-free-ticket-error-43-close aria-label="@PopupText("millions_close")"></button>
+		<a class="free-ticket-error-43__action free-ticket-error-43__action--buy"
+		   href="@Url.Action("BuyTicket", "Home", new { area = "Millions" })"
+		   aria-label="@PopupText("millions_buy_ticket_label")"></a>
+		<button type="button" class="free-ticket-error-43__action free-ticket-error-43__action--close"
+				data-free-ticket-error-43-close aria-label="@PopupText("millions_close")"></button>
+	</section>
+</div>
+
+<div class="free-ticket-error-44" id="freeTicketError44Modal" role="dialog" aria-modal="true"
+	 aria-labelledby="freeTicketError44Title" aria-describedby="freeTicketError44Description" hidden>
+	<div class="free-ticket-error-44__backdrop" data-free-ticket-error-44-close></div>
+	<section class="free-ticket-error-44__panel">
+		<h2 class="free-ticket-visually-hidden" id="freeTicketError44Title">@PopupText("free_ticket_waiting_title")</h2>
+		<p class="free-ticket-visually-hidden" id="freeTicketError44Description">
+			@PopupText("free_ticket_waiting_description")
+		</p>
+		<img class="free-ticket-error-44__image"
+			 src="~/Millions/assets/images/free-ticket-error-44-no-text.png"
+			 alt="" aria-hidden="true" />
+		<strong class="free-ticket-error-44__copy free-ticket-error-44__copy--badge">@PopupText("free_ticket_waiting_badge")</strong>
+		<h2 class="free-ticket-error-44__copy free-ticket-error-44__copy--title">@PopupText("free_ticket_waiting_title")</h2>
+		<p class="free-ticket-error-44__copy free-ticket-error-44__copy--description">@PopupText("free_ticket_waiting_description")</p>
+		<p class="free-ticket-error-44__copy free-ticket-error-44__copy--chance">@PopupText("free_ticket_success_chance")</p>
+		<strong class="free-ticket-error-44__copy free-ticket-error-44__copy--prize">30,000,000 HTG!</strong>
+		<span class="free-ticket-error-44__copy free-ticket-error-44__copy--buy">@PopupText("free_ticket_buy_now")</span>
+		<span class="free-ticket-error-44__copy free-ticket-error-44__copy--close">@PopupText("millions_close")</span>
+		<p class="free-ticket-error-44__copy free-ticket-error-44__copy--footer">@PopupText("free_ticket_waiting_validity")</p>
+		<button type="button" class="free-ticket-error-44__action free-ticket-error-44__action--dismiss-icon"
+				data-free-ticket-error-44-close aria-label="@PopupText("millions_close")"></button>
+		<a class="free-ticket-error-44__action free-ticket-error-44__action--buy"
+		   href="@Url.Action("BuyTicket", "Home", new { area = "Millions", freeTicketMode = "1" })"
+		   aria-label="@PopupText("free_ticket_buy_now")"></a>
+		<button type="button" class="free-ticket-error-44__action free-ticket-error-44__action--close"
+				data-free-ticket-error-44-close aria-label="@PopupText("millions_close")"></button>
+	</section>
+</div>
+
+<div class="free-ticket-error-45" id="freeTicketError45Modal" role="dialog" aria-modal="true"
+	 aria-label="Free Tickets Are Sold Out" hidden>
+	<div class="free-ticket-error-45__backdrop" data-free-ticket-error-45-close></div>
+	<section class="free-ticket-error-45__panel">
+		<img class="free-ticket-error-45__image"
+			 src="~/Millions/assets/images/free-ticket-error-45-no-text.png"
+			 alt="" aria-hidden="true" />
+		<strong class="free-ticket-error-45__copy free-ticket-error-45__copy--ticket-free">@PopupText("free_ticket_error_45_ticket_free")</strong>
+		<strong class="free-ticket-error-45__copy free-ticket-error-45__copy--ticket-tickets">@PopupText("free_ticket_error_45_ticket_tickets")</strong>
+		<strong class="free-ticket-error-45__copy free-ticket-error-45__copy--ticket-sold-out">@PopupText("free_ticket_error_45_ticket_sold_out")</strong>
+		<h2 class="free-ticket-error-45__copy free-ticket-error-45__copy--title">
+			@PopupText("free_ticket_error_45_title") <span>@PopupText("free_ticket_error_45_title_emphasis")</span>
+		</h2>
+		<p class="free-ticket-error-45__copy free-ticket-error-45__copy--description">@PopupText("free_ticket_error_45_description")</p>
+		<strong class="free-ticket-error-45__copy free-ticket-error-45__copy--next-title">@PopupText("free_ticket_error_45_next_title")</strong>
+		<p class="free-ticket-error-45__copy free-ticket-error-45__copy--next-primary">@PopupText("free_ticket_error_45_next_primary")</p>
+		<p class="free-ticket-error-45__copy free-ticket-error-45__copy--next-secondary">@PopupText("free_ticket_error_45_next_secondary")</p>
+		<span class="free-ticket-error-45__copy free-ticket-error-45__copy--buy">@PopupText("millions_buy_ticket_label")</span>
+		<span class="free-ticket-error-45__copy free-ticket-error-45__copy--close">@PopupText("millions_close")</span>
+		<p class="free-ticket-error-45__copy free-ticket-error-45__copy--footer">@PopupText("free_ticket_error_45_footer")</p>
+		<button type="button" class="free-ticket-error-45__action free-ticket-error-45__action--dismiss-icon"
+				data-free-ticket-error-45-close aria-label="@PopupText("millions_close")"></button>
+		<a class="free-ticket-error-45__action free-ticket-error-45__action--buy"
+		   href="@Url.Action("BuyTicket", "Home", new { area = "Millions" })"
+		   aria-label="@PopupText("millions_buy_ticket_label")"></a>
+		<button type="button" class="free-ticket-error-45__action free-ticket-error-45__action--close"
+				data-free-ticket-error-45-close aria-label="@PopupText("millions_close")"></button>
+	</section>
+</div>
+
+<script>
+	(function () {
+		var popup = document.querySelector('.free-ticket-popup');
+		var claimButton = document.getElementById('freeTicketClaimButton');
+		var resultModal = document.getElementById('freeTicketResultModal');
+		var error43Modal = document.getElementById('freeTicketError43Modal');
+		var error44Modal = document.getElementById('freeTicketError44Modal');
+		var error45Modal = document.getElementById('freeTicketError45Modal');
+		var error43ReturnFocus = null;
+		var error44ReturnFocus = null;
+		var error45ReturnFocus = null;
+		if (!popup) return;
+
+		function showFreeTicketResult(result) {
+			if (!resultModal || !result || String(result.responseCode) !== '0' || !result.code) {
+				return false;
+			}
+
+			resultModal.hidden = false;
+			document.body.classList.add('free-ticket-result-open');
+			return true;
+		}
+
+		function showFreeTicketError43() {
+			if (!error43Modal) return false;
+
+			error43ReturnFocus = document.activeElement;
+			error43Modal.hidden = false;
+			document.body.classList.add('free-ticket-result-open');
+			var closeButton = error43Modal.querySelector('[data-free-ticket-error-43-close]');
+			if (closeButton) closeButton.focus();
+			return true;
+		}
+
+		function closeFreeTicketError43() {
+			if (!error43Modal) return;
+
+			error43Modal.hidden = true;
+			document.body.classList.remove('free-ticket-result-open');
+			if (error43ReturnFocus && typeof error43ReturnFocus.focus === 'function') {
+				error43ReturnFocus.focus();
+			}
+		}
+
+		if (error43Modal) {
+			error43Modal.querySelectorAll('[data-free-ticket-error-43-close]').forEach(function (closeButton) {
+				closeButton.addEventListener('click', closeFreeTicketError43);
+			});
+
+			error43Modal.addEventListener('keydown', function (event) {
+				if (event.key === 'Escape') closeFreeTicketError43();
+			});
+		}
+
+		function showFreeTicketError44(result) {
+			if (!error44Modal) return false;
+
+			error44ReturnFocus = document.activeElement;
+			error44Modal.hidden = false;
+			document.body.classList.add('free-ticket-result-open');
+			var closeButton = error44Modal.querySelector('[data-free-ticket-error-44-close]');
+			if (closeButton) closeButton.focus();
+			return true;
+		}
+
+		function closeFreeTicketError44() {
+			if (!error44Modal) return;
+
+			error44Modal.hidden = true;
+			document.body.classList.remove('free-ticket-result-open');
+			if (error44ReturnFocus && typeof error44ReturnFocus.focus === 'function') {
+				error44ReturnFocus.focus();
+			}
+		}
+
+		if (error44Modal) {
+			error44Modal.querySelectorAll('[data-free-ticket-error-44-close]').forEach(function (closeButton) {
+				closeButton.addEventListener('click', closeFreeTicketError44);
+			});
+
+			error44Modal.addEventListener('keydown', function (event) {
+				if (event.key === 'Escape') closeFreeTicketError44();
+			});
+		}
+
+		function showFreeTicketError45() {
+			if (!error45Modal) return false;
+
+			error45ReturnFocus = document.activeElement;
+			error45Modal.hidden = false;
+			document.body.classList.add('free-ticket-result-open');
+			var closeButton = error45Modal.querySelector('[data-free-ticket-error-45-close]');
+			if (closeButton) closeButton.focus();
+			return true;
+		}
+
+		function closeFreeTicketError45() {
+			if (!error45Modal) return;
+
+			error45Modal.hidden = true;
+			document.body.classList.remove('free-ticket-result-open');
+			if (error45ReturnFocus && typeof error45ReturnFocus.focus === 'function') {
+				error45ReturnFocus.focus();
+			}
+		}
+
+		if (error45Modal) {
+			error45Modal.querySelectorAll('[data-free-ticket-error-45-close]').forEach(function (closeButton) {
+				closeButton.addEventListener('click', closeFreeTicketError45);
+			});
+
+			error45Modal.addEventListener('keydown', function (event) {
+				if (event.key === 'Escape') closeFreeTicketError45();
+			});
+		}
+
+		if (resultModal) {
+			resultModal.querySelectorAll('[data-free-ticket-close]').forEach(function (closeButton) {
+				closeButton.addEventListener('click', function () {
+					resultModal.hidden = true;
+					document.body.classList.remove('free-ticket-result-open');
+				});
+			});
+		}
+
+		if (claimButton) {
+			var freeTicketMessages = {
+				'38': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_38)),
+				'39': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_39)),
+				'40': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_40)),
+				'41': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_41)),
+				'42': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_42)),
+				'43': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_43)),
+				'44': @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Lang.get_free_ticket_code_44))
+			};
+
+			if (@(shouldAutoCheckFreeTicket ? "true" : "false") && document.getElementById('playNowSection')) {
+				document.addEventListener('DOMContentLoaded', function () {
+					var initialVerificationToken = document.querySelector('input[name="__RequestVerificationToken"]');
+					$.ajax({
+						url: subDomain + '@Url.Action("GetFreeTicket", "Home", new { area = "Millions" })',
+						type: 'POST',
+						data: {
+							__RequestVerificationToken: initialVerificationToken ? initialVerificationToken.value : '',
+							type: '1'
+						},
+						success: function (result) {
+							if (result && String(result.responseCode) === '0' && result.code) {
+								showFreeTicketError44(result);
+							}
+						}
+					});
+				});
+			}
+
+			claimButton.addEventListener('click', function (event) {
+				event.preventDefault();
+				if (claimButton.dataset.loading === 'true') return;
+
+				var verificationToken = document.querySelector('input[name="__RequestVerificationToken"]');
+				claimButton.dataset.loading = 'true';
+				claimButton.classList.add('free-ticket-popup__card--loading');
+
+				$.ajax({
+					url: subDomain + '@Url.Action("GetFreeTicket", "Home", new { area = "Millions" })',
+					type: 'POST',
+					data: {
+						__RequestVerificationToken: verificationToken ? verificationToken.value : ''
+					},
+					success: function (result) {
+						if (showFreeTicketResult(result)) {
+							return;
+						}
+
+						var responseCode = result && result.responseCode
+							? String(result.responseCode)
+							: '';
+						if (responseCode === '43' && showFreeTicketError43()) {
+							return;
+						}
+						if (responseCode === '44' && showFreeTicketError44(result)) {
+							return;
+						}
+						if (responseCode === '45' && showFreeTicketError45()) {
+							return;
+						}
+						var responseMessage = freeTicketMessages[responseCode]
+							|| (result && result.responseMessage)
+							|| 'Unable to get free ticket.';
+
+						window.alert(responseMessage);
+					},
+					error: function () {
+						window.alert('Unable to get free ticket.');
+					},
+					complete: function () {
+						claimButton.dataset.loading = 'false';
+						claimButton.classList.remove('free-ticket-popup__card--loading');
+					}
+				});
+			});
+		}
+
+		function alignFreeTicketIcon() {
+			var millionsFrame = document.querySelector('.main-content, .main-container');
+			if (!millionsFrame) {
+				popup.style.left = '';
+				popup.style.right = '12px';
+				return;
+			}
+
+			var frameRect = millionsFrame.getBoundingClientRect();
+			var popupWidth = popup.getBoundingClientRect().width;
+			popup.style.left = Math.max(8, frameRect.right - popupWidth - 15) + 'px';
+			popup.style.right = 'auto';
+			popup.style.top = '';
+			popup.style.bottom = '';
+		}
+
+		alignFreeTicketIcon();
+		window.addEventListener('resize', alignFreeTicketIcon, { passive: true });
+		window.addEventListener('scroll', alignFreeTicketIcon, { passive: true, capture: true });
+	})();
+</script>

+ 1 - 1
website/Areas/Millions/Views/Home/_TermUserTicketHistory.cshtml

@@ -99,7 +99,7 @@
                         <i class="fa-solid fa-circle-info text-[12px]"></i>
                         <span class="text-[10px] font-bold uppercase tracking-tight">@Lang.millions_tap_to_see_detail</span>
                     </div>
-                    <button onclick="showTicketDetail('@item.billCode', '@item.money', '@item.moneyWin', '@item.createDate', '@item.channel', '@item.channelPayment', '@item.code', '@item.gameId', '@item.termRandomDate', '@item.id')" 
+                    <button onclick="showTicketDetail('@item.billCode', '@item.money', '@item.moneyWin', '@item.createDate', '@item.channel', '@item.channelPayment', '@item.paymentMessage', '@item.code', '@item.gameId', '@item.termRandomDate', '@item.id')"
                             class="bg-[#00A3FF] text-white px-5 py-1.5 rounded-lg font-black text-[12px] shadow-[0_4px_10px_rgba(0,163,255,0.3)] active:scale-95 transition-all border-[1.5px] border-white uppercase tracking-wide">
                         @Lang.millions_detail
                     </button>

+ 1 - 0
website/Common/Constants.cs

@@ -106,6 +106,7 @@ namespace LotteryWebApp.Common
 
         public const string BASIC_WALLET_TICKET = "0";
         public const string NATCASH_WALLET_TICKET = "1";
+        public const string WALLET_PAYMENT_CHANNEL = "WALLET";
 
         public const string BASIC_WALLET_TRANSFER = "1";
         public const string NATCASH_WALLET_TRANSFER = "2";

+ 11 - 0
website/Controllers/HomeController.cs

@@ -145,12 +145,23 @@ namespace LotteryWebApp.Controllers
                 // check auto login ?uuid=681471d23b83f0a463e2dd5671ea0fc5
                 // ?uuid=681471d23b83f0a463e2dd5671ea0fc5&mcuid=6c6948a89908d83aec8c05be256970a1&mcapp=mocha
 
+<<<<<<< HEAD
                 if (
                     !CheckAuthToken()
                     || token != null
                     || uuid != null
                     || phonenumber != null
                 )
+=======
+                if (CheckAuthToken())
+                {
+                    return Redirect(
+                        GetParameter(Constants.SUB_DOMAIN) + "/Account/ChooseApp"
+                    );
+                }
+
+                if (!CheckAuthToken())
+>>>>>>> websiteNoNatCash
                 {
                     //String uri = HttpContext.Request.QueryString.ToUriComponent().ToString();
                     log.Info("UUID: " + uuid);

+ 366 - 0
website/Languages/Lang.Designer.cs

@@ -1215,6 +1215,237 @@ namespace LotteryWebApp.Languages {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Achte kounye a.
+        /// </summary>
+        public static string free_ticket_buy_now {
+            get {
+                return ResourceManager.GetString("free_ticket_buy_now", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to PLIS CHANS PWOCHEN FWA!.
+        /// </summary>
+        public static string free_ticket_error_43_badge {
+            get {
+                return ResourceManager.GetString("free_ticket_error_43_badge", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Yo bay tikè gratis chak jou. Pa rate chans ou demen!.
+        /// </summary>
+        public static string free_ticket_error_43_daily_description {
+            get {
+                return ResourceManager.GetString("free_ticket_error_43_daily_description", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Nouvo chans chak jou!.
+        /// </summary>
+        public static string free_ticket_error_43_daily_title {
+            get {
+                return ResourceManager.GetString("free_ticket_error_43_daily_title", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Ou pa t resevwa yon tikè gratis jodi a. Retounen demen epi eseye chans ou ankò..
+        /// </summary>
+        public static string free_ticket_error_43_description {
+            get {
+                return ResourceManager.GetString("free_ticket_error_43_description", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Achte yon tikè kounye a pou chans genyen jiska 30,000,000 HTG!.
+        /// </summary>
+        public static string free_ticket_error_43_footer {
+            get {
+                return ResourceManager.GetString("free_ticket_error_43_footer", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Bòn chans pwochen fwa!.
+        /// </summary>
+        public static string free_ticket_error_43_title {
+            get {
+                return ResourceManager.GetString("free_ticket_error_43_title", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_description {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_description", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_footer {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_footer", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_next_primary {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_next_primary", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_next_secondary {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_next_secondary", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_next_title {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_next_title", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_ticket_free {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_ticket_free", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_ticket_sold_out {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_ticket_sold_out", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_ticket_tickets {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_ticket_tickets", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_title {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_title", resourceCulture);
+            }
+        }
+
+        public static string free_ticket_error_45_title_emphasis {
+            get {
+                return ResourceManager.GetString("free_ticket_error_45_title_emphasis", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to TIKÈ GRATIS.
+        /// </summary>
+        public static string free_ticket_success_badge {
+            get {
+                return ResourceManager.GetString("free_ticket_success_badge", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Sèvi avè l kounye a pou chans genyen jiska.
+        /// </summary>
+        public static string free_ticket_success_chance {
+            get {
+                return ResourceManager.GetString("free_ticket_success_chance", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Pita.
+        /// </summary>
+        public static string free_ticket_success_later {
+            get {
+                return ResourceManager.GetString("free_ticket_success_later", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Ou resevwa.
+        /// </summary>
+        public static string free_ticket_success_received {
+            get {
+                return ResourceManager.GetString("free_ticket_success_received", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to 1 Tikè Millions GRATIS..
+        /// </summary>
+        public static string free_ticket_success_ticket {
+            get {
+                return ResourceManager.GetString("free_ticket_success_ticket", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Felisitasyon!.
+        /// </summary>
+        public static string free_ticket_success_title {
+            get {
+                return ResourceManager.GetString("free_ticket_success_title", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Sèvi ak tikè a.
+        /// </summary>
+        public static string free_ticket_success_use {
+            get {
+                return ResourceManager.GetString("free_ticket_success_use", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Tikè gratis la valab sèlman pou tiraj jodi a..
+        /// </summary>
+        public static string free_ticket_success_validity {
+            get {
+                return ResourceManager.GetString("free_ticket_success_validity", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to TIKÈ GRATIS.
+        /// </summary>
+        public static string free_ticket_waiting_badge {
+            get {
+                return ResourceManager.GetString("free_ticket_waiting_badge", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Ou gen 1 tikè Millions gratis ki poko itilize. Sèvi avè l pou tiraj jodi a..
+        /// </summary>
+        public static string free_ticket_waiting_description {
+            get {
+                return ResourceManager.GetString("free_ticket_waiting_description", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Tikè GRATIS ou ap tann ou!.
+        /// </summary>
+        public static string free_ticket_waiting_title {
+            get {
+                return ResourceManager.GetString("free_ticket_waiting_title", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Tikè gratis la valab sèlman pou tiraj jodi a. Pa bliye sèvi avè l!.
+        /// </summary>
+        public static string free_ticket_waiting_validity {
+            get {
+                return ResourceManager.GetString("free_ticket_waiting_validity", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Soti nan.
         /// </summary>
@@ -1242,6 +1473,69 @@ namespace LotteryWebApp.Languages {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Kòd pwomosyon an pa valid.
+        /// </summary>
+        public static string get_free_ticket_code_38 {
+            get {
+                return ResourceManager.GetString("get_free_ticket_code_38", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Kòd pwomosyon an deja itilize.
+        /// </summary>
+        public static string get_free_ticket_code_39 {
+            get {
+                return ResourceManager.GetString("get_free_ticket_code_39", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Pwomosyon an pa aktif.
+        /// </summary>
+        public static string get_free_ticket_code_40 {
+            get {
+                return ResourceManager.GetString("get_free_ticket_code_40", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Demann tikè a pa egziste.
+        /// </summary>
+        public static string get_free_ticket_code_41 {
+            get {
+                return ResourceManager.GetString("get_free_ticket_code_41", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Tranzaksyon ou an pa reyisi.
+        /// </summary>
+        public static string get_free_ticket_code_42 {
+            get {
+                return ResourceManager.GetString("get_free_ticket_code_42", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Bòn chans pwochen fwa.
+        /// </summary>
+        public static string get_free_ticket_code_43 {
+            get {
+                return ResourceManager.GetString("get_free_ticket_code_43", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Yo voye kòd la.
+        /// </summary>
+        public static string get_free_ticket_code_44 {
+            get {
+                return ResourceManager.GetString("get_free_ticket_code_44", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Jwenn OTP.
         /// </summary>
@@ -3456,6 +3750,42 @@ namespace LotteryWebApp.Languages {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Kont prensipal.
+        /// </summary>
+        public static string payment_main_account {
+            get {
+                return ResourceManager.GetString("payment_main_account", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Peye avèk balans mobil ou.
+        /// </summary>
+        public static string payment_main_account_desc {
+            get {
+                return ResourceManager.GetString("payment_main_account_desc", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Chwazi yon kont pou fini acha ou.
+        /// </summary>
+        public static string payment_method_subtitle {
+            get {
+                return ResourceManager.GetString("payment_method_subtitle", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Chwazi metòd peman.
+        /// </summary>
+        public static string payment_method_title {
+            get {
+                return ResourceManager.GetString("payment_method_title", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Metod peyman.
         /// </summary>
@@ -3465,6 +3795,42 @@ namespace LotteryWebApp.Languages {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Enfòmasyon peman ou pwoteje.
+        /// </summary>
+        public static string payment_security_message {
+            get {
+                return ResourceManager.GetString("payment_security_message", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to WALLET.
+        /// </summary>
+        public static string payment_wallet {
+            get {
+                return ResourceManager.GetString("payment_wallet", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Peman sekirize avèk PIN wallet la.
+        /// </summary>
+        public static string payment_wallet_desc {
+            get {
+                return ResourceManager.GetString("payment_wallet_desc", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to Antre PIN WALLET ou.
+        /// </summary>
+        public static string payment_wallet_pin_instruction {
+            get {
+                return ResourceManager.GetString("payment_wallet_pin_instruction", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Nimewo telefon nan pa bon.
         /// </summary>

+ 115 - 1
website/Languages/Lang.fr.resx

@@ -2035,4 +2035,118 @@ We’ll help you create an account in a few easy steps.</value>
   <data name="millions_jackpot_rule7_desc3" xml:space="preserve">
     <value>In case of any dispute, Natcom's decision shall be final.</value>
   </data>
-</root>
+  <data name="payment_method_title" xml:space="preserve">
+    <value>Choose payment method</value>
+  </data>
+  <data name="payment_method_subtitle" xml:space="preserve">
+    <value>Select an account to complete your purchase</value>
+  </data>
+  <data name="payment_main_account" xml:space="preserve">
+    <value>Main account</value>
+  </data>
+  <data name="payment_main_account_desc" xml:space="preserve">
+    <value>Pay with your mobile balance</value>
+  </data>
+  <data name="payment_wallet" xml:space="preserve">
+    <value>WALLET</value>
+  </data>
+  <data name="payment_wallet_desc" xml:space="preserve">
+    <value>Secure payment with wallet PIN</value>
+  </data>
+  <data name="payment_security_message" xml:space="preserve">
+    <value>Your payment information is protected</value>
+  </data>
+  <data name="payment_wallet_pin_instruction" xml:space="preserve">
+    <value>Enter your WALLET PIN</value>
+  </data>
+  <data name="get_free_ticket_code_38" xml:space="preserve">
+    <value>Promotion code invalid</value>
+  </data>
+  <data name="get_free_ticket_code_39" xml:space="preserve">
+    <value>Promotion code has already been used</value>
+  </data>
+  <data name="get_free_ticket_code_40" xml:space="preserve">
+    <value>Promotion is not active</value>
+  </data>
+  <data name="get_free_ticket_code_41" xml:space="preserve">
+    <value>Ticket request does not exist</value>
+  </data>
+  <data name="get_free_ticket_code_42" xml:space="preserve">
+    <value>Your transaction was unsuccessful</value>
+  </data>
+  <data name="get_free_ticket_code_43" xml:space="preserve">
+    <value>Good luck next time</value>
+  </data>
+  <data name="get_free_ticket_code_44" xml:space="preserve">
+    <value>The code has been sent</value>
+  </data>
+  <data name="free_ticket_error_43_title" xml:space="preserve">
+    <value>Good luck next time!</value>
+  </data>
+  <data name="free_ticket_error_43_description" xml:space="preserve">
+    <value>You did not receive a free ticket today. Come back tomorrow and try your luck again.</value>
+  </data>
+  <data name="free_ticket_waiting_title" xml:space="preserve">
+    <value>Your FREE ticket is waiting!</value>
+  </data>
+  <data name="free_ticket_waiting_description" xml:space="preserve">
+    <value>You have 1 free Millions ticket that has not been used. Use it for today's draw.</value>
+  </data>
+  <data name="free_ticket_buy_now" xml:space="preserve">
+    <value>Buy now</value>
+  </data>
+  <data name="free_ticket_success_badge" xml:space="preserve">
+    <value>FREE TICKET</value>
+  </data>
+  <data name="free_ticket_success_title" xml:space="preserve">
+    <value>Congratulations!</value>
+  </data>
+  <data name="free_ticket_success_received" xml:space="preserve">
+    <value>You have received</value>
+  </data>
+  <data name="free_ticket_success_ticket" xml:space="preserve">
+    <value>1 FREE Millions Ticket.</value>
+  </data>
+  <data name="free_ticket_success_chance" xml:space="preserve">
+    <value>Use it now for your chance to win up to</value>
+  </data>
+  <data name="free_ticket_success_use" xml:space="preserve">
+    <value>Use Ticket Now</value>
+  </data>
+  <data name="free_ticket_success_later" xml:space="preserve">
+    <value>Later</value>
+  </data>
+  <data name="free_ticket_success_validity" xml:space="preserve">
+    <value>The free ticket is valid for today's draw only.</value>
+  </data>
+  <data name="free_ticket_error_43_badge" xml:space="preserve">
+    <value>BETTER LUCK NEXT TIME!</value>
+  </data>
+  <data name="free_ticket_error_43_daily_title" xml:space="preserve">
+    <value>New chances every day!</value>
+  </data>
+  <data name="free_ticket_error_43_daily_description" xml:space="preserve">
+    <value>Free tickets are given out daily. Don't miss your chance tomorrow!</value>
+  </data>
+  <data name="free_ticket_error_43_footer" xml:space="preserve">
+    <value>Buy a ticket now for your chance to win up to 30,000,000 HTG!</value>
+  </data>
+  <data name="free_ticket_waiting_badge" xml:space="preserve">
+    <value>FREE TICKET</value>
+  </data>
+  <data name="free_ticket_waiting_validity" xml:space="preserve">
+    <value>Free ticket is valid for today's draw only. Don't forget to use it!</value>
+  </data>
+  <data name="free_ticket_error_45_ticket_free" xml:space="preserve"><value>FREE</value></data>
+  <data name="free_ticket_error_45_ticket_tickets" xml:space="preserve"><value>TICKETS</value></data>
+  <data name="free_ticket_error_45_ticket_sold_out" xml:space="preserve"><value>SOLD OUT</value></data>
+  <data name="free_ticket_error_45_title" xml:space="preserve"><value>Free Tickets</value></data>
+  <data name="free_ticket_error_45_title_emphasis" xml:space="preserve"><value>Are Sold Out</value></data>
+  <data name="free_ticket_error_45_description" xml:space="preserve"><value>All free tickets for today have been claimed. Please come back tomorrow and try your luck again!</value></data>
+  <data name="free_ticket_error_45_next_title" xml:space="preserve"><value>Next chance:</value></data>
+  <data name="free_ticket_error_45_next_primary" xml:space="preserve"><value>Come back tomorrow</value></data>
+  <data name="free_ticket_error_45_next_secondary" xml:space="preserve"><value>and try your luck again!</value></data>
+  <data name="free_ticket_error_45_footer" xml:space="preserve"><value>Don't miss your chance to win up to 30,000,000 HTG!</value></data>
+  <data name="free_ticket_banner_cta" xml:space="preserve"><value>Tap here to get your</value></data>
+  <data name="free_ticket_banner_highlight" xml:space="preserve"><value>FREE TICKET!</value></data>
+</root>

+ 115 - 1
website/Languages/Lang.resx

@@ -2036,4 +2036,118 @@ Apre yo fin tire 20 nimewo, sistem nan konte konbyen nimewo ki enpe oswa pe: &lt
   <data name="millions_jackpot_rule7_desc3" xml:space="preserve">
     <value>Si gen nenpòt diskisyon, desizyon Natcom se desizyon final la.</value>
   </data>
-</root>
+  <data name="payment_method_title" xml:space="preserve">
+    <value>Chwazi metòd peman</value>
+  </data>
+  <data name="payment_method_subtitle" xml:space="preserve">
+    <value>Chwazi yon kont pou fini acha ou</value>
+  </data>
+  <data name="payment_main_account" xml:space="preserve">
+    <value>Kont prensipal</value>
+  </data>
+  <data name="payment_main_account_desc" xml:space="preserve">
+    <value>Peye avèk balans mobil ou</value>
+  </data>
+  <data name="payment_wallet" xml:space="preserve">
+    <value>WALLET</value>
+  </data>
+  <data name="payment_wallet_desc" xml:space="preserve">
+    <value>Peman sekirize avèk PIN wallet la</value>
+  </data>
+  <data name="payment_security_message" xml:space="preserve">
+    <value>Enfòmasyon peman ou pwoteje</value>
+  </data>
+  <data name="payment_wallet_pin_instruction" xml:space="preserve">
+    <value>Antre PIN WALLET ou</value>
+  </data>
+  <data name="get_free_ticket_code_38" xml:space="preserve">
+    <value>Kòd pwomosyon an pa valid</value>
+  </data>
+  <data name="get_free_ticket_code_39" xml:space="preserve">
+    <value>Kòd pwomosyon an deja itilize</value>
+  </data>
+  <data name="get_free_ticket_code_40" xml:space="preserve">
+    <value>Pwomosyon an pa aktif</value>
+  </data>
+  <data name="get_free_ticket_code_41" xml:space="preserve">
+    <value>Demann tikè a pa egziste</value>
+  </data>
+  <data name="get_free_ticket_code_42" xml:space="preserve">
+    <value>Tranzaksyon ou an pa reyisi</value>
+  </data>
+  <data name="get_free_ticket_code_43" xml:space="preserve">
+    <value>Bòn chans pwochen fwa</value>
+  </data>
+  <data name="get_free_ticket_code_44" xml:space="preserve">
+    <value>Yo voye kòd la</value>
+  </data>
+  <data name="free_ticket_error_43_title" xml:space="preserve">
+    <value>Bòn chans pwochen fwa!</value>
+  </data>
+  <data name="free_ticket_error_43_description" xml:space="preserve">
+    <value>Ou pa t resevwa yon tikè gratis jodi a. Retounen demen epi eseye chans ou ankò.</value>
+  </data>
+  <data name="free_ticket_waiting_title" xml:space="preserve">
+    <value>Tikè GRATIS ou ap tann ou!</value>
+  </data>
+  <data name="free_ticket_waiting_description" xml:space="preserve">
+    <value>Ou gen 1 tikè Millions gratis ki poko itilize. Sèvi avè l pou tiraj jodi a.</value>
+  </data>
+  <data name="free_ticket_buy_now" xml:space="preserve">
+    <value>Achte kounye a</value>
+  </data>
+  <data name="free_ticket_success_badge" xml:space="preserve">
+    <value>TIKÈ GRATIS</value>
+  </data>
+  <data name="free_ticket_success_title" xml:space="preserve">
+    <value>Felisitasyon!</value>
+  </data>
+  <data name="free_ticket_success_received" xml:space="preserve">
+    <value>Ou resevwa</value>
+  </data>
+  <data name="free_ticket_success_ticket" xml:space="preserve">
+    <value>1 Tikè Millions GRATIS.</value>
+  </data>
+  <data name="free_ticket_success_chance" xml:space="preserve">
+    <value>Sèvi avè l kounye a pou chans genyen jiska</value>
+  </data>
+  <data name="free_ticket_success_use" xml:space="preserve">
+    <value>Sèvi ak tikè a</value>
+  </data>
+  <data name="free_ticket_success_later" xml:space="preserve">
+    <value>Pita</value>
+  </data>
+  <data name="free_ticket_success_validity" xml:space="preserve">
+    <value>Tikè gratis la valab sèlman pou tiraj jodi a.</value>
+  </data>
+  <data name="free_ticket_error_43_badge" xml:space="preserve">
+    <value>PLIS CHANS PWOCHEN FWA!</value>
+  </data>
+  <data name="free_ticket_error_43_daily_title" xml:space="preserve">
+    <value>Nouvo chans chak jou!</value>
+  </data>
+  <data name="free_ticket_error_43_daily_description" xml:space="preserve">
+    <value>Yo bay tikè gratis chak jou. Pa rate chans ou demen!</value>
+  </data>
+  <data name="free_ticket_error_43_footer" xml:space="preserve">
+    <value>Achte yon tikè kounye a pou chans genyen jiska 30,000,000 HTG!</value>
+  </data>
+  <data name="free_ticket_waiting_badge" xml:space="preserve">
+    <value>TIKÈ GRATIS</value>
+  </data>
+  <data name="free_ticket_waiting_validity" xml:space="preserve">
+    <value>Tikè gratis la valab sèlman pou tiraj jodi a. Pa bliye sèvi avè l!</value>
+  </data>
+  <data name="free_ticket_error_45_ticket_free" xml:space="preserve"><value>GRATIS</value></data>
+  <data name="free_ticket_error_45_ticket_tickets" xml:space="preserve"><value>TIKÈ</value></data>
+  <data name="free_ticket_error_45_ticket_sold_out" xml:space="preserve"><value>FINI</value></data>
+  <data name="free_ticket_error_45_title" xml:space="preserve"><value>Tikè gratis yo</value></data>
+  <data name="free_ticket_error_45_title_emphasis" xml:space="preserve"><value>fini</value></data>
+  <data name="free_ticket_error_45_description" xml:space="preserve"><value>Tout tikè gratis pou jodi a deja reklame. Tanpri retounen demen epi eseye chans ou ankò!</value></data>
+  <data name="free_ticket_error_45_next_title" xml:space="preserve"><value>Pwochen chans:</value></data>
+  <data name="free_ticket_error_45_next_primary" xml:space="preserve"><value>Retounen demen</value></data>
+  <data name="free_ticket_error_45_next_secondary" xml:space="preserve"><value>epi eseye chans ou ankò!</value></data>
+  <data name="free_ticket_error_45_footer" xml:space="preserve"><value>Pa rate chans ou pou genyen jiska 30,000,000 HTG!</value></data>
+  <data name="free_ticket_banner_cta" xml:space="preserve"><value>Peze la pou jwenn</value></data>
+  <data name="free_ticket_banner_highlight" xml:space="preserve"><value>TIKÈ GRATIS ou!</value></data>
+</root>

+ 56 - 0
website/Service/ApiFunctions.cs

@@ -212,6 +212,30 @@ namespace LotteryWebApp.Service
             return deptObj;
         }
 
+        public ConfirmTicketDataResponse BuyTicketV2Api(IConfiguration configuration, ConfirmTicketDataRequest confirmTicketDataRequest)
+        {
+            confirmTicketDataRequest.serviceId = ApiConstants.SERVICE_ID;
+            confirmTicketDataRequest.channel = configuration.GetSection("channel").Value;
+            confirmTicketDataRequest.channelPayment = null;
+
+            String url = configuration.GetSection("buy_ticketv2").Value;
+            String jsonData = SendPost(confirmTicketDataRequest, ApiConstants.SERVICE_ID, url);
+            ConfirmTicketDataResponse deptObj = JsonSerializer.Deserialize<ConfirmTicketDataResponse>(jsonData);
+            return deptObj;
+        }
+        public ConfirmBuyingTicketResponse ConfirmBuyingTicketV2Api(IConfiguration configuration, ConfirmBuyingTicketRequest confirmBuyingTicketRequest, string channelPayment = null, string walletPin = null)
+        {
+            confirmBuyingTicketRequest.serviceId = ApiConstants.SERVICE_ID;
+            confirmBuyingTicketRequest.channel = configuration.GetSection("channel").Value;
+            confirmBuyingTicketRequest.channelPayment = channelPayment;
+            confirmBuyingTicketRequest.walletPin = walletPin;
+
+            String url = configuration.GetSection("confirm_buying_ticketv2").Value;
+            String jsonData = SendPost(confirmBuyingTicketRequest, ApiConstants.SERVICE_ID, url);
+            ConfirmBuyingTicketResponse deptObj = JsonSerializer.Deserialize<ConfirmBuyingTicketResponse>(jsonData);
+            return deptObj;
+        }
+
         public UserTicketResponse GetUserTicketApi(IConfiguration configuration, UserTicketRequest userTicketRequest)
         {
             userTicketRequest.serviceId = ApiConstants.SERVICE_ID;
@@ -223,6 +247,38 @@ namespace LotteryWebApp.Service
             return deptObj;
         }
 
+        public GetFreeTicketResponse GetFreeTicketApi(IConfiguration configuration, GetFreeTicketRequest getFreeTicketRequest)
+        {
+            getFreeTicketRequest.channel = configuration.GetSection("channel").Value;
+
+            String url = configuration.GetSection("get_free_ticket").Value;
+            if (String.IsNullOrWhiteSpace(url))
+            {
+                log.Error("Missing configuration: get_free_ticket");
+                return new GetFreeTicketResponse
+                {
+                    responseCode = Code.ERROR,
+                    responseMessage = "Missing configuration: get_free_ticket"
+                };
+            }
+
+            String jsonData = SendPost(getFreeTicketRequest, ApiConstants.SERVICE_ID, url);
+            try
+            {
+                GetFreeTicketResponse deptObj = JsonSerializer.Deserialize<GetFreeTicketResponse>(jsonData);
+                return deptObj;
+            }
+            catch (System.Text.Json.JsonException ex)
+            {
+                log.Error($"GetFreeTicketApi returned invalid JSON: {jsonData}", ex);
+                return new GetFreeTicketResponse
+                {
+                    responseCode = Code.ERROR,
+                    responseMessage = $"Get free ticket API returned an invalid response: {jsonData}"
+                };
+            }
+        }
+
         public GetTopWininerResponse GetTopWinnerApi(IConfiguration configuration, GetTopWinnerRequest userTicketRequest)
         {
             userTicketRequest.serviceId = ApiConstants.SERVICE_ID;

+ 27 - 0
website/Service/ApiModels.cs

@@ -263,6 +263,8 @@ namespace LotteryWebApp.Service
         public string token { get; set; }
         public string gameId { get; set; }
         public string promotionCode { get; set; }
+        [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
+        public string channelPayment { get; set; }
         public List<TicketInfo> ticket { get; set; }
     }
 
@@ -288,7 +290,10 @@ namespace LotteryWebApp.Service
         public string serviceId { get; set; }
         public string token { get; set; }
         public string paymentCode { get; set; }
+        public string walletPin { get; set; }
+        public string channelPayment { get; set; }
         public string transIdByTicket { get; set; }
+        public string gameId { get; set; }
     }
 
     public class ConfirmBuyingTicketResponse
@@ -304,6 +309,8 @@ namespace LotteryWebApp.Service
     public class ConfirmOTPRequest : Posting
     {
         public string otp { get; set; }
+        [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
+        public string transIdByTicket { get; set; }
         public string msisdn { get; set; }
         public string language { get; set; }
         public string channel { get; set; }
@@ -351,6 +358,26 @@ namespace LotteryWebApp.Service
 
     }
 
+    public class GetFreeTicketRequest : Posting
+    {
+        public string requestId { get; set; }
+        public string gameId { get; set; }
+        public string type { get; set; }
+        public string msisdn { get; set; }
+        public string channel { get; set; }
+        public string language { get; set; }
+        public string token { get; set; }
+    }
+
+    public class GetFreeTicketResponse
+    {
+        public string code { get; set; }
+        public string money { get; set; }
+        public string expireDate { get; set; }
+        public string responseCode { get; set; }
+        public string responseMessage { get; set; }
+    }
+
     public class GetTopWininerResponse
     {
         public string responseCode { get; set; }

+ 1 - 1
website/Views/Shared/_Layout.cshtml

@@ -206,7 +206,7 @@
                 </div>
                 <div>
                     <a class="title-navigator">@Lang.buy_ticket</a>
-                </div>
+               </div>
             </div>
             <div class="navigator-footer-button @(navigator == Constants.HISTORY_NAVIGATOR ? "active" : "")"
                  onclick="navigatorClick('History'); return 0;">

+ 6 - 0
website/appsettings.json

@@ -35,6 +35,12 @@
   "auto_login": "http://10.228.102.57/privateapi/intergame/getUserInfor",
   "check_account": "http://171.244.50.177:8989/mytell/api/userApi/usersRegister/data",
 
+  "buy_ticketv2": "http://171.244.50.177:8900/api/loto/buyTicketV2/data",
+  "confirm_buying_ticketv2": "http://171.244.50.177:8900/api/loto/confirmTicketV2/data",
+
+  "get_free_ticket": "http://171.244.50.177:8900/api/loto/getPromotionCode/data",
+
+
   "Logging": {
     "LogLevel": {
       "Default": "Information",

BIN
website/wwwroot/Millions/assets/images/banner_free.jpg


BIN
website/wwwroot/Millions/assets/images/banner_free_transparent.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-error-43-no-text.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-error-43.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-error-44-no-text.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-error-44.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-error-45-no-text.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-floating-icon.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-success-no-text.png


BIN
website/wwwroot/Millions/assets/images/free-ticket-success.png


+ 785 - 0
website/wwwroot/Millions/css/popup-free-ticket.css

@@ -0,0 +1,785 @@
+.free-ticket-popup,
+.free-ticket-popup * {
+    box-sizing: border-box;
+}
+
+.free-ticket-popup {
+    position: fixed;
+    z-index: 90;
+    right: 12px;
+    bottom: calc(82px + env(safe-area-inset-bottom));
+    width: min(380px, calc(100vw - 24px));
+    aspect-ratio: 1552 / 322;
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+.free-ticket-popup__card {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    display: block;
+    width: 100%;
+    height: 100%;
+    color: inherit;
+    cursor: pointer;
+    filter: drop-shadow(0 4px 7px rgba(126, 86, 0, .3));
+    text-decoration: none;
+    transform-origin: center bottom;
+    will-change: transform, filter;
+}
+
+.free-ticket-popup__image {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+.free-ticket-popup__ticket-text,
+.free-ticket-popup__button-text {
+    position: absolute;
+    z-index: 2;
+    color: #06429b;
+    font-weight: 900;
+    text-align: center;
+    text-shadow: 0 1px rgba(255, 255, 255, .35);
+    pointer-events: none;
+}
+
+.free-ticket-popup__ticket-text {
+    top: 37%;
+    left: 3.2%;
+    width: 21%;
+    transform: rotate(-8deg);
+    font-size: clamp(8px, 2.8vw, 12px);
+    line-height: .86;
+    text-transform: uppercase;
+}
+
+.free-ticket-popup__button-text {
+    top: 36%;
+    right: 17%;
+    bottom: auto;
+    left: 28%;
+    display: flex;
+    height: 30%;
+    align-items: center;
+    justify-content: center;
+    gap: .2em;
+    overflow: hidden;
+    color: #282828;
+    font-size: clamp(8px, 2.8vw, 11px);
+    line-height: 1.1;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.free-ticket-popup__button-highlight {
+    color: #075bdc;
+}
+
+.free-ticket-popup__card:hover,
+.free-ticket-popup__card:focus-visible {
+    transform: translateY(-3px) scale(1.06);
+    filter: brightness(1.06) drop-shadow(0 4px 6px rgba(0, 35, 105, .42));
+    outline: 1px solid rgba(112, 183, 255, .9);
+    outline-offset: 1px;
+}
+
+.free-ticket-popup__card:active {
+    transform: translateY(0) scale(.96);
+}
+
+.free-ticket-popup__card--loading {
+    animation: free-ticket-loading .75s linear infinite !important;
+    cursor: wait;
+    opacity: .72;
+}
+
+.free-ticket-popup--claimed {
+    display: none;
+}
+
+body.free-ticket-result-open {
+    overflow: hidden;
+}
+
+.free-ticket-result[hidden] {
+    display: none;
+}
+
+.free-ticket-error-43[hidden],
+.free-ticket-error-44[hidden],
+.free-ticket-error-45[hidden] {
+    display: none;
+}
+
+.free-ticket-error-45 {
+    position: fixed;
+    z-index: 230;
+    inset: 0;
+    display: grid;
+    place-items: center;
+    padding: clamp(10px, 4vw, 24px);
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+.free-ticket-error-45__backdrop {
+    position: absolute;
+    inset: 0;
+    background: rgba(0, 20, 72, .68);
+    backdrop-filter: blur(3px);
+}
+
+.free-ticket-error-45__panel {
+    position: relative;
+    z-index: 1;
+    container-type: inline-size;
+    width: min(460px, 100%, calc((100dvh - 32px) * 508 / 406));
+    aspect-ratio: 508 / 406;
+    overflow: hidden;
+    border-radius: clamp(12px, 3vw, 18px);
+    box-shadow: 0 22px 55px rgba(0, 15, 70, .45);
+}
+
+.free-ticket-error-45__image {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+.free-ticket-error-45__copy {
+    position: absolute;
+    z-index: 2;
+    box-sizing: border-box;
+    margin: 0;
+    color: #082d7d;
+    font-weight: 700;
+    line-height: 1.05;
+    pointer-events: none;
+    text-align: center;
+}
+
+.free-ticket-error-45__copy--ticket-free,
+.free-ticket-error-45__copy--ticket-tickets,
+.free-ticket-error-45__copy--ticket-sold-out {
+    left: 38%;
+    width: 24%;
+    color: #fff;
+    font-size: clamp(18px, 4.57cqw, 21px);
+    font-weight: 900;
+    text-transform: uppercase;
+    transform: rotate(-8deg);
+}
+
+.free-ticket-error-45__copy--ticket-free { top: 8.8%; }
+.free-ticket-error-45__copy--ticket-tickets { top: 17.5%; }
+.free-ticket-error-45__copy--ticket-sold-out {
+    top: 26.2%;
+    color: #0a358b;
+    font-size: clamp(14px, 3.7cqw, 17px);
+}
+
+.free-ticket-error-45__copy--title {
+    top: 44.8%;
+    left: 18%;
+    width: 64%;
+    font-size: clamp(16px, 4.13cqw, 19px);
+    font-weight: 900;
+    white-space: nowrap;
+}
+
+.free-ticket-error-45__copy--title span { color: #e0182d; }
+
+.free-ticket-error-45__copy--description {
+    top: 55.4%;
+    left: 20%;
+    width: 60%;
+    color: #4d5876;
+    font-size: clamp(11px, 2.61cqw, 12px);
+    font-weight: 400;
+    line-height: 1.3;
+}
+
+.free-ticket-error-45__copy--next-title,
+.free-ticket-error-45__copy--next-primary,
+.free-ticket-error-45__copy--next-secondary {
+    left: 39%;
+    width: 40%;
+    font-size: clamp(10px, 2.61cqw, 12px);
+    text-align: left;
+}
+
+.free-ticket-error-45__copy--next-title { top: 67.4%; }
+.free-ticket-error-45__copy--next-primary {
+    top: 71%;
+    color: #e0182d;
+}
+.free-ticket-error-45__copy--next-secondary {
+    top: 74.5%;
+    font-weight: 400;
+}
+
+.free-ticket-error-45__copy--buy,
+.free-ticket-error-45__copy--close {
+    top: 80.7%;
+    display: flex;
+    height: 7.5%;
+    align-items: center;
+    justify-content: center;
+    font-size: clamp(10px, 2.83cqw, 13px);
+    font-weight: 900;
+}
+
+.free-ticket-error-45__copy--buy {
+    left: 12.2%;
+    width: 37.2%;
+    color: #fff;
+}
+
+.free-ticket-error-45__copy--close {
+    left: 51.2%;
+    width: 35%;
+}
+
+.free-ticket-error-45__copy--footer {
+    top: 90.3%;
+    left: 30%;
+    width: 59%;
+    font-size: clamp(10px, 2.61cqw, 12px);
+    font-weight: 400;
+    line-height: 1.05;
+}
+
+.free-ticket-error-45__action {
+    position: absolute;
+    z-index: 2;
+    display: block;
+    border: 0;
+    border-radius: 8px;
+    background: transparent;
+    cursor: pointer;
+    color: transparent;
+}
+
+.free-ticket-error-45__action--dismiss-icon {
+    top: 2.5%;
+    right: 1.8%;
+    width: 7.5%;
+    height: 10%;
+}
+
+.free-ticket-error-45__action--buy {
+    top: 80.9%;
+    left: 12.2%;
+    width: 37.2%;
+    height: 8.1%;
+}
+
+.free-ticket-error-45__action--close {
+    top: 80.9%;
+    left: 51.2%;
+    width: 35%;
+    height: 8.1%;
+}
+
+.free-ticket-error-45__action:focus-visible {
+    outline: 3px solid #ffd447;
+    outline-offset: 2px;
+}
+
+.free-ticket-visually-hidden {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border: 0;
+}
+
+.free-ticket-error-43,
+.free-ticket-error-44 {
+    position: fixed;
+    z-index: 230;
+    inset: 0;
+    display: grid;
+    place-items: center;
+    padding: clamp(10px, 4vw, 24px);
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+.free-ticket-error-43__backdrop,
+.free-ticket-error-44__backdrop {
+    position: absolute;
+    inset: 0;
+    background: rgba(0, 20, 72, .68);
+    backdrop-filter: blur(3px);
+}
+
+.free-ticket-error-43__panel,
+.free-ticket-error-44__panel {
+    position: relative;
+    z-index: 1;
+    container-type: inline-size;
+    width: min(460px, 100%, calc((100dvh - 32px) * 547 / 437));
+    aspect-ratio: 547 / 437;
+    overflow: hidden;
+    border-radius: clamp(12px, 3vw, 18px);
+    box-shadow: 0 22px 55px rgba(0, 15, 70, .45);
+}
+
+.free-ticket-error-43__image,
+.free-ticket-error-44__image {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+.free-ticket-error-43__copy,
+.free-ticket-error-44__copy,
+.free-ticket-result__copy {
+    position: absolute;
+    z-index: 2;
+    box-sizing: border-box;
+    margin: 0;
+    pointer-events: none;
+    text-align: center;
+}
+
+.free-ticket-error-43__copy--badge {
+    top: 14%;
+    left: 29%;
+    width: 34%;
+    color: #0a358b;
+    font-size: clamp(12px, 3.26cqw, 15px);
+    font-weight: 900;
+    line-height: 1.05;
+    text-transform: uppercase;
+    transform: rotate(-7deg);
+}
+
+.free-ticket-error-43__copy--title,
+.free-ticket-error-44__copy--title {
+    top: 39.2%;
+    left: 10%;
+    width: 80%;
+    color: #082d7d;
+    font-size: clamp(19px, 4.78cqw, 22px);
+    font-weight: 900;
+    line-height: 1;
+    text-wrap: balance;
+}
+
+.free-ticket-error-44__copy--title {
+    top: 43.7%;
+    font-size: clamp(18px, 4.57cqw, 21px);
+}
+
+.free-ticket-error-43__copy--description,
+.free-ticket-error-44__copy--description {
+    top: 51.5%;
+    left: 16%;
+    width: 68%;
+    color: #4d5876;
+    font-size: clamp(9px, 2.61cqw, 12px);
+    line-height: 1.25;
+    text-wrap: balance;
+}
+
+.free-ticket-error-44__copy--description {
+    top: 55.2%;
+    left: 17%;
+    width: 66%;
+    font-size: clamp(11px, 2.83cqw, 13px);
+}
+
+.free-ticket-error-43__copy--description {
+    font-size: clamp(11px, 3.04cqw, 14px);
+}
+
+.free-ticket-error-43__copy--daily-title {
+    top: 64.5%;
+    left: 36%;
+    width: 47%;
+    color: #074dc7;
+    font-size: clamp(12px, 3.26cqw, 15px);
+    font-weight: 900;
+    line-height: 1.05;
+    text-align: left;
+}
+
+.free-ticket-error-43__copy--daily-description {
+    top: 69.2%;
+    left: 36%;
+    width: 47%;
+    color: #44516e;
+    font-size: clamp(10px, 2.83cqw, 13px);
+    line-height: 1.15;
+    text-align: left;
+}
+
+.free-ticket-error-43__copy--buy,
+.free-ticket-error-43__copy--close,
+.free-ticket-error-44__copy--buy,
+.free-ticket-error-44__copy--close {
+    top: 79.8%;
+    display: flex;
+    height: 9.4%;
+    align-items: center;
+    justify-content: center;
+    font-size: clamp(13px, 3.48cqw, 16px);
+    font-weight: 900;
+    line-height: 1;
+}
+
+.free-ticket-error-43__copy--buy,
+.free-ticket-error-44__copy--buy {
+    left: 12.3%;
+    width: 36.8%;
+    color: #fff;
+}
+
+.free-ticket-error-43__copy--close,
+.free-ticket-error-44__copy--close {
+    left: 51.1%;
+    width: 35.9%;
+    color: #0843ac;
+}
+
+.free-ticket-error-44__copy--buy {
+    left: 10.9%;
+    width: 38%;
+}
+
+.free-ticket-error-44__copy--close {
+    left: 51.4%;
+    width: 37.8%;
+}
+
+.free-ticket-error-43__copy--footer,
+.free-ticket-error-44__copy--footer {
+    top: 90.3%;
+    left: 23%;
+    width: 65%;
+    color: #133b7f;
+    font-size: clamp(10px, 2.61cqw, 12px);
+    line-height: 1.05;
+}
+
+.free-ticket-error-43__copy--footer {
+    font-size: clamp(10px, 2.61cqw, 12px);
+}
+
+.free-ticket-error-44__copy--footer {
+    left: 27%;
+    width: 61%;
+}
+
+.free-ticket-error-44__copy--badge {
+    top: 14%;
+    left: 30%;
+    width: 30%;
+    color: #fff;
+    font-size: clamp(18px, 4.57cqw, 21px);
+    font-weight: 900;
+    line-height: 1.05;
+    text-transform: uppercase;
+    transform: rotate(-4deg);
+}
+
+.free-ticket-error-44__copy--chance {
+    top: 65.8%;
+    left: 34%;
+    width: 46%;
+    color: #44516e;
+    font-size: clamp(10px, 2.61cqw, 12px);
+    line-height: 1.15;
+    text-wrap: balance;
+}
+
+.free-ticket-error-44__copy--prize {
+    top: 72.2%;
+    left: 34%;
+    width: 46%;
+    color: #1353c7;
+    font-size: clamp(15px, 3.91cqw, 18px);
+    font-weight: 900;
+    white-space: nowrap;
+}
+
+.free-ticket-error-43__action,
+.free-ticket-error-44__action {
+    position: absolute;
+    z-index: 2;
+    display: block;
+    border: 0;
+    border-radius: 8px;
+    background: transparent;
+    cursor: pointer;
+    color: transparent;
+}
+
+.free-ticket-error-43__action--dismiss-icon,
+.free-ticket-error-44__action--dismiss-icon {
+    top: 2.5%;
+    right: 1.8%;
+    width: 7.5%;
+    height: 10%;
+}
+
+.free-ticket-error-43__action--buy,
+.free-ticket-error-44__action--buy {
+    top: 79.4%;
+    left: 12.3%;
+    width: 36.8%;
+    height: 8.8%;
+}
+
+.free-ticket-error-44__action--buy {
+    top: 79.8%;
+    left: 10.9%;
+    width: 38%;
+    height: 9.4%;
+}
+
+.free-ticket-error-43__action--close,
+.free-ticket-error-44__action--close {
+    top: 79.4%;
+    left: 51.1%;
+    width: 35.9%;
+    height: 8.8%;
+}
+
+.free-ticket-error-44__action--close {
+    top: 79.8%;
+    left: 51.4%;
+    width: 37.8%;
+    height: 9.4%;
+}
+
+.free-ticket-error-43__action:focus-visible,
+.free-ticket-error-44__action:focus-visible {
+    outline: 3px solid #ffd447;
+    outline-offset: 2px;
+}
+
+@media (max-width: 359px) {
+    .free-ticket-error-43,
+    .free-ticket-error-44 {
+        padding: 8px;
+    }
+}
+
+.free-ticket-result {
+    position: fixed;
+    z-index: 220;
+    inset: 0;
+    display: grid;
+    place-items: center;
+    padding: 20px;
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+.free-ticket-result__backdrop {
+    position: absolute;
+    inset: 0;
+    background: rgba(0, 20, 72, .68);
+    backdrop-filter: blur(3px);
+}
+
+.free-ticket-result__panel {
+    position: relative;
+    z-index: 1;
+    container-type: inline-size;
+    width: min(460px, 100%, calc((100dvh - 32px) * 520 / 412));
+    aspect-ratio: 520 / 412;
+    overflow: hidden;
+    border-radius: clamp(12px, 3vw, 18px);
+    box-shadow: 0 22px 55px rgba(0, 15, 70, .45);
+}
+
+.free-ticket-result__image {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+.free-ticket-result__copy--badge {
+    top: 12%;
+    left: 34%;
+    width: 32%;
+    color: #fff4a1;
+    font-size: clamp(20px, 5cqw, 23px);
+    font-weight: 900;
+    line-height: .95;
+    text-transform: uppercase;
+    transform: rotate(-5deg);
+}
+
+.free-ticket-result__copy--title {
+    top: 41.5%;
+    left: 16%;
+    width: 68%;
+    color: #061f69;
+    font-size: clamp(22px, 5.43cqw, 25px);
+    font-weight: 900;
+    line-height: 1.05;
+}
+
+.free-ticket-result__copy--received {
+    top: 50.5%;
+    left: 25%;
+    width: 50%;
+    color: #4d5876;
+    font-size: clamp(11px, 2.83cqw, 13px);
+}
+
+.free-ticket-result__copy--ticket {
+    top: 54.2%;
+    left: 18%;
+    width: 64%;
+    color: #d60021;
+    font-size: clamp(15px, 3.91cqw, 18px);
+    font-weight: 900;
+}
+
+.free-ticket-result__copy--chance {
+    top: 64.2%;
+    left: 31%;
+    width: 52%;
+    color: #4d5876;
+    font-size: clamp(10px, 2.5cqw, 11.5px);
+    line-height: 1.1;
+}
+
+.free-ticket-result__copy--prize {
+    top: 70.2%;
+    left: 31%;
+    width: 52%;
+    color: #d00020;
+    font-size: clamp(15px, 3.91cqw, 18px);
+    font-weight: 900;
+    white-space: nowrap;
+}
+
+.free-ticket-result__copy--use,
+.free-ticket-result__copy--later {
+    top: 79.4%;
+    display: flex;
+    height: 10.1%;
+    align-items: center;
+    justify-content: center;
+    font-size: clamp(13px, 3.48cqw, 16px);
+    font-weight: 900;
+    line-height: 1;
+}
+
+.free-ticket-result__copy--use {
+    left: 9.2%;
+    width: 39.2%;
+    color: #fff;
+}
+
+.free-ticket-result__copy--later {
+    left: 51%;
+    width: 39.6%;
+    color: #0843ac;
+}
+
+.free-ticket-result__copy--validity {
+    top: 89.8%;
+    left: 34%;
+    width: 56%;
+    color: #133b7f;
+    font-size: clamp(10px, 2.61cqw, 12px);
+    line-height: 1.05;
+}
+
+.free-ticket-result__action {
+    position: absolute;
+    z-index: 2;
+    display: block;
+    border: 0;
+    border-radius: 8px;
+    background: transparent;
+    cursor: pointer;
+    color: transparent;
+}
+
+.free-ticket-result__action--dismiss-icon {
+    top: 1.8%;
+    right: 1.5%;
+    width: 8%;
+    height: 11%;
+}
+
+.free-ticket-result__action--use {
+    top: 80.1%;
+    left: 9.2%;
+    width: 39.2%;
+    height: 10.1%;
+}
+
+.free-ticket-result__action--later {
+    top: 80.1%;
+    left: 51%;
+    width: 39.6%;
+    height: 10.1%;
+}
+
+.free-ticket-result__action:focus-visible {
+    outline: 3px solid #ffd447;
+    outline-offset: 2px;
+}
+
+@media (max-width: 359px) {
+    .free-ticket-popup {
+        right: 10px;
+    }
+
+}
+
+@media (max-width: 600px) {
+    .free-ticket-error-43,
+    .free-ticket-error-44,
+    .free-ticket-error-45,
+    .free-ticket-result {
+        padding: 6px;
+    }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+    .free-ticket-popup__card {
+        animation: free-ticket-attention 1s ease-in-out infinite;
+    }
+
+    .free-ticket-popup__card:hover,
+    .free-ticket-popup__card:focus-visible,
+    .free-ticket-popup__card:active {
+        animation-play-state: paused;
+    }
+}
+
+@keyframes free-ticket-attention {
+    0%, 68%, 100% {
+        transform: translateY(0) rotate(0);
+        filter: drop-shadow(0 3px 4px rgba(0, 35, 105, .3));
+    }
+    73% { transform: translateY(-2px) scale(1.008); }
+    78% {
+        transform: translateY(-3px) scale(1.015);
+        filter: brightness(1.07) drop-shadow(0 0 5px rgba(255, 201, 40, .8));
+    }
+    83% { transform: translateY(-2px) scale(1.008); }
+    88% { transform: translateY(0) rotate(0); }
+}
+
+@keyframes free-ticket-loading {
+    to { transform: rotate(360deg); }
+}