diff --git a/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx index 1ddd3bc5e..d9af43cbc 100644 --- a/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx +++ b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx @@ -7,6 +7,7 @@ import SettingAPI from '../../../api/setting'; import { SettingName } from '../../../models/setting'; import PayzenAPI from '../../../api/payzen'; import { Loader } from '../../base/loader'; +import { KryptonClient, KryptonError, ProcessPaymentAnswer } from '../../../models/payzen'; interface PayzenFormProps { onSubmit: () => void, @@ -27,8 +28,8 @@ interface PayzenFormProps { export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cartItems, customer, operator, formId }) => { const { t } = useTranslation('shared'); - const PayZenKR = useRef(null); - const [loading, setLoading] = useState(true); + const PayZenKR = useRef(null); + const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader'); useEffect(() => { const api = new SettingAPI(); @@ -43,6 +44,7 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE .then(({ KR }) => KR.addForm("#payzenPaymentForm")) .then(({ KR, result }) => KR.showForm(result.formId)) .then(({ KR }) => KR.onFormReady(handleFormReady)) + .then(({ KR }) => KR.onFormCreated(handleFormCreated)) .then(({ KR }) => PayZenKR.current = KR); }).catch(error => onError(error)); }); @@ -51,23 +53,36 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE /** * Callback triggered on PayZen successful payments */ - const onPaid = (event) => { + const onPaid = (event: ProcessPaymentAnswer): boolean => { if (event.clientAnswer.orderStatus === 'PAID') { PayZenKR.current.removeForms(); onSuccess(event.clientAnswer); } else { - onError(event.clientAnswer); + const transaction = event.clientAnswer.transactions[0]; + const error = `${transaction?.errorMessage}. ${transaction?.detailedErrorMessage || ''}`; + onError(error || event.clientAnswer.orderStatus); } + return true; }; + /** + * Callback triggered when the PayZen form was entirely loaded and displayed + */ const handleFormReady = () => { - setLoading(false); + setLoadingClass('hidden'); }; + /** + * Callback triggered when the PayZen form has started to show up but is not entirely loaded + */ + const handleFormCreated = () => { + setLoadingClass('loader-overlay'); + } + /** * Callback triggered when the PayZen payment was refused */ - const handleError = (answer) => { + const handleError = (answer: KryptonError) => { const message = `${answer.errorMessage}. ${answer.detailedErrorMessage ? answer.detailedErrorMessage : ''}`; onError(message); } @@ -82,8 +97,8 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE try { const { result } = await PayZenKR.current.validateForm(); if (result === null) { - PayZenKR.current.onSubmit(onPaid); - PayZenKR.current.onError(handleError); + await PayZenKR.current.onSubmit(onPaid); + await PayZenKR.current.onError(handleError); await PayZenKR.current.submit(); } } catch (err) { @@ -94,7 +109,7 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE const Loader: FunctionComponent = () => { return ( -
+
); @@ -102,7 +117,7 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE return (
- {loading && } +
diff --git a/app/frontend/src/javascript/models/payzen.ts b/app/frontend/src/javascript/models/payzen.ts index b30b6e1d2..1a48e62f5 100644 --- a/app/frontend/src/javascript/models/payzen.ts +++ b/app/frontend/src/javascript/models/payzen.ts @@ -6,3 +6,203 @@ export interface CreatePaymentResponse { formToken: string orderId: string } + +export interface OrderDetails { + mode?: 'TEST' | 'PRODUCTION', + orderCurrency?: string, + orderEffectiveAmount?: number, + orderId?: string, + orderTotalAmount?: number, + _type: 'V4/OrderDetails' +} + +export interface Customer { + email?: string, + reference?: string, + billingDetails?: { + address?: string, + address2?: string, + category?: 'PRIVATE' | 'COMPANY', + cellPhoneNumber?: string, + city?: string + country?: string, + district?: string, + firstName?: string, + identityCode?: string, + language?: 'DE' | 'EN' | 'ZH' | 'ES' | 'FR' | 'IT' | 'JP' | 'NL' | 'PL' | 'PT' | 'RU', + lastName?: string, + phoneNumber?: string, + state?: string, + streetNumber?: string, + title?: string, + zipCode?: string, + _type: 'V4/Customer/BillingDetails' + }, + shippingDetails: { + address?: string, + address2?: string, + category?: 'PRIVATE' | 'COMPANY', + city?: string + country?: string, + deliveryCompanyName?: string, + district?: string, + firstName?: string, + identityCode?: string, + lastName?: string, + legalName?: string, + phoneNumber?: string, + shippingMethod?: 'RECLAIM_IN_SHOP' | 'RELAY_POINT' | 'RECLAIM_IN_STATION' | 'PACKAGE_DELIVERY_COMPANY' | 'ETICKET', + shippingSpeed?: 'STANDARD' | 'EXPRESS' | 'PRIORITY', + state?: string, + streetNumber?: string, + zipCode?: string, + _type: 'V4/Customer/ShippingDetails' + }, + shoppingCart: { + insuranceAmount?: number, + shippingAmount?: number, + taxAmount?: number + cartItemInfo: Array<{ + productAmount?: string, + productLabel?: string + productQty?: number, + productRef?: string, + productType?: 'FOOD_AND_GROCERY' | 'AUTOMOTIVE' | 'ENTERTAINMENT' | 'HOME_AND_GARDEN' | 'HOME_APPLIANCE' | 'AUCTION_AND_GROUP_BUYING' | 'FLOWERS_AND_GIFTS' | 'COMPUTER_AND_SOFTWARE' | 'HEALTH_AND_BEAUTY' | 'SERVICE_FOR_INDIVIDUAL' | 'SERVICE_FOR_BUSINESS' | 'SPORTS' | 'CLOTHING_AND_ACCESSORIES' | 'TRAVEL' | 'HOME_AUDIO_PHOTO_VIDEO' | 'TELEPHONY', + productVat?: number, + }>, + _type: 'V4/Customer/ShoppingCart' + } + _type: 'V4/Customer/Customer' +} + +export interface PaymentTransaction { + amount?: number, + creationDate?: string, + currency?: string, + detailedErrorCode? : string, + detailedErrorMessage?: string, + detailedStatus?: 'ACCEPTED' | 'AUTHORISED' | 'AUTHORISED_TO_VALIDATE' | 'CANCELLED' | 'CAPTURED' | 'EXPIRED' | 'PARTIALLY_AUTHORISED' | 'REFUSED' | 'UNDER_VERIFICATION' | 'WAITING_AUTHORISATION' | 'WAITING_AUTHORISATION_TO_VALIDATE' | 'ERROR', + effectiveStrongAuthentication?: 'ENABLED' | 'DISABLED' , + errorCode?: string, + errorMessage?: string, + metadata?: any, + operationType?: 'DEBIT' | 'CREDIT' | 'VERIFICATION', + orderDetails?: OrderDetails, + paymentMethodToken?: string, + paymentMethodType?: 'CARD', + shopId?: string, + status?: 'PAID' | 'UNPAID' | 'RUNNING' | 'PARTIALLY_PAID', + transactionDetails?: { + creationContext?: 'CHARGE' | 'REFUND', + effectiveAmount?: number, + effectiveCurrency?: string, + liabilityShift?: 'YES' | 'NO', + mid?: string, + parentTransactionUuid?: string, + sequenceNumber?: string, + cardDetails?: any, + fraudManagement?: any, + taxAmount?: number, + taxRate?: number, + preTaxAmount?: number, + externalTransactionId?: number, + dcc?: any, + nsu?: string, + tid?: string, + acquirerNetwork?: string, + taxRefundAmount?: number, + occurrenceType?: string + }, + uuid?: string, + _type: 'V4/PaymentTransaction' +} + +export interface Payment { + customer: Customer, + orderCycle: 'OPEN' | 'CLOSED', + orderDetails: OrderDetails, + orderStatus: 'PAID' | 'UNPAID' | 'RUNNING' | 'PARTIALLY_PAID', + serverDate: string, + shopId: string, + transactions: Array, + _type: 'V4/Payment' +} + +export interface ProcessPaymentAnswer { + clientAnswer: Payment, + hash: string, + hashAlgorithm: string, + hashKey: string, + rawClientAnswer: string + _type: 'V4/Charge/ProcessPaymentAnswer' +} + +export interface KryptonError { + children: Array, + detailedErrorCode: string, + detailedErrorMessage: string, + errorCode: string, + errorMessage: string, + field: any, + formId: string, + metadata: { + answer: ProcessPaymentAnswer, + formToken: string + }, + _errorKey: string, + _type: 'krypton/error' +} + +export interface KryptonFocus { + field: string, + formId: string, + _type: 'krypton/focus' +} + +export interface KryptonConfig { + 'kr-public-key': string, + 'kr-language': string, + 'kr-post-url-success': string, + 'kr-get-url-success': string, + 'kr-post-url-refused': string, + 'kr-get-url-refused': string, + 'kr-clear-on-error': boolean, + 'kr-hide-debug-toolbar': boolean, + 'kr-spa-mode': boolean +} + +type DefaultCallback = () => void +type BrandChangedCallback = (event: {KR: KryptonClient, cardInfo: {brand: string}}) => void +type ErrorCallback = (event: KryptonError) => void +type FocusCallback = (event: KryptonFocus) => void +type InstallmentChangedCallback = (event: {KR: KryptonClient, installmentInfo: {brand: string, hasInterests: boolean, installmentCount: number, totalAmount: number}}) => void +type SubmitCallback = (event: ProcessPaymentAnswer) => boolean +type ClickCallback = (event: any) => boolean + +export interface KryptonClient { + addForm: (selector: string) => Promise<{KR: KryptonClient, result: {formId: string}}>, + showForm: (formId: string) => Promise<{KR: KryptonClient}>, + hideForm: (formId: string) => Promise<{KR: KryptonClient}>, + removeForms: () => Promise<{KR: KryptonClient}>, + attachForm: (selector: string) => Promise<{KR: KryptonClient}>, + onBrandChanged: (callback: BrandChangedCallback) => Promise<{KR: KryptonClient}>, + onError: (callback: ErrorCallback) => Promise<{KR: KryptonClient}>, + onFocus: (callback: FocusCallback) => Promise<{KR: KryptonClient}>, + onInstallmentChanged: (callback: InstallmentChangedCallback) => Promise<{KR: KryptonClient}> + onFormReady: (callback: DefaultCallback) => Promise<{KR: KryptonClient}>, + onFormCreated: (callback: DefaultCallback) => Promise<{KR: KryptonClient}>, + onSubmit: (callback: SubmitCallback) => Promise<{KR: KryptonClient}>, + button: { + onClick: (callback: ClickCallback | Promise) => Promise<{KR: KryptonClient}> + }, + openPopin: () => Promise<{KR: KryptonClient}>, + closePopin: () => Promise<{KR: KryptonClient}>, + fields: { + focus: (selector: string) => Promise<{KR: KryptonClient}>, + }, + setFormConfig: (config: KryptonConfig) => Promise<{KR: KryptonClient}>, + setShopName: (name: string) => Promise<{KR: KryptonClient}>, + setFormToken: (formToken: string) => Promise<{KR: KryptonClient}>, + validateForm: () => Promise<{KR: KryptonClient, result?: KryptonError}>, + submit: () => Promise<{KR: KryptonClient}>, +} diff --git a/app/frontend/src/stylesheets/modules/payzen-modal.scss b/app/frontend/src/stylesheets/modules/payzen-modal.scss index 2d1d1e1dc..ef6a8b63f 100644 --- a/app/frontend/src/stylesheets/modules/payzen-modal.scss +++ b/app/frontend/src/stylesheets/modules/payzen-modal.scss @@ -1,8 +1,17 @@ .payzen-modal { .payzen-form { + .hidden { + display: none; + } .loader { text-align: center; } + .loader-overlay { + position: absolute; + top: 65px; + left: 190px; + z-index: 1; + } .container { display: flex; justify-content: center;