1
0
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:
Sylvain 2021-04-07 16:21:12 +02:00
parent df7893f65f
commit 67d0ce24b4
32 changed files with 342 additions and 118 deletions

View File

@ -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));

View File

@ -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;
}

View File

@ -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;

View File

@ -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

View File

@ -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>
);

View File

@ -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();
/**

View File

@ -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]);

View File

@ -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">

View File

@ -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">

View File

@ -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);
/**

View File

@ -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();
}, []);

View File

@ -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();
}, []);

View File

@ -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);
/**

View File

@ -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.

View File

@ -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']));

View File

@ -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');
/**

View File

@ -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 }) => {

View File

@ -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');

View File

@ -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>;

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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);

View File

@ -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)

View File

@ -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 {

View File

@ -17,6 +17,7 @@
border-radius: 4px;
user-select: none;
text-decoration: none;
height: 38px;
&:hover {
background-color: #f2f2f2;

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -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

View File

@ -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;
}
}
}
}

View File

@ -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"

View File

@ -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"