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])
|
||||
|
||||
res = PlansService.create(type, partner, plan_params)
|
||||
if res[:errors]
|
||||
render json: res[:errors], status: :unprocessable_entity
|
||||
if res.errors
|
||||
render json: res.errors, status: :unprocessable_entity
|
||||
else
|
||||
render json: res, status: :created
|
||||
end
|
||||
|
@ -32,8 +32,11 @@ class API::SettingsController < API::ApiController
|
||||
db_setting = Setting.find_or_initialize_by(name: setting[:name])
|
||||
next unless SettingService.before_update(db_setting)
|
||||
|
||||
db_setting.save && db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile)
|
||||
SettingService.after_update(db_setting)
|
||||
if db_setting.save
|
||||
db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile)
|
||||
SettingService.after_update(db_setting)
|
||||
end
|
||||
|
||||
@settings.push db_setting
|
||||
end
|
||||
end
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
PaymentSchedule,
|
||||
PaymentScheduleIndexRequest, RefreshItemResponse
|
||||
} from '../models/payment-schedule';
|
||||
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
|
||||
|
||||
export default class PaymentScheduleAPI {
|
||||
async list (query: PaymentScheduleIndexRequest): Promise<Array<PaymentSchedule>> {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import apiClient from './api-client';
|
||||
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';
|
||||
|
||||
export default class SettingAPI {
|
||||
@ -10,10 +10,18 @@ export default class SettingAPI {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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> {
|
||||
const api = new SettingAPI();
|
||||
return wrapPromise(api.get(name));
|
||||
@ -24,15 +32,41 @@ export default class SettingAPI {
|
||||
return wrapPromise(api.query(names));
|
||||
}
|
||||
|
||||
private
|
||||
|
||||
static toSettingsMap(data: Object): Map<SettingName, any> {
|
||||
private static toSettingsMap(data: Object): Map<SettingName, any> {
|
||||
const dataArray: Array<Array<string | any>> = Object.entries(data);
|
||||
const map = new Map();
|
||||
dataArray.forEach(item => {
|
||||
map.set(SettingName[item[0]], item[1]);
|
||||
map.set(item[0] as SettingName, item[1]);
|
||||
});
|
||||
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
|
||||
*/
|
||||
|
||||
import React, { BaseSyntheticEvent, ReactNode, useCallback, useState } from 'react';
|
||||
import React, { BaseSyntheticEvent, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import { debounce as _debounce } from 'lodash';
|
||||
import SettingAPI from '../api/setting';
|
||||
import { SettingName } from '../models/setting';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
|
||||
interface FabInputProps {
|
||||
id: string,
|
||||
onChange?: (event: BaseSyntheticEvent) => void,
|
||||
onChange?: (value: any) => void,
|
||||
value: any,
|
||||
icon?: ReactNode,
|
||||
addOn?: ReactNode,
|
||||
@ -19,10 +22,14 @@ interface FabInputProps {
|
||||
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 }) => {
|
||||
const [inputValue, setInputValue] = useState<any>(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
onChange(value);
|
||||
}, [value]);
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const handleChange = (e: BaseSyntheticEvent): void => {
|
||||
setInputValue(e.target.value);
|
||||
const newValue = e.target.value;
|
||||
setInputValue(newValue);
|
||||
if (typeof onChange === 'function') {
|
||||
if (debounce) {
|
||||
handler(e);
|
||||
handler(newValue);
|
||||
} else {
|
||||
onChange(e);
|
||||
onChange(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
* 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 { Loader } from './loader';
|
||||
import { IApplication } from '../models/application';
|
||||
@ -12,7 +12,8 @@ import { FabModal, ModalSize } from './fab-modal';
|
||||
import { User } from '../models/user';
|
||||
import { Gateway } from '../models/gateway';
|
||||
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;
|
||||
@ -21,22 +22,31 @@ interface SelectGatewayModalModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
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 [preventConfirmGateway, setPreventConfirmGateway] = useState<boolean>(true);
|
||||
const [selectedGateway, setSelectedGateway] = useState<string>('');
|
||||
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
|
||||
*/
|
||||
const onGatewayConfirmed = () => {
|
||||
setPreventConfirmGateway(true);
|
||||
toggleModal();
|
||||
updateSettings();
|
||||
setPreventConfirmGateway(false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,6 +77,25 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
|
||||
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 (
|
||||
<FabModal title={t('app.admin.invoices.payment.gateway_modal.select_gateway_title')}
|
||||
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 (
|
||||
<Loader>
|
||||
<SelectGatewayModal isOpen={isOpen} toggleModal={toggleModal} currentUser={currentUser} />
|
||||
<SelectGatewayModal isOpen={isOpen} toggleModal={toggleModal} currentUser={currentUser} onSuccess={onSuccess} onError={onError} />
|
||||
</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
|
||||
*/
|
||||
const testPublicKey = (e: BaseSyntheticEvent) => {
|
||||
const key = e.target.value;
|
||||
const testPublicKey = (key: string) => {
|
||||
if (!key.match(/^pk_/)) {
|
||||
setPublicKeyAddOn(<i className="fa fa-times" />);
|
||||
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
|
||||
*/
|
||||
const testSecretKey = (e: BaseSyntheticEvent) => {
|
||||
const key = e.target.value;
|
||||
const testSecretKey = (key: string) => {
|
||||
if (!key.match(/^sk_/)) {
|
||||
setSecretKeyAddOn(<i className="fa fa-times" />);
|
||||
setSecretKeyAddOnClassName('key-invalid');
|
||||
|
@ -698,6 +698,21 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
}, 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.
|
||||
* This is intended as a contextual help (when pressing F1)
|
||||
|
@ -101,7 +101,8 @@ export enum SettingName {
|
||||
UpcomingEventsShown = 'upcoming_events_shown',
|
||||
PaymentSchedulePrefix = 'payment_schedule_prefix',
|
||||
TrainingsModule = 'trainings_module',
|
||||
AddressRequired = 'address_required'
|
||||
AddressRequired = 'address_required',
|
||||
PaymentGateway = 'payment_gateway'
|
||||
}
|
||||
|
||||
export interface Setting {
|
||||
@ -110,3 +111,15 @@ export interface Setting {
|
||||
last_update: Date,
|
||||
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"
|
||||
fa-icon="fa-font">
|
||||
</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 class="row m-t" ng-show="allSettings.online_payment_module === 'true'">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.invoices.payment.stripe_keys' }}</h3>
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.settings @settings.each do |setting|
|
||||
if setting[:errors]
|
||||
if setting.errors.keys.count.positive?
|
||||
json.error setting.errors.full_messages
|
||||
json.id setting.id
|
||||
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_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"
|
||||
gateway_configuration_error: "An error occurred while configuring the payment gateway."
|
||||
# select a payment gateway
|
||||
gateway_modal:
|
||||
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_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"
|
||||
gateway_configuration_error: "Une erreur est survenue lors de la configuration de la passerelle de paiement."
|
||||
# select a payment gateway
|
||||
gateway_modal:
|
||||
select_gateway_title: "Sélectionnez une passerelle de paiement"
|
||||
|
Loading…
x
Reference in New Issue
Block a user