From fe5c4e6233bbed48342dad14ed49aa5e01221c60 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Fri, 9 Apr 2021 12:09:54 +0200 Subject: [PATCH] integrate the payzen form widget into the modal Also: do not fetch the api from unmounted components --- .../payment/abstract-payment-modal.tsx | 3 + .../components/payment/payment-modal.tsx | 4 + .../components/payment/payzen/payzen-form.tsx | 77 +++++++++++++++++++ .../payment/payzen/payzen-keys-form.tsx | 8 +- .../payment/payzen/payzen-modal.tsx | 23 +++--- .../payment/payzen/payzen-settings.tsx | 24 +++--- .../components/payment/stripe/stripe-form.tsx | 2 +- .../payment/stripe/stripe-keys-form.tsx | 12 +-- .../payment/stripe/stripe-modal.tsx | 7 +- .../src/stylesheets/modules/payzen-modal.scss | 5 ++ app/policies/setting_policy.rb | 3 +- 11 files changed, 130 insertions(+), 38 deletions(-) create mode 100644 app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx diff --git a/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx b/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx index 8881a16f5..eb1b182cc 100644 --- a/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx +++ b/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx @@ -49,6 +49,9 @@ const cgvFile = CustomAssetAPI.get(CustomAssetName.CgvFile); /** * This component is an abstract modal that must be extended by each payment gateway to include its payment form. + * + * This component must not be called directly but must be extended for each implemented payment gateway + * @see https://reactjs.org/docs/composition-vs-inheritance.html */ export const AbstractPaymentModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName }) => { // customer's wallet diff --git a/app/frontend/src/javascript/components/payment/payment-modal.tsx b/app/frontend/src/javascript/components/payment/payment-modal.tsx index 9a0c22b01..360f23203 100644 --- a/app/frontend/src/javascript/components/payment/payment-modal.tsx +++ b/app/frontend/src/javascript/components/payment/payment-modal.tsx @@ -25,6 +25,10 @@ interface PaymentModalProps { // initial request to the API const paymentGateway = SettingAPI.get(SettingName.PaymentGateway); +/** + * This component open a modal dialog for the configured payment gateway, allowing the user to input his card data + * to process an online payment. + */ const PaymentModal: React.FC = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cartItems, customer }) => { const gateway = paymentGateway.read(); diff --git a/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx new file mode 100644 index 000000000..b3bcb2341 --- /dev/null +++ b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx @@ -0,0 +1,77 @@ +import React, { FormEvent, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import KRGlue from "@lyracom/embedded-form-glue"; +import { CartItems } from '../../../models/payment'; +import { User } from '../../../models/user'; +import SettingAPI from '../../../api/setting'; +import { SettingName } from '../../../models/setting'; + +interface PayzenFormProps { + onSubmit: () => void, + onSuccess: (result: any) => void, + onError: (message: string) => void, + customer: User, + operator: User, + className?: string, + paymentSchedule?: boolean, + cartItems?: CartItems, + formId: string, +} + +/** + * A form component to collect the credit card details and to create the payment method on Stripe. + * The form validation button must be created elsewhere, using the attribute form={formId}. + */ +export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cartItems, customer, operator, formId }) => { + + const { t } = useTranslation('shared'); + + useEffect(() => { + const api = new SettingAPI(); + api.query([SettingName.PayZenEndpoint, SettingName.PayZenPublicKey]).then(settings => { + const formToken = "DEMO-TOKEN-TO-BE-REPLACED"; + + KRGlue.loadLibrary(settings.get(SettingName.PayZenEndpoint), settings.get(SettingName.PayZenPublicKey)) /* Load the remote library */ + .then(({ KR }) => + KR.setFormConfig({ + /* set the minimal configuration */ + formToken: formToken, + "kr-language": "en-US" /* to update initialization parameter */ + }) + ) + .then(({ KR }) => + KR.addForm("#payzenPaymentForm") + ) /* add a payment form to myPaymentForm div*/ + .then(({ KR, result }) => + KR.showForm(result.formId) + ); /* show the payment form */ + }).catch(error => console.error(error)); + }); + + /** + * Handle the submission of the form. + */ + const handleSubmit = async (event: FormEvent): Promise => { + event.preventDefault(); + onSubmit(); + + + try { + onSuccess(null); + } catch (err) { + // catch api errors + onError(err); + } + + } + + + return ( +
+
+
+
+ {children} + + ); +} diff --git a/app/frontend/src/javascript/components/payment/payzen/payzen-keys-form.tsx b/app/frontend/src/javascript/components/payment/payzen/payzen-keys-form.tsx index 6a83ad0d9..82a1a3c74 100644 --- a/app/frontend/src/javascript/components/payment/payzen/payzen-keys-form.tsx +++ b/app/frontend/src/javascript/components/payment/payzen/payzen-keys-form.tsx @@ -20,9 +20,6 @@ const payZenSettings: Array = [SettingName.PayZenUsername, SettingN // settings related the to PayZen REST API (server side) const restApiSettings: Array = [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. // this cannot be handled by a React state because of their asynchronous nature let pendingKeysValidation = false; @@ -48,7 +45,10 @@ const PayZenKeysFormComponent: React.FC = ({ onValidKeys }) * When the component loads for the first time, initialize the keys with the values fetched from the API (if any) */ useEffect(() => { - updateSettings(new Map(payZenKeys.read())); + const api = new SettingAPI(); + api.query(payZenSettings).then(payZenKeys => { + updateSettings(new Map(payZenKeys)); + }).catch(error => console.error(error)); }, []); /** diff --git a/app/frontend/src/javascript/components/payment/payzen/payzen-modal.tsx b/app/frontend/src/javascript/components/payment/payzen/payzen-modal.tsx index fc8d70775..5b824a566 100644 --- a/app/frontend/src/javascript/components/payment/payzen/payzen-modal.tsx +++ b/app/frontend/src/javascript/components/payment/payzen/payzen-modal.tsx @@ -1,8 +1,5 @@ import React, { FunctionComponent, ReactNode } from 'react'; -import { react2angular } from 'react2angular'; import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal'; -import { Loader } from '../../base/loader'; -import { IApplication } from '../../../models/application'; import { CartItems, PaymentConfirmation } from '../../../models/payment'; import { PaymentSchedule } from '../../../models/payment-schedule'; import { User } from '../../../models/user'; @@ -10,8 +7,8 @@ import { User } from '../../../models/user'; import payzenLogo from '../../../../../images/payzen-secure.png'; import mastercardLogo from '../../../../../images/mastercard.png'; import visaLogo from '../../../../../images/visa.png'; +import { PayzenForm } from './payzen-form'; -declare var Application: IApplication; interface PayZenModalProps { isOpen: boolean, @@ -26,6 +23,9 @@ interface PayZenModalProps { /** * This component enables the user to input his card data or process payments, using the PayZen gateway. * Supports Strong-Customer Authentication (SCA). + * + * This component should not be called directly. Prefer using which can handle the configuration + * of a different payment gateway. */ export const PayZenModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer }) => { /** @@ -46,13 +46,16 @@ export const PayZenModal: React.FC = ({ isOpen, toggleModal, a */ const renderForm: FunctionComponent = ({ onSubmit, onSuccess, onError, operator, className, formId, cartItems, customer, paymentSchedule, children}) => { return ( -
-

PayZen

- - Operated by {operator.name} - User: {customer.name} + {children} - +
); } diff --git a/app/frontend/src/javascript/components/payment/payzen/payzen-settings.tsx b/app/frontend/src/javascript/components/payment/payzen/payzen-settings.tsx index ceef03d24..1e65b0f9b 100644 --- a/app/frontend/src/javascript/components/payment/payzen/payzen-settings.tsx +++ b/app/frontend/src/javascript/components/payment/payzen/payzen-settings.tsx @@ -38,13 +38,6 @@ const icons:Map = new Map([ [SettingName.PayZenPublicKey, 'info'] ]) -// 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) -}; - /** * This component displays a summary of the PayZen account keys, with a button triggering the modal to edit them */ @@ -61,11 +54,18 @@ export const PayzenSettings: React.FC = ({ onEditKeys, onCu * For the private settings, we initialize them with the placeholder value, if the setting is set. */ useEffect(() => { - const map = new Map(payZenKeys.read()); - for (const setting of payZenPrivateSettings) { - map.set(setting, isPresent[setting].read() ? PAYZEN_HIDDEN : ''); - } - updateSettings(map); + const api = new SettingAPI(); + api.query(payZenPublicSettings.concat(payZenOtherSettings)).then(payZenKeys => { + api.isPresent(SettingName.PayZenPassword).then(pzPassword => { + api.isPresent(SettingName.PayZenHmacKey).then(pzHmac => { + const map = new Map(payZenKeys); + map.set(SettingName.PayZenPassword, pzPassword ? PAYZEN_HIDDEN : ''); + map.set(SettingName.PayZenHmacKey, pzHmac ? PAYZEN_HIDDEN : ''); + + updateSettings(map); + }).catch(error => { console.error(error); }) + }).catch(error => { console.error(error); }); + }).catch(error => { console.error(error); }); }, []); diff --git a/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx b/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx index 4b064284e..ffc78e5ea 100644 --- a/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx @@ -139,7 +139,7 @@ export const StripeForm: React.FC = ({ onSubmit, onSuccess, onE }; return ( -
+ {children} diff --git a/app/frontend/src/javascript/components/payment/stripe/stripe-keys-form.tsx b/app/frontend/src/javascript/components/payment/stripe/stripe-keys-form.tsx index 480551394..9cd434dff 100644 --- a/app/frontend/src/javascript/components/payment/stripe/stripe-keys-form.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/stripe-keys-form.tsx @@ -12,9 +12,6 @@ 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 */ @@ -44,9 +41,12 @@ const StripeKeysFormComponent: React.FC = ({ onValidKeys }) */ useEffect(() => { mounted.current = true; - const keys = stripeKeys.read(); - setPublicKey(keys.get(SettingName.StripePublicKey)); - setSecretKey(keys.get(SettingName.StripeSecretKey)); + + const api = new SettingAPI(); + api.query([SettingName.StripePublicKey, SettingName.StripeSecretKey]).then(stripeKeys => { + setPublicKey(stripeKeys.get(SettingName.StripePublicKey)); + setSecretKey(stripeKeys.get(SettingName.StripeSecretKey)); + }).catch(error => console.error(error)); // when the component unmounts, mark it as unmounted return () => { diff --git a/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx b/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx index 580b80f59..01a2990e1 100644 --- a/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx @@ -1,11 +1,8 @@ import React, { FunctionComponent, ReactNode } from 'react'; -import { react2angular } from 'react2angular'; import { SetupIntent } from '@stripe/stripe-js'; import { StripeElements } from './stripe-elements'; import { StripeForm } from './stripe-form'; import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal'; -import { Loader } from '../../base/loader'; -import { IApplication } from '../../../models/application'; import { CartItems, PaymentConfirmation } from '../../../models/payment'; import { PaymentSchedule } from '../../../models/payment-schedule'; import { User } from '../../../models/user'; @@ -14,7 +11,6 @@ import stripeLogo from '../../../../../images/powered_by_stripe.png'; import mastercardLogo from '../../../../../images/mastercard.png'; import visaLogo from '../../../../../images/visa.png'; -declare var Application: IApplication; interface StripeModalProps { isOpen: boolean, @@ -29,6 +25,9 @@ interface StripeModalProps { /** * This component enables the user to input his card data or process payments, using the Stripe gateway. * Supports Strong-Customer Authentication (SCA). + * + * This component should not be called directly. Prefer using which can handle the configuration + * of a different payment gateway. */ export const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer }) => { /** diff --git a/app/frontend/src/stylesheets/modules/payzen-modal.scss b/app/frontend/src/stylesheets/modules/payzen-modal.scss index 29388050d..26deeaf94 100644 --- a/app/frontend/src/stylesheets/modules/payzen-modal.scss +++ b/app/frontend/src/stylesheets/modules/payzen-modal.scss @@ -1,4 +1,9 @@ .payzen-modal { + .payzen-form { + .container { + display: flex; justify-content: center; + } + } .payzen-modal-icons { text-align: center; diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 01006b7e1..0fb0c2b98 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -38,7 +38,8 @@ class SettingPolicy < ApplicationPolicy fablab_name name_genre event_explications_alert space_explications_alert link_name home_content phone_required tracking_id book_overlapping_slots slot_duration events_in_calendar spaces_module plans_module invoicing_module recaptcha_site_key feature_tour_display disqus_shortname allowed_cad_extensions openlab_app_id openlab_default - online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required payment_gateway] + online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required + payment_gateway payzen_endpoint payzen_public_key] end ##