|
@@ -6,6 +6,16 @@
|
|
|
}
|
|
}
|
|
|
@using LotteryWebApp.Languages;
|
|
@using LotteryWebApp.Languages;
|
|
|
@using LotteryWebApp.Common;
|
|
@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 {
|
|
@section Styles {
|
|
|
<link rel="stylesheet" href="/Millions/css/buy-ticket.css" asp-append-version="true" />
|
|
<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>
|
|
<span class="text-[13px] font-black" style="color: @themeColor">HTG</span>
|
|
|
</div>
|
|
</div>
|
|
|
</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
|
|
@Lang.millions_payment
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -239,12 +249,15 @@ else
|
|
|
|
|
|
|
|
<!-- Ticket Scrollable List Area -->
|
|
<!-- Ticket Scrollable List Area -->
|
|
|
<div id="ticketContainer" class="ticket-scroll-area p-4 flex flex-col gap-6 bg-[#F5F5F5] pb-6">
|
|
<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">
|
|
<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="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">
|
|
<div class="flex flex-col gap-1 pl-8">
|
|
|
<span class="text-[16px] font-extrabold text-[#333]">@Lang.millions_select_10_lucky_numbers</span>
|
|
<span class="text-[16px] font-extrabold text-[#333]">@Lang.millions_select_10_lucky_numbers</span>
|
|
@@ -280,11 +293,14 @@ else
|
|
|
</div>
|
|
</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 class="h-4"></div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -302,7 +318,7 @@ else
|
|
|
<span class="text-[13px] font-black text-[#0062FF]">HTG</span>
|
|
<span class="text-[13px] font-black text-[#0062FF]">HTG</span>
|
|
|
</div>
|
|
</div>
|
|
|
</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
|
|
@Lang.millions_payment
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -311,6 +327,28 @@ else
|
|
|
</div>
|
|
</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) -->
|
|
<!-- 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 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]">
|
|
<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>
|
|
|
</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) -->
|
|
<!-- 6-Slots OTP Grid (Large Gray Blocks) -->
|
|
|
<div class="flex items-center justify-center gap-2 w-full mt-1 px-2" id="otpInputs">
|
|
<div class="flex items-center justify-center gap-2 w-full mt-1 px-2" id="otpInputs">
|
|
@@ -480,6 +518,16 @@ else
|
|
|
placeholder="-">
|
|
placeholder="-">
|
|
|
}
|
|
}
|
|
|
</div>
|
|
</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 -->
|
|
<!-- OTP Error Message -->
|
|
|
<p id="otpError" class="text-[#0062FF] font-bold text-[14px] text-center mb-1 hidden animate__animated animate__shakeX"></p>
|
|
<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>
|
|
<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 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() {
|
|
function reindexTickets() {
|
|
|
$(".ticket-card").each(function(index) {
|
|
$(".ticket-card").each(function(index) {
|
|
@@ -928,12 +979,16 @@ else
|
|
|
filledTickets++;
|
|
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));
|
|
$("#totalPrice").text(formatMoneyV2(filledTickets * pricePerTicket));
|
|
|
$("#totalTicketCount").text(filledTickets);
|
|
$("#totalTicketCount").text(filledTickets);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function addNewTicket() {
|
|
function addNewTicket() {
|
|
|
|
|
+ if (isFreeTicketMode) return;
|
|
|
|
|
+
|
|
|
var container = $("#ticketContainer");
|
|
var container = $("#ticketContainer");
|
|
|
var wrapper = $("#addButtonWrapper");
|
|
var wrapper = $("#addButtonWrapper");
|
|
|
var currentIndex = $(".ticket-card").length + 1;
|
|
var currentIndex = $(".ticket-card").length + 1;
|
|
@@ -982,9 +1037,35 @@ else
|
|
|
|
|
|
|
|
// --- Order Summary Modal Logic ---
|
|
// --- Order Summary Modal Logic ---
|
|
|
// --- Payment Preparation & Order Summary Logic ---
|
|
// --- Payment Preparation & Order Summary Logic ---
|
|
|
|
|
+<<<<<<< HEAD
|
|
|
let pendingPaymentRequest = null;
|
|
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) {
|
|
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 isBSMode = @(Model.termType == Constants.PIC10_BIGSMALL_CODE || Model.termType == Constants.PIC10_ODDEVEN_CODE ? "true" : "false");
|
|
|
const tickets = [];
|
|
const tickets = [];
|
|
|
|
|
|
|
@@ -1007,6 +1088,8 @@ else
|
|
|
});
|
|
});
|
|
|
} else {
|
|
} else {
|
|
|
$(".ticket-card").each(function() {
|
|
$(".ticket-card").each(function() {
|
|
|
|
|
+ if (isFreeTicketMode && tickets.length >= 1) return false;
|
|
|
|
|
+
|
|
|
const balls = $(this).find(".ball-filled");
|
|
const balls = $(this).find(".ball-filled");
|
|
|
if (balls.length === maxBalls) {
|
|
if (balls.length === maxBalls) {
|
|
|
const selectedNumbers = [];
|
|
const selectedNumbers = [];
|
|
@@ -1025,12 +1108,62 @@ else
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+<<<<<<< HEAD
|
|
|
pendingPaymentRequest = {
|
|
pendingPaymentRequest = {
|
|
|
gameId: "@Model.termType",
|
|
gameId: "@Model.termType",
|
|
|
ticket: tickets
|
|
ticket: tickets
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
showOrderSummary(null, 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;
|
|
let currentTransId = null;
|
|
@@ -1079,7 +1212,7 @@ else
|
|
|
</div>
|
|
</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="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">
|
|
<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>
|
|
<span class="text-[12px] font-black text-[#0062FF]">HTG</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -1089,9 +1222,15 @@ else
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
$("#summaryTotalCount").text(displayTickets.length);
|
|
$("#summaryTotalCount").text(displayTickets.length);
|
|
|
|
|
+<<<<<<< HEAD
|
|
|
const totalMoney = apiData
|
|
const totalMoney = apiData
|
|
|
? (apiData.totalMoneyPayment || apiData.totalMoney || "0")
|
|
? (apiData.totalMoneyPayment || apiData.totalMoney || "0")
|
|
|
: displayTickets.reduce((sum, ticket) => sum + (parseInt(ticket.money, 10) || 0), 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));
|
|
$("#summaryTotalAmount").text(formatMoneyV2(totalMoney));
|
|
|
|
|
|
|
|
summaryModal.removeClass("hidden").addClass("flex");
|
|
summaryModal.removeClass("hidden").addClass("flex");
|
|
@@ -1102,10 +1241,23 @@ else
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function confirmCheckout(btn) {
|
|
function confirmCheckout(btn) {
|
|
|
|
|
+<<<<<<< HEAD
|
|
|
if (!pendingPaymentRequest) {
|
|
if (!pendingPaymentRequest) {
|
|
|
showNotification("@Lang.millions_ticket_not_valid", "warning");
|
|
showNotification("@Lang.millions_ticket_not_valid", "warning");
|
|
|
return;
|
|
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");
|
|
$("#paymentSourceModal").removeClass("hidden").addClass("flex");
|
|
|
}
|
|
}
|
|
@@ -1163,26 +1315,34 @@ else
|
|
|
type: 'POST',
|
|
type: 'POST',
|
|
|
success: function(data) {
|
|
success: function(data) {
|
|
|
if (data.responseCode === "0") {
|
|
if (data.responseCode === "0") {
|
|
|
|
|
+<<<<<<< HEAD
|
|
|
hidePaymentSourceModal();
|
|
hidePaymentSourceModal();
|
|
|
showOtpModal();
|
|
showOtpModal();
|
|
|
|
|
+=======
|
|
|
|
|
+ showOtpModal(false);
|
|
|
|
|
+>>>>>>> websiteNoNatCash
|
|
|
} else {
|
|
} else {
|
|
|
showNotification(data.responseMessage || "Failed to send OTP", data.responseCode);
|
|
showNotification(data.responseMessage || "Failed to send OTP", data.responseCode);
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
error: function() {
|
|
error: function() {
|
|
|
showNotification("Network error occurred.", "warning");
|
|
showNotification("Network error occurred.", "warning");
|
|
|
|
|
+ },
|
|
|
|
|
+ complete: function() {
|
|
|
|
|
+ isSendingOtp = false;
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let otpInterval;
|
|
let otpInterval;
|
|
|
- function showOtpModal() {
|
|
|
|
|
|
|
+ function showOtpModal(isWalletPin) {
|
|
|
hideOrderSummary();
|
|
hideOrderSummary();
|
|
|
$("#otpModal").removeClass("hidden").addClass("flex");
|
|
$("#otpModal").removeClass("hidden").addClass("flex");
|
|
|
$("#otpInputs input").val("");
|
|
$("#otpInputs input").val("");
|
|
|
|
|
+ $("#walletPinInput").val("").attr("type", "password");
|
|
|
|
|
+ $("#walletPinEye").attr("class", "fa-solid fa-eye");
|
|
|
$("#otpConfirmBtn").prop('disabled', true);
|
|
$("#otpConfirmBtn").prop('disabled', true);
|
|
|
$("#otpError").addClass("hidden").text("");
|
|
$("#otpError").addClass("hidden").text("");
|
|
|
- $("#otp1").focus();
|
|
|
|
|
|
|
|
|
|
// Map game type to readable name & image
|
|
// Map game type to readable name & image
|
|
|
let gameTypeStr = "@Model.termType";
|
|
let gameTypeStr = "@Model.termType";
|
|
@@ -1219,7 +1379,33 @@ else
|
|
|
$("#otpTicketTypeContainer").append(`<span class='text-[17px] font-[900] ${typeColorClass} italic'>${displayName}</span>`);
|
|
$("#otpTicketTypeContainer").append(`<span class='text-[17px] font-[900] ${typeColorClass} italic'>${displayName}</span>`);
|
|
|
|
|
|
|
|
$("#otpTotalAmount").text($("#summaryTotalAmount").text());
|
|
$("#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
|
|
// Add backspace support for OTP inputs
|
|
@@ -1320,25 +1506,68 @@ else
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function finalizePurchase(btn) {
|
|
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;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ isFinalizingPurchase = true;
|
|
|
const originalText = $(btn).html();
|
|
const originalText = $(btn).html();
|
|
|
$(btn).prop('disabled', true).html('<i class="fa-solid fa-spinner fa-spin mr-2"></i> ...');
|
|
$(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 = {
|
|
const finalData = {
|
|
|
transIdByTicket: currentTransId,
|
|
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({
|
|
$.ajax({
|
|
|
url: subDomain + '@Url.Action("ConfirmBuyingTicketV2", "Home")',
|
|
url: subDomain + '@Url.Action("ConfirmBuyingTicketV2", "Home")',
|
|
|
type: 'POST',
|
|
type: 'POST',
|
|
@@ -1356,14 +1585,21 @@ else
|
|
|
showNotification(res.responseMessage || "System is upgrading", res.responseCode);
|
|
showNotification(res.responseMessage || "System is upgrading", res.responseCode);
|
|
|
} else {
|
|
} else {
|
|
|
$("#otpError").text(res.responseMessage || "Invalid OTP").removeClass("hidden");
|
|
$("#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() {
|
|
error: function() {
|
|
|
$(btn).prop('disabled', false).html(originalText);
|
|
$(btn).prop('disabled', false).html(originalText);
|
|
|
showNotification("Network error occurred during payment completion.", "warning");
|
|
showNotification("Network error occurred during payment completion.", "warning");
|
|
|
|
|
+ },
|
|
|
|
|
+ complete: function() {
|
|
|
|
|
+ isFinalizingPurchase = false;
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|