import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useAppSelector } from '@akinon/next/redux/hooks'; import { AccountService } from '../services/account'; import { setMasterpassRestAccountData, setAccountStatus, updateModalState, setShowLinkModal, setShowOTPModal, setCardToDelete, setRemovingCardId, updatePaymentState, resetState, setError } from '../redux/reducer'; import { PAYMENT_CONSTANTS } from '../utils/payment-constants'; import { handleMasterpassResponse, handleVerification } from '../utils/response-handler'; import { getPaymentPending, setPaymentPending } from '../utils/payment-utils'; import type { AccountAccessSuccessResponse, CardModel } from '../types/account.types'; import { useMasterpassToken } from './useMasterpassToken'; export const useMasterpassAccount = () => { const dispatch = useDispatch(); const { token, tokenData, accountData, modalState, accountStatus, newCardFormData, paymentState, currency } = useAppSelector((state) => state.masterpassRest); const initializeAccount = useCallback( async (newTokenData: any, rawToken?: string) => { if (!newTokenData?.AccountKey) return; const accountService = new AccountService(rawToken || token, newTokenData?.MerchantId); try { const response = await accountService.accountAccess({ accountKey: newTokenData.AccountKey, accountKeyType: 'Msisdn', userId: newTokenData.UserId }); if (response.statusCode !== 200 && response.exception) { const { code } = response.exception; if (code === PAYMENT_CONSTANTS.EXCEPTION_CODES.ACCOUNT_NOT_FOUND) { dispatch( setAccountStatus({ isAccountNotFound: true, isAccountNotLinked: false, shouldShowDirectForm: true }) ); } else if ( code === PAYMENT_CONSTANTS.EXCEPTION_CODES.ACCOUNT_NOT_LINKED_TO_MERCHANT ) { dispatch( setAccountStatus({ isAccountNotFound: false, isAccountNotLinked: true, shouldShowDirectForm: true }) ); dispatch(setShowLinkModal(true)); } } else if ('result' in response) { dispatch( setMasterpassRestAccountData( response as AccountAccessSuccessResponse ) ); dispatch( setAccountStatus({ isAccountNotFound: false, isAccountNotLinked: false, shouldShowDirectForm: false }) ); } } catch (error) { dispatch( setAccountStatus({ isAccountNotFound: false, isAccountNotLinked: false, shouldShowDirectForm: true }) ); } }, [dispatch, token] ); const onTokenReady = useCallback( (newTokenData: any, rawToken?: string) => { initializeAccount(newTokenData, rawToken); }, [initializeAccount] ); const { refetch } = useMasterpassToken({ useThreeD: paymentState.useThreeD, token, onTokenReady }); const updateModalStateAction = useCallback( (updates: Partial) => { dispatch(updateModalState(updates)); }, [dispatch] ); const resetData = useCallback(() => { dispatch(resetState()); }, [dispatch]); const refreshToken = async (options?: { three_d?: boolean; skipReset?: boolean }) => { if (options?.three_d !== false && !options?.skipReset) { resetData(); } const result = await refetch(options); if (result?.data?.token) { const decodedToken = window.Utils.decodeJwt(result.data.token); return { tokenData: decodedToken, token: result.data.token, merchantId: decodedToken?.MerchantId }; } return null; }; const refreshAccountData = useCallback(async () => { if (!tokenData?.AccountKey) return; const accountService = new AccountService(token, tokenData?.MerchantId); const response = await accountService.accountAccess({ accountKey: tokenData.AccountKey, accountKeyType: 'Msisdn', userId: tokenData.UserId }); if ('result' in response) { dispatch( setMasterpassRestAccountData(response as AccountAccessSuccessResponse) ); dispatch( setAccountStatus({ isAccountNotFound: false, isAccountNotLinked: false, shouldShowDirectForm: false }) ); } }, [tokenData?.AccountKey, tokenData?.UserId, tokenData?.MerchantId, token, dispatch]); const handleLinkConfirm = useCallback(async () => { try { const freshResult = await refreshToken({ three_d: false }); const activeTokenData = freshResult?.tokenData || tokenData; const activeToken = freshResult?.token || token; const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId; if (!activeTokenData?.AccountKey) return; const accountService = new AccountService(activeToken, activeMerchantId); const response = await accountService.linkToMerchant({ accountKey: activeTokenData.AccountKey }); const result = await handleMasterpassResponse(response); if (result.requiresOTP) { dispatch( updateModalState({ otpType: result.otpType, otpContext: 'account', verificationData: result.data, showLinkModal: false, showOTPModal: true }) ); } else if (result.success) { dispatch(setShowLinkModal(false)); await refreshAccountData(); } else { dispatch(setShowLinkModal(false)); } } catch (error) { dispatch(setShowLinkModal(false)); } }, [tokenData, token, dispatch, refreshToken, refreshAccountData]); const handleOTPSubmit = useCallback( async (otp: string, texts?: any) => { const result = await handleVerification(otp, token, tokenData?.MerchantId); const isPaymentContext = modalState.otpContext === 'payment'; if (result.success) { dispatch(setShowOTPModal(false)); // A payment-context OTP still has an open Masterpass transaction. The // caller has to complete it against the backend; refreshing the account // here would leave the order unfinalized. if (isPaymentContext) { return { success: true, isPaymentContext: true, verificationData: result.data }; } await refreshAccountData(); return { success: true }; } else if (result.sessionExpired) { dispatch(setShowOTPModal(false)); dispatch( updateModalState({ showInformationModal: true, informationModalData: { type: 'warning', title: texts?.sessionExpiredTitle || 'Session Expired', message: texts?.sessionExpiredMessage || 'Your session has expired due to inactivity. Please restart the process to continue.', secondaryMessage: texts?.sessionExpiredSecondaryMessage || 'For security reasons, verification codes are only valid for a limited time.', buttonText: texts?.sessionExpiredButton || 'Start Again' } }) ); return { success: false, sessionExpired: true }; } else if (result.requires3D && result.redirectUrl) { dispatch(setShowOTPModal(false)); // Leaving the tab for the bank restarts the wait, so refresh the record // without losing the order number it was opened with. if (isPaymentContext) { setPaymentPending(getPaymentPending()?.orderNo ?? null); } window.location.href = result.redirectUrl; return { success: false, requires3D: true, redirectUrl: result.redirectUrl, message: result.message || '3D Secure verification required' }; } else if (result.requiresOTP) { dispatch( updateModalState({ otpType: result.otpType, verificationData: result.data, showOTPModal: true }) ); return { success: false, requiresOTP: true, otpType: result.otpType }; } else { return { success: false, message: result.message }; } }, [ dispatch, refreshAccountData, token, tokenData?.MerchantId, modalState.otpContext ] ); const handleRemoveCard = useCallback( (card: CardModel) => { dispatch(setCardToDelete(card)); }, [dispatch] ); const confirmRemoveCard = useCallback(async () => { const { cardToDelete } = modalState; if (!cardToDelete || !tokenData?.AccountKey) return; dispatch(setRemovingCardId(cardToDelete.uniqueCardNumber)); try { const accountService = new AccountService(token, tokenData?.MerchantId); const response = await accountService.removeCard({ accountKey: tokenData.AccountKey, cardAlias: cardToDelete.cardAlias }); if (response.statusCode === 200) { await refreshAccountData(); dispatch(setCardToDelete(null)); dispatch( updatePaymentState({ selectedCard: null, selectedInstallment: null, installments: [], cardType: null, cvc: '' }) ); } else { dispatch( setError(response.exception?.message || 'Failed to remove card.') ); } } catch (error) { dispatch(setError('An error occurred while removing the card.')); } finally { dispatch(setRemovingCardId(null)); } }, [ modalState.cardToDelete, tokenData?.AccountKey, tokenData?.MerchantId, token, dispatch, refreshAccountData ]); const handleAddCard = useCallback( async (cardData?: { cardNumber: string; cardholderName: string; expiryDate: string; cvv: string; cardAlias: string; }) => { const finalCardData = cardData || { cardNumber: newCardFormData.cardNumber || '', cardholderName: newCardFormData.cardholderName || '', expiryDate: newCardFormData.expiryDate || '', cvv: newCardFormData.cvv || '', cardAlias: newCardFormData.cardAlias || '' }; try { const freshResult = await refreshToken({ three_d: false }); const activeTokenData = freshResult?.tokenData || tokenData; const activeToken = freshResult?.token || token; const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId; if (!activeTokenData?.AccountKey) { throw new Error('Token data not available'); } const accountService = new AccountService(activeToken, activeMerchantId); const timestamp = Date.now().toString().slice(-10); const random = Math.floor(Math.random() * 1000) .toString() .padStart(3, '0'); const requestReferenceNumber = `${timestamp}${random}`; const [month, year] = finalCardData.expiryDate.split('/'); const formattedExpiryDate = `${year}${month}`; const response = await accountService.addCard({ requestReferenceNumber, accountKey: activeTokenData.AccountKey, accountKeyType: 'Msisdn', cardNumber: finalCardData.cardNumber.replace(/\D/g, ''), cardHolderName: finalCardData.cardholderName, expiryDate: formattedExpiryDate, cvv: finalCardData.cvv, accountAliasName: finalCardData.cardAlias, userId: activeTokenData.UserId, isMsisdnValidatedByMerchant: activeTokenData.IsMsisdnValidated === 'True' }); const result = await handleMasterpassResponse(response); if (result.requiresOTP) { dispatch( updateModalState({ otpType: result.otpType, otpContext: 'account', verificationData: result.data, showLinkModal: false, showOTPModal: true }) ); return result; } else if (result.requires3D && result.redirectUrl) { window.location.href = result.redirectUrl; return result; } else if (result.success) { await new Promise((resolve) => setTimeout(resolve, 1000)); await refreshAccountData(); return { success: true, data: result.data }; } return result; } catch (error) { dispatch( setError( error instanceof Error ? error.message : 'An unexpected error occurred' ) ); return { success: false, message: error instanceof Error ? error.message : 'An unexpected error occurred' }; } }, [ tokenData?.AccountKey, tokenData?.UserId, newCardFormData, refreshAccountData, refreshToken, dispatch ] ); return { token, tokenData, accountData, modalState, accountStatus, newCardFormData, currency, updateModalState: updateModalStateAction, initializeAccount, refreshAccountData, refreshToken, handleLinkConfirm, handleOTPSubmit, handleRemoveCard, confirmRemoveCard, handleAddCard, resetData }; };