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:
parent
5aacd6695c
commit
720328ee92
15
app/frontend/src/javascript/api/stripe-client.ts
Normal file
15
app/frontend/src/javascript/api/stripe-client.ts
Normal 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;
|
||||
|
29
app/frontend/src/javascript/api/stripe.ts
Normal file
29
app/frontend/src/javascript/api/stripe.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -18,5 +18,10 @@
|
||||
.key-valid {
|
||||
background-color: #7bca38;
|
||||
}
|
||||
|
||||
.key-invalid {
|
||||
background-color: #d92227;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user