mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
save & retreive stripe settings
This commit is contained in:
parent
720328ee92
commit
c25e54a6af
@ -27,8 +27,8 @@ class API::PlansController < API::ApiController
|
|||||||
partner = params[:plan][:partner_id].empty? ? nil : User.find(params[:plan][:partner_id])
|
partner = params[:plan][:partner_id].empty? ? nil : User.find(params[:plan][:partner_id])
|
||||||
|
|
||||||
res = PlansService.create(type, partner, plan_params)
|
res = PlansService.create(type, partner, plan_params)
|
||||||
if res[:errors]
|
if res.errors
|
||||||
render json: res[:errors], status: :unprocessable_entity
|
render json: res.errors, status: :unprocessable_entity
|
||||||
else
|
else
|
||||||
render json: res, status: :created
|
render json: res, status: :created
|
||||||
end
|
end
|
||||||
|
@ -32,8 +32,11 @@ class API::SettingsController < API::ApiController
|
|||||||
db_setting = Setting.find_or_initialize_by(name: setting[:name])
|
db_setting = Setting.find_or_initialize_by(name: setting[:name])
|
||||||
next unless SettingService.before_update(db_setting)
|
next unless SettingService.before_update(db_setting)
|
||||||
|
|
||||||
db_setting.save && db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile)
|
if db_setting.save
|
||||||
SettingService.after_update(db_setting)
|
db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile)
|
||||||
|
SettingService.after_update(db_setting)
|
||||||
|
end
|
||||||
|
|
||||||
@settings.push db_setting
|
@settings.push db_setting
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
PaymentSchedule,
|
PaymentSchedule,
|
||||||
PaymentScheduleIndexRequest, RefreshItemResponse
|
PaymentScheduleIndexRequest, RefreshItemResponse
|
||||||
} from '../models/payment-schedule';
|
} from '../models/payment-schedule';
|
||||||
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
|
|
||||||
|
|
||||||
export default class PaymentScheduleAPI {
|
export default class PaymentScheduleAPI {
|
||||||
async list (query: PaymentScheduleIndexRequest): Promise<Array<PaymentSchedule>> {
|
async list (query: PaymentScheduleIndexRequest): Promise<Array<PaymentSchedule>> {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import apiClient from './api-client';
|
import apiClient from './api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { Setting, SettingName } from '../models/setting';
|
import { Setting, SettingBulkResult, SettingError, SettingName } from '../models/setting';
|
||||||
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
|
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
|
||||||
|
|
||||||
export default class SettingAPI {
|
export default class SettingAPI {
|
||||||
@ -10,10 +10,18 @@ export default class SettingAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async query (names: Array<SettingName>): Promise<Map<SettingName, any>> {
|
async query (names: Array<SettingName>): Promise<Map<SettingName, any>> {
|
||||||
const res: AxiosResponse = await apiClient.get(`/api/settings/?names=[${names.join(',')}]`);
|
const params = new URLSearchParams();
|
||||||
|
params.append('names', `['${names.join("','")}']`);
|
||||||
|
|
||||||
|
const res: AxiosResponse = await apiClient.get(`/api/settings?${params.toString()}`);
|
||||||
return SettingAPI.toSettingsMap(res?.data);
|
return SettingAPI.toSettingsMap(res?.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
static get (name: SettingName): IWrapPromise<Setting> {
|
static get (name: SettingName): IWrapPromise<Setting> {
|
||||||
const api = new SettingAPI();
|
const api = new SettingAPI();
|
||||||
return wrapPromise(api.get(name));
|
return wrapPromise(api.get(name));
|
||||||
@ -24,15 +32,41 @@ export default class SettingAPI {
|
|||||||
return wrapPromise(api.query(names));
|
return wrapPromise(api.query(names));
|
||||||
}
|
}
|
||||||
|
|
||||||
private
|
private static toSettingsMap(data: Object): Map<SettingName, any> {
|
||||||
|
|
||||||
static toSettingsMap(data: Object): Map<SettingName, any> {
|
|
||||||
const dataArray: Array<Array<string | any>> = Object.entries(data);
|
const dataArray: Array<Array<string | any>> = Object.entries(data);
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
dataArray.forEach(item => {
|
dataArray.forEach(item => {
|
||||||
map.set(SettingName[item[0]], item[1]);
|
map.set(item[0] as SettingName, item[1]);
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static toBulkMap(data: Array<Setting|SettingError>): Map<SettingName, SettingBulkResult> {
|
||||||
|
const map = new Map();
|
||||||
|
data.forEach(item => {
|
||||||
|
const itemData: SettingBulkResult = { status: true };
|
||||||
|
if ('error' in item) {
|
||||||
|
itemData.error = item.error;
|
||||||
|
itemData.status = false;
|
||||||
|
}
|
||||||
|
if ('value' in item) {
|
||||||
|
itemData.value = item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.set(item.name as SettingName, itemData)
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static toObjectArray(data: Map<SettingName, any>): Array<Object> {
|
||||||
|
const array = [];
|
||||||
|
data.forEach((value, key) => {
|
||||||
|
array.push({
|
||||||
|
name: key,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return array;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,12 +2,15 @@
|
|||||||
* This component is a template for an input component that wraps the application style
|
* This component is a template for an input component that wraps the application style
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { BaseSyntheticEvent, ReactNode, useCallback, useState } from 'react';
|
import React, { BaseSyntheticEvent, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||||
import { debounce as _debounce } from 'lodash';
|
import { debounce as _debounce } from 'lodash';
|
||||||
|
import SettingAPI from '../api/setting';
|
||||||
|
import { SettingName } from '../models/setting';
|
||||||
|
import { loadStripe } from '@stripe/stripe-js';
|
||||||
|
|
||||||
interface FabInputProps {
|
interface FabInputProps {
|
||||||
id: string,
|
id: string,
|
||||||
onChange?: (event: BaseSyntheticEvent) => void,
|
onChange?: (value: any) => void,
|
||||||
value: any,
|
value: any,
|
||||||
icon?: ReactNode,
|
icon?: ReactNode,
|
||||||
addOn?: ReactNode,
|
addOn?: ReactNode,
|
||||||
@ -19,10 +22,14 @@ interface FabInputProps {
|
|||||||
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week',
|
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const FabInput: React.FC<FabInputProps> = ({ id, onChange, value, icon, className, disabled, type, required, debounce, addOn, addOnClassName }) => {
|
export const FabInput: React.FC<FabInputProps> = ({ id, onChange, value, icon, className, disabled, type, required, debounce, addOn, addOnClassName }) => {
|
||||||
const [inputValue, setInputValue] = useState<any>(value);
|
const [inputValue, setInputValue] = useState<any>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(value);
|
||||||
|
onChange(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the current component was provided an icon to display
|
* Check if the current component was provided an icon to display
|
||||||
*/
|
*/
|
||||||
@ -46,12 +53,13 @@ export const FabInput: React.FC<FabInputProps> = ({ id, onChange, value, icon, c
|
|||||||
* Handle the action of the button
|
* Handle the action of the button
|
||||||
*/
|
*/
|
||||||
const handleChange = (e: BaseSyntheticEvent): void => {
|
const handleChange = (e: BaseSyntheticEvent): void => {
|
||||||
setInputValue(e.target.value);
|
const newValue = e.target.value;
|
||||||
|
setInputValue(newValue);
|
||||||
if (typeof onChange === 'function') {
|
if (typeof onChange === 'function') {
|
||||||
if (debounce) {
|
if (debounce) {
|
||||||
handler(e);
|
handler(newValue);
|
||||||
} else {
|
} else {
|
||||||
onChange(e);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* The configuration of a payment gateway is required to enable the online payments.
|
* The configuration of a payment gateway is required to enable the online payments.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { BaseSyntheticEvent, useState } from 'react';
|
import React, { BaseSyntheticEvent, useEffect, useState } from 'react';
|
||||||
import { react2angular } from 'react2angular';
|
import { react2angular } from 'react2angular';
|
||||||
import { Loader } from './loader';
|
import { Loader } from './loader';
|
||||||
import { IApplication } from '../models/application';
|
import { IApplication } from '../models/application';
|
||||||
@ -12,7 +12,8 @@ import { FabModal, ModalSize } from './fab-modal';
|
|||||||
import { User } from '../models/user';
|
import { User } from '../models/user';
|
||||||
import { Gateway } from '../models/gateway';
|
import { Gateway } from '../models/gateway';
|
||||||
import { StripeKeysForm } from './stripe-keys-form';
|
import { StripeKeysForm } from './stripe-keys-form';
|
||||||
import { SettingName } from '../models/setting';
|
import { SettingBulkResult, SettingName } from '../models/setting';
|
||||||
|
import SettingAPI from '../api/setting';
|
||||||
|
|
||||||
|
|
||||||
declare var Application: IApplication;
|
declare var Application: IApplication;
|
||||||
@ -21,22 +22,31 @@ interface SelectGatewayModalModalProps {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
|
onError: (errors: Map<SettingName, SettingBulkResult>|any) => void,
|
||||||
|
onSuccess: (results: Map<SettingName, SettingBulkResult>) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal }) => {
|
const paymentGateway = SettingAPI.get(SettingName.PaymentGateway);
|
||||||
|
|
||||||
|
const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, onError, onSuccess }) => {
|
||||||
const { t } = useTranslation('admin');
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
const [preventConfirmGateway, setPreventConfirmGateway] = useState<boolean>(true);
|
const [preventConfirmGateway, setPreventConfirmGateway] = useState<boolean>(true);
|
||||||
const [selectedGateway, setSelectedGateway] = useState<string>('');
|
const [selectedGateway, setSelectedGateway] = useState<string>('');
|
||||||
const [gatewayConfig, setGatewayConfig] = useState<Map<SettingName, string>>(new Map());
|
const [gatewayConfig, setGatewayConfig] = useState<Map<SettingName, string>>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const gateway = paymentGateway.read();
|
||||||
|
setSelectedGateway(gateway.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback triggered when the user has filled and confirmed the settings of his gateway
|
* Callback triggered when the user has filled and confirmed the settings of his gateway
|
||||||
*/
|
*/
|
||||||
const onGatewayConfirmed = () => {
|
const onGatewayConfirmed = () => {
|
||||||
setPreventConfirmGateway(true);
|
setPreventConfirmGateway(true);
|
||||||
toggleModal();
|
updateSettings();
|
||||||
|
setPreventConfirmGateway(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,6 +77,25 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
|
|||||||
setPreventConfirmGateway(false);
|
setPreventConfirmGateway(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the new gateway settings to the API to save them
|
||||||
|
*/
|
||||||
|
const updateSettings = (): void => {
|
||||||
|
const settings = new Map<SettingName, string>(gatewayConfig);
|
||||||
|
settings.set(SettingName.PaymentGateway, selectedGateway);
|
||||||
|
|
||||||
|
const api = new SettingAPI();
|
||||||
|
api.bulkUpdate(settings).then(result => {
|
||||||
|
if (Array.from(result.values()).filter(item => !item.status).length > 0) {
|
||||||
|
onError(result);
|
||||||
|
} else {
|
||||||
|
onSuccess(result);
|
||||||
|
}
|
||||||
|
}, reason => {
|
||||||
|
onError(reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FabModal title={t('app.admin.invoices.payment.gateway_modal.select_gateway_title')}
|
<FabModal title={t('app.admin.invoices.payment.gateway_modal.select_gateway_title')}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@ -91,12 +120,12 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectGatewayModalWrapper: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, currentUser }) => {
|
const SelectGatewayModalWrapper: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, currentUser, onSuccess, onError }) => {
|
||||||
return (
|
return (
|
||||||
<Loader>
|
<Loader>
|
||||||
<SelectGatewayModal isOpen={isOpen} toggleModal={toggleModal} currentUser={currentUser} />
|
<SelectGatewayModal isOpen={isOpen} toggleModal={toggleModal} currentUser={currentUser} onSuccess={onSuccess} onError={onError} />
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Application.Components.component('selectGatewayModal', react2angular(SelectGatewayModalWrapper, ['isOpen', 'toggleModal', 'currentUser']));
|
Application.Components.component('selectGatewayModal', react2angular(SelectGatewayModalWrapper, ['isOpen', 'toggleModal', 'currentUser', 'onSuccess', 'onError']));
|
||||||
|
@ -44,8 +44,7 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys })
|
|||||||
/**
|
/**
|
||||||
* Send a test call to the Stripe API to check if the inputted public key is valid
|
* Send a test call to the Stripe API to check if the inputted public key is valid
|
||||||
*/
|
*/
|
||||||
const testPublicKey = (e: BaseSyntheticEvent) => {
|
const testPublicKey = (key: string) => {
|
||||||
const key = e.target.value;
|
|
||||||
if (!key.match(/^pk_/)) {
|
if (!key.match(/^pk_/)) {
|
||||||
setPublicKeyAddOn(<i className="fa fa-times" />);
|
setPublicKeyAddOn(<i className="fa fa-times" />);
|
||||||
setPublicKeyAddOnClassName('key-invalid');
|
setPublicKeyAddOnClassName('key-invalid');
|
||||||
@ -66,8 +65,7 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys })
|
|||||||
/**
|
/**
|
||||||
* Send a test call to the Stripe API to check if the inputted secret key is valid
|
* Send a test call to the Stripe API to check if the inputted secret key is valid
|
||||||
*/
|
*/
|
||||||
const testSecretKey = (e: BaseSyntheticEvent) => {
|
const testSecretKey = (key: string) => {
|
||||||
const key = e.target.value;
|
|
||||||
if (!key.match(/^sk_/)) {
|
if (!key.match(/^sk_/)) {
|
||||||
setSecretKeyAddOn(<i className="fa fa-times" />);
|
setSecretKeyAddOn(<i className="fa fa-times" />);
|
||||||
setSecretKeyAddOnClassName('key-invalid');
|
setSecretKeyAddOnClassName('key-invalid');
|
||||||
|
@ -698,6 +698,21 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
}, 50);
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered after the gateway was successfully configured in the dedicated modal
|
||||||
|
*/
|
||||||
|
$scope.onGatewayModalSuccess = function (settings) {
|
||||||
|
$scope.toggleSelectGatewayModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered after the gateway failed to be configured
|
||||||
|
*/
|
||||||
|
$scope.onGatewayModalError = function (errors) {
|
||||||
|
growl.error(_t('app.admin.invoices.payment.gateway_configuration_error'));
|
||||||
|
console.error(errors);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the feature-tour for the admin/invoices page.
|
* Setup the feature-tour for the admin/invoices page.
|
||||||
* This is intended as a contextual help (when pressing F1)
|
* This is intended as a contextual help (when pressing F1)
|
||||||
|
@ -101,7 +101,8 @@ export enum SettingName {
|
|||||||
UpcomingEventsShown = 'upcoming_events_shown',
|
UpcomingEventsShown = 'upcoming_events_shown',
|
||||||
PaymentSchedulePrefix = 'payment_schedule_prefix',
|
PaymentSchedulePrefix = 'payment_schedule_prefix',
|
||||||
TrainingsModule = 'trainings_module',
|
TrainingsModule = 'trainings_module',
|
||||||
AddressRequired = 'address_required'
|
AddressRequired = 'address_required',
|
||||||
|
PaymentGateway = 'payment_gateway'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Setting {
|
export interface Setting {
|
||||||
@ -110,3 +111,15 @@ export interface Setting {
|
|||||||
last_update: Date,
|
last_update: Date,
|
||||||
history: Array<HistoryValue>
|
history: Array<HistoryValue>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SettingError {
|
||||||
|
error: string,
|
||||||
|
id: number,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingBulkResult {
|
||||||
|
status: boolean,
|
||||||
|
value?: any,
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
@ -13,7 +13,11 @@
|
|||||||
on-before-save="selectPaymentGateway"
|
on-before-save="selectPaymentGateway"
|
||||||
fa-icon="fa-font">
|
fa-icon="fa-font">
|
||||||
</boolean-setting>
|
</boolean-setting>
|
||||||
<select-gateway-modal is-open="openSelectGatewayModal" toggle-modal="toggleSelectGatewayModal" current-user="currentUser" />
|
<select-gateway-modal is-open="openSelectGatewayModal"
|
||||||
|
toggle-modal="toggleSelectGatewayModal"
|
||||||
|
current-user="currentUser"
|
||||||
|
on-success="onGatewayModalSuccess"
|
||||||
|
on-error="onGatewayModalError" />
|
||||||
</div>
|
</div>
|
||||||
<div class="row m-t" ng-show="allSettings.online_payment_module === 'true'">
|
<div class="row m-t" ng-show="allSettings.online_payment_module === 'true'">
|
||||||
<h3 class="m-l" translate>{{ 'app.admin.invoices.payment.stripe_keys' }}</h3>
|
<h3 class="m-l" translate>{{ 'app.admin.invoices.payment.stripe_keys' }}</h3>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
json.settings @settings.each do |setting|
|
json.settings @settings.each do |setting|
|
||||||
if setting[:errors]
|
if setting.errors.keys.count.positive?
|
||||||
json.error setting.errors.full_messages
|
json.error setting.errors.full_messages
|
||||||
json.id setting.id
|
json.id setting.id
|
||||||
json.name setting.name
|
json.name setting.name
|
||||||
|
@ -643,6 +643,7 @@ en:
|
|||||||
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://stripe.com/docs/currencies' target='_blank'>Stripe supported currencies</a>."
|
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://stripe.com/docs/currencies' target='_blank'>Stripe supported currencies</a>."
|
||||||
currency_alert_html: "<strong>Warning</strong>: the currency cannot be changed after the first online payment was made. Please define this setting carefully before opening Fab-manager to your members."
|
currency_alert_html: "<strong>Warning</strong>: the currency cannot be changed after the first online payment was made. Please define this setting carefully before opening Fab-manager to your members."
|
||||||
stripe_currency: "Stripe currency"
|
stripe_currency: "Stripe currency"
|
||||||
|
gateway_configuration_error: "An error occurred while configuring the payment gateway."
|
||||||
# select a payment gateway
|
# select a payment gateway
|
||||||
gateway_modal:
|
gateway_modal:
|
||||||
select_gateway_title: "Select a payment gateway"
|
select_gateway_title: "Select a payment gateway"
|
||||||
|
@ -643,6 +643,7 @@ fr:
|
|||||||
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://stripe.com/docs/currencies' target='_blank'>devises supportées par Stripe</a>."
|
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://stripe.com/docs/currencies' target='_blank'>devises supportées par Stripe</a>."
|
||||||
currency_alert_html: "<strong>Attention</strong> : la devise ne peut pas être modifiée après que le premier paiement en ligne ait été effectué. Veuillez définir attentivement ce paramètre avant d'ouvrir Fab-manager à vos membres."
|
currency_alert_html: "<strong>Attention</strong> : la devise ne peut pas être modifiée après que le premier paiement en ligne ait été effectué. Veuillez définir attentivement ce paramètre avant d'ouvrir Fab-manager à vos membres."
|
||||||
stripe_currency: "Devise Stripe"
|
stripe_currency: "Devise Stripe"
|
||||||
|
gateway_configuration_error: "Une erreur est survenue lors de la configuration de la passerelle de paiement."
|
||||||
# select a payment gateway
|
# select a payment gateway
|
||||||
gateway_modal:
|
gateway_modal:
|
||||||
select_gateway_title: "Sélectionnez une passerelle de paiement"
|
select_gateway_title: "Sélectionnez une passerelle de paiement"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user