1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-17 06:52:27 +01:00

stripe keys form w/ live keys validation

This commit is contained in:
Sylvain 2021-03-30 11:26:47 +02:00
parent 5aacd6695c
commit 720328ee92
8 changed files with 145 additions and 53 deletions

View File

@ -0,0 +1,15 @@
import axios, { AxiosInstance } from 'axios'
function client(key: string): AxiosInstance {
return axios.create({
baseURL: 'https://api.stripe.com/v1/',
headers: {
common: {
Authorization: `Bearer ${key}`
}
}
});
}
export default client;

View File

@ -0,0 +1,29 @@
import stripeClient from './stripe-client';
import { AxiosResponse } from 'axios';
export default class StripeAPI {
/**
* @see https://stripe.com/docs/api/tokens/create_pii
*/
static async createPIIToken(key: string, piiId: string): Promise<any> {
const params = new URLSearchParams();
params.append('pii[id_number]', piiId);
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
const res: AxiosResponse = await stripeClient(key).post('tokens', params, config);
return res?.data;
}
/**
* @see https://stripe.com/docs/api/charges/list
*/
static async listAllCharges(key: string): Promise<any> {
const res: AxiosResponse = await stripeClient(key).get('charges');
return res?.data;
}
}

View File

@ -2,10 +2,10 @@
* This component is a template for a clickable button that wraps the application style
*/
import React, { ReactNode, SyntheticEvent } from 'react';
import React, { ReactNode, BaseSyntheticEvent } from 'react';
interface FabButtonProps {
onClick?: (event: SyntheticEvent) => void,
onClick?: (event: BaseSyntheticEvent) => void,
icon?: ReactNode,
className?: string,
disabled?: boolean,
@ -25,7 +25,7 @@ export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className,
/**
* Handle the action of the button
*/
const handleClick = (e: SyntheticEvent): void => {
const handleClick = (e: BaseSyntheticEvent): void => {
if (typeof onClick === 'function') {
onClick(e);
}

View File

@ -2,12 +2,12 @@
* This component is a template for an input component that wraps the application style
*/
import React, { ReactNode, SyntheticEvent, useCallback } from 'react';
import React, { BaseSyntheticEvent, ReactNode, useCallback, useState } from 'react';
import { debounce as _debounce } from 'lodash';
interface FabInputProps {
id: string,
onChange?: (event: SyntheticEvent) => void,
onChange?: (event: BaseSyntheticEvent) => void,
value: any,
icon?: ReactNode,
addOn?: ReactNode,
@ -21,6 +21,8 @@ interface FabInputProps {
export const FabInput: React.FC<FabInputProps> = ({ id, onChange, value, icon, className, disabled, type, required, debounce, addOn, addOnClassName }) => {
const [inputValue, setInputValue] = useState<any>(value);
/**
* Check if the current component was provided an icon to display
*/
@ -37,23 +39,27 @@ export const FabInput: React.FC<FabInputProps> = ({ id, onChange, value, icon, c
/**
* Debounced (ie. temporised) version of the 'on change' callback.
*
*/
const handler = useCallback(_debounce(onChange, debounce), []);
/**
* Handle the action of the button
*/
const handleChange = (e: SyntheticEvent): void => {
const handleChange = (e: BaseSyntheticEvent): void => {
setInputValue(e.target.value);
if (typeof onChange === 'function') {
handler(e);
if (debounce) {
handler(e);
} else {
onChange(e);
}
}
}
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={value} onChange={handleChange} disabled={disabled} required={required} />
<input id={id} type={type} className="fab-input--input" value={inputValue} onChange={handleChange} disabled={disabled} required={required} />
{hasAddOn() && <span className={`fab-input--addon ${addOnClassName ? addOnClassName : ''}`}>{addOn}</span>}
</div>
);

View File

@ -2,7 +2,7 @@
* This component is a template for a modal dialog that wraps the application style
*/
import React, { ReactNode, SyntheticEvent } from 'react';
import React, { ReactNode, BaseSyntheticEvent } from 'react';
import Modal from 'react-modal';
import { useTranslation } from 'react-i18next';
import { Loader } from './loader';
@ -27,7 +27,7 @@ interface FabModalProps {
className?: string,
width?: ModalSize,
customFooter?: ReactNode,
onConfirm?: (event: SyntheticEvent) => void,
onConfirm?: (event: BaseSyntheticEvent) => void,
preventConfirm?: boolean
}

View File

@ -12,6 +12,7 @@ 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';
declare var Application: IApplication;
@ -27,6 +28,7 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
const [preventConfirmGateway, setPreventConfirmGateway] = useState<boolean>(true);
const [selectedGateway, setSelectedGateway] = useState<string>('');
const [gatewayConfig, setGatewayConfig] = useState<Map<SettingName, string>>(new Map());
/**
@ -43,7 +45,6 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
const setGateway = (event: BaseSyntheticEvent) => {
const gateway = event.target.value;
setSelectedGateway(gateway);
setPreventConfirmGateway(!gateway);
}
/**
@ -53,6 +54,19 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
return selectedGateway !== '';
}
/**
* Callback triggered when the embedded form has validated all the stripe keys
*/
const handleValidStripeKeys = (publicKey: string, secretKey: string): void => {
setGatewayConfig((prev) => {
const newMap = new Map(prev);
newMap.set(SettingName.StripeSecretKey, secretKey);
newMap.set(SettingName.StripePublicKey, publicKey);
return newMap;
});
setPreventConfirmGateway(false);
}
return (
<FabModal title={t('app.admin.invoices.payment.gateway_modal.select_gateway_title')}
isOpen={isOpen}
@ -72,7 +86,7 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
<option value={Gateway.Stripe}>{t('app.admin.invoices.payment.gateway_modal.stripe')}</option>
<option value={Gateway.PayZen}>{t('app.admin.invoices.payment.gateway_modal.payzen')}</option>
</select>
{selectedGateway === Gateway.Stripe && <StripeKeysForm param={'lorem ipsum'} />}
{selectedGateway === Gateway.Stripe && <StripeKeysForm onValidKeys={handleValidStripeKeys} />}
</FabModal>
);
};

View File

@ -2,21 +2,22 @@
* Form to set the stripe's public and private keys
*/
import React, { ReactNode, useEffect, useState } from 'react';
import React, { BaseSyntheticEvent, ReactNode, useEffect, useState } from 'react';
import { Loader } from './loader';
import { useTranslation } from 'react-i18next';
import SettingAPI from '../api/setting';
import { SettingName } from '../models/setting';
import { FabInput } from './fab-input';
import StripeAPI from '../api/stripe';
interface StripeKeysFormProps {
param: string
onValidKeys: (stripePublic: string, stripeSecret:string) => void
}
const stripeKeys = SettingAPI.query([SettingName.StripePublicKey, SettingName.StripeSecretKey]);
const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ param }) => {
const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys }) => {
const { t } = useTranslation('admin');
const [publicKey, setPublicKey] = useState<string>('');
@ -32,13 +33,56 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ param }) => {
setSecretKey(keys.get(SettingName.StripeSecretKey));
}, []);
useEffect(() => {
const validClassName = 'key-valid';
if (publicKeyAddOnClassName === validClassName && secretKeyAddOnClassName === validClassName) {
onValidKeys(publicKey, secretKey);
}
}, [publicKeyAddOnClassName, secretKeyAddOnClassName]);
// see StripeKeysModalController
// from app/frontend/src/javascript/controllers/admin/invoices.js
const testPublicKey = () => {
setPublicKeyAddOnClassName('key-valid');
setPublicKeyAddOn(<i className="fa fa-check" />);
/**
* 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;
if (!key.match(/^pk_/)) {
setPublicKeyAddOn(<i className="fa fa-times" />);
setPublicKeyAddOnClassName('key-invalid');
return;
}
StripeAPI.createPIIToken(key, 'test').then(() => {
setPublicKey(key);
setPublicKeyAddOn(<i className="fa fa-check" />);
setPublicKeyAddOnClassName('key-valid');
}, reason => {
if (reason.response.status === 401) {
setPublicKeyAddOn(<i className="fa fa-times" />);
setPublicKeyAddOnClassName('key-invalid');
}
});
}
/**
* 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;
if (!key.match(/^sk_/)) {
setSecretKeyAddOn(<i className="fa fa-times" />);
setSecretKeyAddOnClassName('key-invalid');
return;
}
StripeAPI.listAllCharges(key).then(() => {
setSecretKey(key);
setSecretKeyAddOn(<i className="fa fa-check" />);
setSecretKeyAddOnClassName('key-valid');
}, reason => {
if (reason.response.status === 401) {
setSecretKeyAddOn(<i className="fa fa-times" />);
setSecretKeyAddOnClassName('key-invalid');
}
});
}
return (
@ -53,50 +97,29 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ param }) => {
onChange={testPublicKey}
addOn={publicKeyAddOn}
addOnClassName={publicKeyAddOnClassName}
debounce={200}
required />
<div className="key-input">
<span className="key-input__icon"><i className="fa fa-info" /></span>
<input type="text"
id="stripe_public_key"
value={publicKey}
ng-model-options='{ debounce: 200 }'
ng-change='testPublicKey()'
required />
<span className="input-group-addon"
ng-class="{'label-success': publicKeyStatus, 'label-danger text-white': !publicKeyStatus}"
ng-show="publicKeyStatus !== undefined && publicKey">
<i className="fa fa-times" ng-show="!publicKeyStatus" />
<i className="fa fa-check" ng-show="publicKeyStatus" />
</span>
</div>
</div>
<div className="stripe-secret-input">
<label htmlFor="stripe_secret_key">{ t('app.admin.invoices.payment.secret_key') } *</label>
<div className="key-input">
<span className="key-input__icon"><i className="fa fa-key" /></span>
<input type="text"
id="stripe_secret_key"
value={secretKey}
ng-model-options='{ debounce: 200 }'
ng-change='testSecretKey()'
required />
<span className="input-group-addon"
ng-class="{'label-success': secretKeyStatus, 'label-danger text-white': !secretKeyStatus}"
ng-show="secretKeyStatus !== undefined && secretKey">
<i className="fa fa-times" ng-show="!secretKeyStatus" />
<i className="fa fa-check" ng-show="secretKeyStatus" />
</span>
</div>
<FabInput id="stripe_secret_key"
icon={<i className="fa fa-key" />}
value={secretKey}
onChange={testSecretKey}
addOn={secretKeyAddOn}
addOnClassName={secretKeyAddOnClassName}
debounce={200}
required/>
</div>
</form>
</div>
);
}
export const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ param }) => {
export const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys }) => {
return (
<Loader>
<StripeKeysFormComponent param={param} />
<StripeKeysFormComponent onValidKeys={onValidKeys} />
</Loader>
);
}

View File

@ -18,5 +18,10 @@
.key-valid {
background-color: #7bca38;
}
.key-invalid {
background-color: #d92227;
color: white;
}
}
}