|
|
@@ -208,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>
|
|
|
@@ -318,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>
|
|
|
@@ -1017,8 +1017,29 @@ else
|
|
|
// --- Payment Preparation & Order Summary Logic ---
|
|
|
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();
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
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 = [];
|
|
|
|
|
|
@@ -1074,6 +1095,9 @@ else
|
|
|
}
|
|
|
|
|
|
function selectPaymentChannel(channelPayment) {
|
|
|
+ if (isPreparingCheckout) return;
|
|
|
+ isPreparingCheckout = true;
|
|
|
+
|
|
|
currentChannelPayment = channelPayment;
|
|
|
hidePaymentChannelModal();
|
|
|
|
|
|
@@ -1100,6 +1124,9 @@ else
|
|
|
error: function(err) {
|
|
|
|
|
|
showNotification("Network error occurred.", "warning");
|
|
|
+ },
|
|
|
+ complete: function() {
|
|
|
+ isPreparingCheckout = false;
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
@@ -1173,17 +1200,14 @@ else
|
|
|
}
|
|
|
|
|
|
function confirmCheckout(btn) {
|
|
|
- if (hasFreeTicketPromotion) {
|
|
|
- hideOrderSummary();
|
|
|
- finalizePurchase(btn, true);
|
|
|
- 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...');
|
|
|
|
|
|
@@ -1203,6 +1227,9 @@ else
|
|
|
error: function() {
|
|
|
$(btn).prop('disabled', false).html(originalText);
|
|
|
showNotification("Network error occurred.", "warning");
|
|
|
+ },
|
|
|
+ complete: function() {
|
|
|
+ isSendingOtp = false;
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
@@ -1378,33 +1405,69 @@ else
|
|
|
$("#receiptSuccessModal").removeClass("hidden").addClass("flex");
|
|
|
}
|
|
|
|
|
|
- function finalizePurchase(btn, skipOtpValidation) {
|
|
|
- const isFreeTicketPurchase = hasFreeTicketPromotion && skipOtpValidation === true;
|
|
|
+ function finalizePurchase(btn) {
|
|
|
+ if (isFinalizingPurchase) return;
|
|
|
+
|
|
|
const isWalletPayment = currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET';
|
|
|
- let otpCode = isFreeTicketPurchase
|
|
|
- ? ""
|
|
|
- : (isWalletPayment ? $("#walletPinInput").val().trim() : "");
|
|
|
- if (!isFreeTicketPurchase && !isWalletPayment) {
|
|
|
+ let otpCode = isWalletPayment ? $("#walletPinInput").val().trim() : "";
|
|
|
+ if (!isWalletPayment) {
|
|
|
$("#otpInputs input").each(function() { otpCode += $(this).val(); });
|
|
|
}
|
|
|
|
|
|
- if (!isFreeTicketPurchase && ((isWalletPayment && otpCode.length === 0) || (!isWalletPayment && otpCode.length < 6))) {
|
|
|
+ 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,
|
|
|
gameId: isFreeTicketMode ? "@Constants.Millions_CODE" : "@Model.termType",
|
|
|
- paymentCode: isFreeTicketPurchase || currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET' ? null : otpCode,
|
|
|
- walletPin: !isFreeTicketPurchase && currentChannelPayment === '@Constants.NATCASH_WALLET_TICKET' ? otpCode : null,
|
|
|
+ paymentCode: null,
|
|
|
+ walletPin: isWalletPayment ? walletPin : null,
|
|
|
channelPayment: currentChannelPayment
|
|
|
};
|
|
|
|
|
|
- // Free-ticket transactions skip OTP; paid transactions keep the existing verification flow.
|
|
|
+ // This final API is unreachable until ConfirmOTP succeeds (or a wallet PIN is supplied).
|
|
|
$.ajax({
|
|
|
url: subDomain + '@Url.Action("ConfirmBuyingTicketV2", "Home")',
|
|
|
type: 'POST',
|
|
|
@@ -1434,6 +1497,9 @@ else
|
|
|
error: function() {
|
|
|
$(btn).prop('disabled', false).html(originalText);
|
|
|
showNotification("Network error occurred during payment completion.", "warning");
|
|
|
+ },
|
|
|
+ complete: function() {
|
|
|
+ isFinalizingPurchase = false;
|
|
|
}
|
|
|
});
|
|
|
}
|