mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
set payzen currency
also: refactored the comments in the react components to fit high quality
This commit is contained in:
parent
df7893f65f
commit
67d0ce24b4
@ -13,6 +13,9 @@ client.interceptors.response.use(function (response) {
|
||||
// Any status code that lie within the range of 2xx cause this function to trigger
|
||||
return response;
|
||||
}, function (error) {
|
||||
// 304 Not Modified should be considered as a success
|
||||
if (error.response?.status === 304) { return Promise.resolve(error.response); }
|
||||
|
||||
// Any status codes that falls outside the range of 2xx cause this function to trigger
|
||||
const message = error.response?.data || error.message || error;
|
||||
return Promise.reject(extractHumanReadableMessage(message));
|
||||
|
@ -17,6 +17,12 @@ export default class SettingAPI {
|
||||
return SettingAPI.toSettingsMap(res?.data);
|
||||
}
|
||||
|
||||
async update (name: SettingName, value: any): Promise<Setting> {
|
||||
const res: AxiosResponse = await apiClient.patch(`/api/settings/${name}`, { setting: { value } });
|
||||
if (res.status === 304) { return { name, value }; }
|
||||
return res?.data?.setting;
|
||||
}
|
||||
|
||||
async bulkUpdate (settings: Map<SettingName, any>): Promise<Map<SettingName, SettingBulkResult>> {
|
||||
const res: AxiosResponse = await apiClient.patch('/api/settings/bulk_update', { settings: SettingAPI.toObjectArray(settings) });
|
||||
return SettingAPI.toBulkMap(res?.data?.settings);
|
||||
@ -46,7 +52,7 @@ export default class SettingAPI {
|
||||
const dataArray: Array<Array<string | any>> = Object.entries(data);
|
||||
const map = new Map();
|
||||
dataArray.forEach(item => {
|
||||
map.set(item[0] as SettingName, item[1]);
|
||||
map.set(item[0] as SettingName, item[1] || '');
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* This component shows 3 input fields for filtering invoices/payment-schedules by reference, customer name and date
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { LabelledInput } from './labelled-input';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -10,25 +6,43 @@ interface DocumentFiltersProps {
|
||||
onFilterChange: (value: { reference: string, customer: string, date: Date }) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows 3 input fields for filtering invoices/payment-schedules by reference, customer name and date
|
||||
*/
|
||||
export const DocumentFilters: React.FC<DocumentFiltersProps> = ({ onFilterChange }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// stores the value of reference input
|
||||
const [referenceFilter, setReferenceFilter] = useState('');
|
||||
// stores the value of the customer input
|
||||
const [customerFilter, setCustomerFilter] = useState('');
|
||||
// stores the value of the date input
|
||||
const [dateFilter, setDateFilter] = useState(null);
|
||||
|
||||
/**
|
||||
* When any filter changes, trigger the callback with the current value of all filters
|
||||
*/
|
||||
useEffect(() => {
|
||||
onFilterChange({ reference: referenceFilter, customer: customerFilter, date: dateFilter });
|
||||
}, [referenceFilter, customerFilter, dateFilter])
|
||||
|
||||
/**
|
||||
* Callback triggered when the input 'reference' is updated.
|
||||
*/
|
||||
const handleReferenceUpdate = (e) => {
|
||||
setReferenceFilter(e.target.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the input 'customer' is updated.
|
||||
*/
|
||||
const handleCustomerUpdate = (e) => {
|
||||
setCustomerFilter(e.target.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the input 'date' is updated.
|
||||
*/
|
||||
const handleDateUpdate = (e) => {
|
||||
let date = e.target.value;
|
||||
if (e.target.value === '') date = null;
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* This component is a template for a clickable button that wraps the application style
|
||||
*/
|
||||
|
||||
import React, { ReactNode, BaseSyntheticEvent } from 'react';
|
||||
|
||||
interface FabButtonProps {
|
||||
@ -13,7 +9,9 @@ interface FabButtonProps {
|
||||
form?: string,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This component is a template for a clickable button that wraps the application style
|
||||
*/
|
||||
export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className, disabled, type, form, children }) => {
|
||||
/**
|
||||
* Check if the current component was provided an icon to display
|
||||
|
@ -1,13 +1,9 @@
|
||||
/**
|
||||
* This component is a template for an input component that wraps the application style
|
||||
*/
|
||||
|
||||
import React, { BaseSyntheticEvent, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import { debounce as _debounce } from 'lodash';
|
||||
|
||||
interface FabInputProps {
|
||||
id: string,
|
||||
onChange?: (value: any) => void,
|
||||
onChange?: (value: any, validity?: ValidityState) => void,
|
||||
defaultValue: any,
|
||||
icon?: ReactNode,
|
||||
addOn?: ReactNode,
|
||||
@ -17,12 +13,22 @@ interface FabInputProps {
|
||||
required?: boolean,
|
||||
debounce?: number,
|
||||
readOnly?: boolean,
|
||||
maxLength?: number,
|
||||
pattern?: string,
|
||||
placeholder?: string,
|
||||
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week',
|
||||
}
|
||||
|
||||
export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue, icon, className, disabled, type, required, debounce, addOn, addOnClassName, readOnly }) => {
|
||||
/**
|
||||
* This component is a template for an input component that wraps the application style
|
||||
*/
|
||||
export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue, icon, className, disabled, type, required, debounce, addOn, addOnClassName, readOnly, maxLength, pattern, placeholder }) => {
|
||||
const [inputValue, setInputValue] = useState<any>(defaultValue);
|
||||
|
||||
/**
|
||||
* When the component is mounted, initialize the default value for the input.
|
||||
* If the default value changes, update the value of the input until there's no content in it.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!inputValue) {
|
||||
setInputValue(defaultValue);
|
||||
@ -55,13 +61,13 @@ export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue,
|
||||
* Handle the change of content in the input field, and trigger the parent callback, if any
|
||||
*/
|
||||
const handleChange = (e: BaseSyntheticEvent): void => {
|
||||
const newValue = e.target.value;
|
||||
setInputValue(newValue);
|
||||
const { value, validity } = e.target;
|
||||
setInputValue(value);
|
||||
if (typeof onChange === 'function') {
|
||||
if (debounce) {
|
||||
debouncedOnChange(newValue);
|
||||
debouncedOnChange(value, validity);
|
||||
} else {
|
||||
onChange(newValue);
|
||||
onChange(value, validity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,7 +75,17 @@ export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue,
|
||||
return (
|
||||
<div className={`fab-input ${className ? className : ''}`}>
|
||||
{hasIcon() && <span className="fab-input--icon">{icon}</span>}
|
||||
<input id={id} type={type} className="fab-input--input" value={inputValue} onChange={handleChange} disabled={disabled} required={required} readOnly={readOnly} />
|
||||
<input id={id}
|
||||
type={type}
|
||||
className="fab-input--input"
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
readOnly={readOnly}
|
||||
maxLength={maxLength}
|
||||
pattern={pattern}
|
||||
placeholder={placeholder} />
|
||||
{hasAddOn() && <span className={`fab-input--addon ${addOnClassName ? addOnClassName : ''}`}>{addOn}</span>}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* This component is a template for a modal dialog that wraps the application style
|
||||
*/
|
||||
|
||||
import React, { ReactNode, BaseSyntheticEvent } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -31,10 +27,16 @@ interface FabModalProps {
|
||||
preventConfirm?: boolean
|
||||
}
|
||||
|
||||
// initial request to the API
|
||||
const blackLogoFile = CustomAssetAPI.get(CustomAssetName.LogoBlackFile);
|
||||
|
||||
/**
|
||||
* This component is a template for a modal dialog that wraps the application style
|
||||
*/
|
||||
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children, confirmButton, className, width = 'sm', closeButton, customFooter, onConfirm, preventConfirm }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// the theme's logo, for back backgrounds
|
||||
const blackLogo = blackLogoFile.read();
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,3 @@
|
||||
/**
|
||||
* This component renders a translation with some HTML content.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -9,6 +6,9 @@ interface HtmlTranslateProps {
|
||||
options?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* This component renders a translation with some HTML content.
|
||||
*/
|
||||
export const HtmlTranslate: React.FC<HtmlTranslateProps> = ({ trKey, options }) => {
|
||||
const { t } = useTranslation(trKey?.split('.')[1]);
|
||||
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* This component shows input field with its label, styled
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface LabelledInputProps {
|
||||
@ -12,6 +8,9 @@ interface LabelledInputProps {
|
||||
onChange: (value: any) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows input field with its label, styled
|
||||
*/
|
||||
export const LabelledInput: React.FC<LabelledInputProps> = ({ id, type, label, value, onChange }) => {
|
||||
return (
|
||||
<div className="input-with-label">
|
||||
|
@ -1,10 +1,8 @@
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
/**
|
||||
* This component is a wrapper that display a loader while the children components have their rendering suspended
|
||||
*/
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
|
||||
export const Loader: React.FC = ({children }) => {
|
||||
const loading = (
|
||||
<div className="fa-3x">
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* This component displays a summary of the monthly payment schedule for the current cart, with a subscription
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
@ -20,8 +16,13 @@ interface PaymentScheduleSummaryProps {
|
||||
schedule: PaymentSchedule
|
||||
}
|
||||
|
||||
/**
|
||||
* This component displays a summary of the monthly payment schedule for the current cart, with a subscription
|
||||
*/
|
||||
const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedule }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// is open, the modal dialog showing the full details of the payment schedule?
|
||||
const [modal, setModal] = useState(false);
|
||||
|
||||
/**
|
||||
|
@ -1,8 +1,3 @@
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
* for the currentUser
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IApplication } from '../models/application';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -20,14 +15,24 @@ interface PaymentSchedulesDashboardProps {
|
||||
currentUser: User
|
||||
}
|
||||
|
||||
// how many payment schedules should we display for each page?
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
* for the currentUser
|
||||
*/
|
||||
const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser }) => {
|
||||
const { t } = useTranslation('logged');
|
||||
|
||||
// list of displayed payment schedules
|
||||
const [paymentSchedules, setPaymentSchedules] = useState<Array<PaymentSchedule>>([]);
|
||||
// current page
|
||||
const [pageNumber, setPageNumber] = useState<number>(1);
|
||||
|
||||
/**
|
||||
* When the component is loaded first, refresh the list of schedules to fill the first page.
|
||||
*/
|
||||
useEffect(() => {
|
||||
handleRefreshList();
|
||||
}, []);
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IApplication } from '../models/application';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -20,17 +16,29 @@ interface PaymentSchedulesListProps {
|
||||
currentUser: User
|
||||
}
|
||||
|
||||
// how many payment schedules should we display for each page?
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// list of displayed payment schedules
|
||||
const [paymentSchedules, setPaymentSchedules] = useState<Array<PaymentSchedule>>([]);
|
||||
// current page
|
||||
const [pageNumber, setPageNumber] = useState<number>(1);
|
||||
// current filter, by reference, for the schedules
|
||||
const [referenceFilter, setReferenceFilter] = useState<string>(null);
|
||||
// current filter, by customer's name, for the schedules
|
||||
const [customerFilter, setCustomerFilter] = useState<string>(null);
|
||||
// current filter, by date, for the schedules and the deadlines
|
||||
const [dateFilter, setDateFilter] = useState<Date>(null);
|
||||
|
||||
/**
|
||||
* When the component is loaded first, refresh the list of schedules to fill the first page.
|
||||
*/
|
||||
useEffect(() => {
|
||||
handleRefreshList();
|
||||
}, []);
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
|
||||
import React, { ReactEventHandler, ReactNode, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from './loader';
|
||||
@ -29,18 +25,31 @@ interface PaymentSchedulesTableProps {
|
||||
operator: User,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// for each payment schedule: are the details (all deadlines) shown or hidden?
|
||||
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
|
||||
// is open, the modal dialog to confirm the cashing of a check?
|
||||
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
|
||||
// is open, the modal dialog the resolve a pending card payment?
|
||||
const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
|
||||
// the user cannot confirm the action modal (3D secure), unless he has resolved the pending action
|
||||
const [isConfirmActionDisabled, setConfirmActionDisabled] = useState<boolean>(true);
|
||||
// is open, the modal dialog to update the card details
|
||||
const [showUpdateCard, setShowUpdateCard] = useState<boolean>(false);
|
||||
// when an action is triggered on a deadline, the deadline is saved here until the action is done or cancelled.
|
||||
const [tempDeadline, setTempDeadline] = useState<PaymentScheduleItem>(null);
|
||||
// when an action is triggered on a deadline, the parent schedule is saved here until the action is done or cancelled.
|
||||
const [tempSchedule, setTempSchedule] = useState<PaymentSchedule>(null);
|
||||
// prevent submitting the form to update the card details, until all required fields are filled correctly
|
||||
const [canSubmitUpdateCard, setCanSubmitUpdateCard] = useState<boolean>(true);
|
||||
// errors are saved here, if any, for display purposes.
|
||||
const [errors, setErrors] = useState<string>(null);
|
||||
// is open, the modal dialog to cancel the associated subscription?
|
||||
const [showCancelSubscription, setShowCancelSubscription] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
|
@ -18,8 +18,12 @@ interface PayZenKeysFormProps {
|
||||
onValidKeys: (payZenSettings: Map<SettingName, string>) => void
|
||||
}
|
||||
|
||||
// all settings related to PayZen that are requested by this form
|
||||
const payZenSettings: Array<SettingName> = [SettingName.PayZenUsername, SettingName.PayZenPassword, SettingName.PayZenEndpoint, SettingName.PayZenHmacKey, SettingName.PayZenPublicKey];
|
||||
// settings related the to PayZen REST API (server side)
|
||||
const restApiSettings: Array<SettingName> = [SettingName.PayZenUsername, SettingName.PayZenPassword, SettingName.PayZenEndpoint, SettingName.PayZenHmacKey];
|
||||
|
||||
// initial request to the API
|
||||
const payZenKeys = SettingAPI.query(payZenSettings);
|
||||
|
||||
// Prevent multiples call to the payzen keys validation endpoint.
|
||||
|
@ -1,8 +1,4 @@
|
||||
/**
|
||||
* This component displays a summary of the PayZen account keys, with a button triggering the modal to edit them
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Loader } from './loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { IApplication } from '../models/application';
|
||||
@ -16,13 +12,23 @@ import { FabButton } from './fab-button';
|
||||
declare var Application: IApplication;
|
||||
|
||||
interface PayzenSettingsProps {
|
||||
onEditKeys: (onlinePaymentModule: {value: boolean}) => void
|
||||
onEditKeys: (onlinePaymentModule: { value: boolean }) => void,
|
||||
onCurrencyUpdateSuccess: (currency: string) => void
|
||||
}
|
||||
|
||||
const PAYZEN_HIDDEN = 'testpassword_HiDdEnHIddEnHIdDEnHiDdEnHIddEnHIdDEn';
|
||||
// placeholder value for the hidden settings
|
||||
const PAYZEN_HIDDEN = 'HiDdEnHIddEnHIdDEnHiDdEnHIddEnHIdDEn';
|
||||
|
||||
// settings related to PayZen that can be shown publicly
|
||||
const payZenPublicSettings: Array<SettingName> = [SettingName.PayZenPublicKey, SettingName.PayZenEndpoint, SettingName.PayZenUsername];
|
||||
// settings related to PayZen that must be kept on server-side
|
||||
const payZenPrivateSettings: Array<SettingName> = [SettingName.PayZenPassword, SettingName.PayZenHmacKey];
|
||||
const payZenSettings: Array<SettingName> = payZenPublicSettings.concat(payZenPrivateSettings);
|
||||
// other settings related to PayZen
|
||||
const payZenOtherSettings: Array<SettingName> = [SettingName.PayZenCurrency];
|
||||
// all PayZen settings
|
||||
const payZenSettings: Array<SettingName> = payZenPublicSettings.concat(payZenPrivateSettings).concat(payZenOtherSettings);
|
||||
|
||||
// icons for the inputs of each setting
|
||||
const icons:Map<SettingName, string> = new Map([
|
||||
[SettingName.PayZenHmacKey, 'subscript'],
|
||||
[SettingName.PayZenPassword, 'key'],
|
||||
@ -31,17 +37,28 @@ const icons:Map<SettingName, string> = new Map([
|
||||
[SettingName.PayZenPublicKey, 'info']
|
||||
])
|
||||
|
||||
const payZenKeys = SettingAPI.query(payZenPublicSettings);
|
||||
// initial requests to the API
|
||||
const payZenKeys = SettingAPI.query(payZenPublicSettings.concat(payZenOtherSettings));
|
||||
const isPresent = {
|
||||
[SettingName.PayZenPassword]: SettingAPI.isPresent(SettingName.PayZenPassword),
|
||||
[SettingName.PayZenHmacKey]: SettingAPI.isPresent(SettingName.PayZenHmacKey)
|
||||
};
|
||||
|
||||
export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys }) => {
|
||||
/**
|
||||
* This component displays a summary of the PayZen account keys, with a button triggering the modal to edit them
|
||||
*/
|
||||
export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCurrencyUpdateSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// all the values of the settings related to PayZen
|
||||
const [settings, updateSettings] = useImmer<Map<SettingName, string>>(new Map(payZenSettings.map(name => [name, ''])));
|
||||
// store a possible error state for currency
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
/**
|
||||
* When the component is mounted, we initialize the values of the settings with those fetched from the API.
|
||||
* For the private settings, we initialize them with the placeholder value, if the setting is set.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const map = payZenKeys.read();
|
||||
for (const setting of payZenPrivateSettings) {
|
||||
@ -51,42 +68,90 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys }) =>
|
||||
}, []);
|
||||
|
||||
|
||||
/**
|
||||
* Callback triggered when the user clicks on the "update keys" button.
|
||||
* This will open the modal dialog allowing to change the keys
|
||||
*/
|
||||
const handleKeysUpdate = (): void => {
|
||||
onEditKeys({ value: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the user changes the content of the currency input field.
|
||||
*/
|
||||
const handleCurrencyUpdate = (value: string, validity?: ValidityState): void => {
|
||||
if (!validity || validity.valid) {
|
||||
setError('');
|
||||
updateSettings(draft => draft.set(SettingName.PayZenCurrency, value));
|
||||
} else {
|
||||
setError(t('app.admin.invoices.payment.payzen.currency_error'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the user clicks on the "save currency" button.
|
||||
* This will update the setting on the server.
|
||||
*/
|
||||
const saveCurrency = (): void => {
|
||||
const api = new SettingAPI();
|
||||
api.update(SettingName.PayZenCurrency, settings.get(SettingName.PayZenCurrency)).then(result => {
|
||||
setError('');
|
||||
updateSettings(draft => draft.set(SettingName.PayZenCurrency, result.value));
|
||||
onCurrencyUpdateSuccess(result.value);
|
||||
}, reason => {
|
||||
setError(t('app.admin.invoices.payment.payzen.error_while_saving')+reason);
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="payzen-settings">
|
||||
<h3 className="title">{t('app.admin.invoices.payment.payzen.payzen_keys')}</h3>
|
||||
<div className="payzen-keys">
|
||||
{payZenSettings.map(setting => {
|
||||
return (
|
||||
<div className="key-wrapper" key={setting}>
|
||||
<label htmlFor={setting}>{t(`app.admin.invoices.payment.payzen.${setting}`)}</label>
|
||||
<FabInput defaultValue={settings.get(setting)}
|
||||
id={setting}
|
||||
type={payZenPrivateSettings.indexOf(setting) > -1 ? 'password' : 'text'}
|
||||
icon={<i className={`fas fa-${icons.get(setting)}`} />}
|
||||
readOnly
|
||||
disabled />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="edit-keys">
|
||||
<FabButton className="edit-keys-btn" onClick={handleKeysUpdate}>{t('app.admin.invoices.payment.edit_keys')}</FabButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="payzen-keys">
|
||||
{payZenPublicSettings.concat(payZenPrivateSettings).map(setting => {
|
||||
return (
|
||||
<div className="key-wrapper" key={setting}>
|
||||
<label htmlFor={setting}>{t(`app.admin.invoices.payment.payzen.${setting}`)}</label>
|
||||
<FabInput defaultValue={settings.get(setting)}
|
||||
id={setting}
|
||||
type={payZenPrivateSettings.indexOf(setting) > -1 ? 'password' : 'text'}
|
||||
icon={<i className={`fas fa-${icons.get(setting)}`} />}
|
||||
readOnly
|
||||
disabled />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="edit-keys">
|
||||
<FabButton className="edit-keys-btn" onClick={handleKeysUpdate}>{t('app.admin.invoices.payment.edit_keys')}</FabButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="payzen-currency">
|
||||
<h3 className="title">{t('app.admin.invoices.payment.payzen.currency')}</h3>
|
||||
<p className="currency-info" dangerouslySetInnerHTML={{__html: t('app.admin.invoices.payment.payzen.currency_info_html')}} />
|
||||
{error && <p className="currency-error">{error}</p>}
|
||||
<div className="payzen-currency-form">
|
||||
<div className="currency-wrapper">
|
||||
<label htmlFor="payzen_currency">{t('app.admin.invoices.payment.payzen.payzen_currency')}</label>
|
||||
<FabInput defaultValue={settings.get(SettingName.PayZenCurrency)}
|
||||
id="payzen_currency"
|
||||
icon={<i className="fas fa-money-bill" />}
|
||||
onChange={handleCurrencyUpdate}
|
||||
maxLength={3}
|
||||
pattern="[A-Z]{3}" />
|
||||
</div>
|
||||
<FabButton className="save-currency" onClick={saveCurrency}>{t('app.admin.invoices.payment.payzen.save')}</FabButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const PayzenSettingsWrapper: React.FC<PayzenSettingsProps> = ({ onEditKeys }) => {
|
||||
const PayzenSettingsWrapper: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCurrencyUpdateSuccess }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PayzenSettings onEditKeys={onEditKeys} />
|
||||
<PayzenSettings onEditKeys={onEditKeys} onCurrencyUpdateSuccess={onCurrencyUpdateSuccess} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('payzenSettings', react2angular(PayzenSettingsWrapper, ['onEditKeys']));
|
||||
Application.Components.component('payzenSettings', react2angular(PayzenSettingsWrapper, ['onEditKeys', 'onCurrencyUpdateSuccess']));
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* This component is a "card" publicly presenting the details of a plan
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
@ -26,6 +22,9 @@ interface PlanCardProps {
|
||||
onSelectPlan: (plan: Plan) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a "card" (visually), publicly presenting the details of a plan and allowing a user to subscribe.
|
||||
*/
|
||||
const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected }) => {
|
||||
const { t } = useTranslation('public');
|
||||
/**
|
||||
|
@ -27,6 +27,7 @@ interface SelectGatewayModalModalProps {
|
||||
onSuccess: (results: Map<SettingName, SettingBulkResult>) => void,
|
||||
}
|
||||
|
||||
// initial request to the API
|
||||
const paymentGateway = SettingAPI.get(SettingName.PaymentGateway);
|
||||
|
||||
const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, onError, onSuccess }) => {
|
||||
|
@ -1,8 +1,3 @@
|
||||
/**
|
||||
* This component is a switch enabling the users to choose if they want to pay by monthly schedule
|
||||
* or with a one time payment
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
@ -20,6 +15,10 @@ interface SelectScheduleProps {
|
||||
className: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a switch enabling the users to choose if they want to pay by monthly schedule
|
||||
* or with a one time payment
|
||||
*/
|
||||
const SelectSchedule: React.FC<SelectScheduleProps> = ({ show, selected, onChange, className }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
|
@ -7,13 +7,23 @@ interface StripeConfirmProps {
|
||||
onResponse: () => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component runs a 3D secure confirmation for the given Stripe payment (identified by clientSecret).
|
||||
* A message is shown, depending on the result of the confirmation.
|
||||
* In case of success, a callback "onResponse" is also run.
|
||||
*/
|
||||
export const StripeConfirm: React.FC<StripeConfirmProps> = ({ clientSecret, onResponse }) => {
|
||||
const stripe = useStripe();
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// the message displayed to the user
|
||||
const [message, setMessage] = useState<string>(t('app.shared.stripe_confirm.pending'));
|
||||
// the style class of the message
|
||||
const [type, setType] = useState<string>('info');
|
||||
|
||||
/**
|
||||
* When the component is mounted, run the 3DS confirmation.
|
||||
*/
|
||||
useEffect(() => {
|
||||
stripe.confirmCardPayment(clientSecret).then(function(result) {
|
||||
onResponse();
|
||||
@ -27,7 +37,8 @@ export const StripeConfirm: React.FC<StripeConfirmProps> = ({ clientSecret, onRe
|
||||
setMessage(t('app.shared.stripe_confirm.success'));
|
||||
}
|
||||
});
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
return <div className="stripe-confirm">
|
||||
<div className={`message--${type}`}><span className="message-text">{message}</span></div>
|
||||
</div>;
|
||||
|
@ -1,18 +1,21 @@
|
||||
/**
|
||||
* This component initializes the stripe's Elements tag with the API key
|
||||
*/
|
||||
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import SettingAPI from '../api/setting';
|
||||
import { SettingName } from '../models/setting';
|
||||
|
||||
// initial request to the API
|
||||
const stripePublicKey = SettingAPI.get(SettingName.StripePublicKey);
|
||||
|
||||
/**
|
||||
* This component initializes the stripe's Elements tag with the API key
|
||||
*/
|
||||
export const StripeElements: React.FC = memo(({ children }) => {
|
||||
const [stripe, setStripe] = useState(undefined);
|
||||
|
||||
/**
|
||||
* When this component is mounted, we initialize the <Elements> tag with the Stripe's public key
|
||||
*/
|
||||
useEffect(() => {
|
||||
const key = stripePublicKey.read();
|
||||
const promise = loadStripe(key.value);
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* Form to set the stripe's public and private keys
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { Loader } from './loader';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -15,31 +11,52 @@ interface StripeKeysFormProps {
|
||||
onValidKeys: (stripePublic: string, stripeSecret:string) => void
|
||||
}
|
||||
|
||||
// initial request to the API
|
||||
const stripeKeys = SettingAPI.query([SettingName.StripePublicKey, SettingName.StripeSecretKey]);
|
||||
|
||||
/**
|
||||
* Form to set the stripe's public and private keys
|
||||
*/
|
||||
const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// used to prevent promises from resolving if the component was unmounted
|
||||
const mounted = useRef(false);
|
||||
|
||||
// Stripe's public key
|
||||
const [publicKey, setPublicKey] = useState<string>('');
|
||||
// Icon of the input field for the Stripe's public key. Used to display if the key is valid.
|
||||
const [publicKeyAddOn, setPublicKeyAddOn] = useState<ReactNode>(null);
|
||||
const [publicKeyAddOnClassName, setPublicKeyAddOnClassName] = useState<string>('');
|
||||
// Style class for the add-on icon, for the public key
|
||||
const [publicKeyAddOnClassName, setPublicKeyAddOnClassName] = useState<'key-invalid' | 'key-valid' | ''>('');
|
||||
// Stripe's secret key
|
||||
const [secretKey, setSecretKey] = useState<string>('');
|
||||
// Icon of the input field for the Stripe's secret key. Used to display if the key is valid.
|
||||
const [secretKeyAddOn, setSecretKeyAddOn] = useState<ReactNode>(null);
|
||||
const [secretKeyAddOnClassName, setSecretKeyAddOnClassName] = useState<string>('');
|
||||
// Style class for the add-on icon, for the public key
|
||||
const [secretKeyAddOnClassName, setSecretKeyAddOnClassName] = useState<'key-invalid' | 'key-valid' | ''>('');
|
||||
|
||||
/**
|
||||
* When the component loads for the first time:
|
||||
* - mark it as mounted
|
||||
* - initialize the keys with the values fetched from the API (if any)
|
||||
*/
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
const keys = stripeKeys.read();
|
||||
setPublicKey(keys.get(SettingName.StripePublicKey));
|
||||
setSecretKey(keys.get(SettingName.StripeSecretKey));
|
||||
|
||||
// when the component unmounts, mark it as unmounted
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* When the style class for the public and private key are updated, check if they indicate valid keys.
|
||||
* If both are valid, run the 'onValidKeys' callback
|
||||
*/
|
||||
useEffect(() => {
|
||||
const validClassName = 'key-valid';
|
||||
if (publicKeyAddOnClassName === validClassName && secretKeyAddOnClassName === validClassName) {
|
||||
|
@ -1,8 +1,3 @@
|
||||
/**
|
||||
* This component enables the user to input his card data or process payments.
|
||||
* Supports Strong-Customer Authentication (SCA).
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from './loader';
|
||||
@ -41,8 +36,13 @@ interface StripeModalProps {
|
||||
customer: User
|
||||
}
|
||||
|
||||
// initial request to the API
|
||||
const cgvFile = CustomAssetAPI.get(CustomAssetName.CgvFile);
|
||||
|
||||
/**
|
||||
* This component enables the user to input his card data or process payments.
|
||||
* Supports Strong-Customer Authentication (SCA).
|
||||
*/
|
||||
const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer }) => {
|
||||
// customer's wallet
|
||||
const [wallet, setWallet] = useState(null);
|
||||
|
@ -1,7 +1,3 @@
|
||||
/**
|
||||
* This component displays a summary of the amount paid with the virtual wallet, for the current transaction
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
@ -26,6 +22,9 @@ interface WalletInfoProps {
|
||||
price: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component displays a summary of the amount paid with the virtual wallet, for the current transaction
|
||||
*/
|
||||
export const WalletInfo: React.FC<WalletInfoProps> = ({ cartItems, currentUser, wallet, price }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
const [remainingPrice, setRemainingPrice] = useState(0);
|
||||
|
@ -708,6 +708,13 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
console.error(errors);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the PayZen currency was successfully updated
|
||||
*/
|
||||
$scope.alertPayZenCurrencyUpdated = function (currency) {
|
||||
growl.success(_t('app.admin.invoices.payment.payzen.currency_updated', { CURRENCY: currency }));
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup the feature-tour for the admin/invoices page.
|
||||
* This is intended as a contextual help (when pressing F1)
|
||||
|
@ -107,14 +107,15 @@ export enum SettingName {
|
||||
PayZenPassword = 'payzen_password',
|
||||
PayZenEndpoint = 'payzen_endpoint',
|
||||
PayZenPublicKey = 'payzen_public_key',
|
||||
PayZenHmacKey = 'payzen_hmac'
|
||||
PayZenHmacKey = 'payzen_hmac',
|
||||
PayZenCurrency = 'payzen_currency'
|
||||
}
|
||||
|
||||
export interface Setting {
|
||||
name: SettingName,
|
||||
value: string,
|
||||
last_update: Date,
|
||||
history: Array<HistoryValue>
|
||||
last_update?: Date,
|
||||
history?: Array<HistoryValue>
|
||||
}
|
||||
|
||||
export interface SettingError {
|
||||
|
@ -17,6 +17,7 @@
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
height: 38px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f2f2f2;
|
||||
|
@ -14,4 +14,35 @@
|
||||
margin: 5px 15px;
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.payzen-currency {
|
||||
.currency-info {
|
||||
padding: 15px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #faebcc;
|
||||
border-radius: 4px;
|
||||
color: #8a6d3b;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
.currency-error {
|
||||
padding: 15px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #ebccd1;
|
||||
color: #a94442;
|
||||
background-color: #f2dede;
|
||||
}
|
||||
.payzen-currency-form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
|
||||
.currency-wrapper {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
.save-currency {
|
||||
margin: 5px 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-t" ng-show="allSettings.online_payment_module === 'true' && allSettings.payment_gateway === 'payzen'">
|
||||
<payzen-settings on-edit-keys="selectPaymentGateway" />
|
||||
<payzen-settings on-edit-keys="selectPaymentGateway" on-currency-update-success="alertPayZenCurrencyUpdated" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -115,7 +115,8 @@ class Setting < ApplicationRecord
|
||||
payzen_password
|
||||
payzen_endpoint
|
||||
payzen_public_key
|
||||
payzen_hmac] }
|
||||
payzen_hmac
|
||||
payzen_currency] }
|
||||
# WARNING: when adding a new key, you may also want to add it in app/policies/setting_policy.rb#public_whitelist
|
||||
|
||||
def value
|
||||
|
@ -356,3 +356,15 @@ section#cookies-modal div.cookies-consent .cookies-actions button.accept {
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.payzen-settings {
|
||||
.payzen-currency {
|
||||
.payzen-currency-form {
|
||||
.save-currency {
|
||||
background-color: $secondary;
|
||||
border-color: $secondary;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -654,6 +654,13 @@ en:
|
||||
payzen_endpoint: "REST API server name"
|
||||
payzen_hmac: "HMAC-SHA-256 key"
|
||||
payzen_public_key: "Client public key"
|
||||
currency: "Currency"
|
||||
payzen_currency: "PayZen currency"
|
||||
currency_info_html: "Please specify below the currency used for online payment. You should provide a three-letter ISO code, from the list of <a href='https://payzen.io/en-EN/payment-file/ips/list-of-supported-currencies.html' target='_blank'> PayZen supported currencies</a>."
|
||||
save: "Save"
|
||||
currency_error: "The inputted value is not a valid currency"
|
||||
error_while_saving: "An error occurred while saving the currency: "
|
||||
currency_updated: "The PayZen currency was successfully updated to {CURRENCY}."
|
||||
# select a payment gateway
|
||||
gateway_modal:
|
||||
select_gateway_title: "Select a payment gateway"
|
||||
|
@ -654,6 +654,13 @@ fr:
|
||||
payzen_endpoint: "Nom du serveur de l'API REST"
|
||||
payzen_hmac: "Clef HMAC-SHA-256"
|
||||
payzen_public_key: "Clef publique client"
|
||||
currency: "Devise"
|
||||
payzen_currency: "Devise PayZen"
|
||||
currency_info_html: "Veuillez indiquer la devise à utiliser lors des paiements en ligne. Vous devez fournir un code ISO à trois lettres, issu de la liste des <a href='https://payzen.io/fr-FR/back-office/reporting/liste-des-devises-supportees.html' target='_blank'>devises supportées par PayZen</a>."
|
||||
save: "Enregistrer"
|
||||
currency_error: "La valeur saisie n'est pas une devise valide"
|
||||
error_while_saving: "Une erreur est survenue lors de l'enregistrement de la devise : "
|
||||
currency_updated: "La devise PayZen a bien été mise à jour à {CURRENCY}."
|
||||
# select a payment gateway
|
||||
gateway_modal:
|
||||
select_gateway_title: "Sélectionnez une passerelle de paiement"
|
||||
|
Loading…
x
Reference in New Issue
Block a user