diff --git a/app/frontend/packs/application.js.erb b/app/frontend/packs/application.js.erb index 0c3ef4dd8..d4c1eb9b3 100644 --- a/app/frontend/packs/application.js.erb +++ b/app/frontend/packs/application.js.erb @@ -91,6 +91,7 @@ importAll(require.context('../src/javascript/controllers/', true, /.*/)); importAll(require.context('../src/javascript/services/', true, /.*/)); importAll(require.context('../src/javascript/directives/', true, /.*/)); importAll(require.context('../src/javascript/filters/', true, /.*/)); +importAll(require.context('../src/javascript/typings/', true, /.*/)); importAll(require.context('../images', true)); importAll(require.context('../templates', true)); diff --git a/app/frontend/src/javascript/components/stripe-form.tsx b/app/frontend/src/javascript/components/stripe-form.tsx new file mode 100644 index 000000000..10c785a75 --- /dev/null +++ b/app/frontend/src/javascript/components/stripe-form.tsx @@ -0,0 +1,64 @@ +import React, { FormEvent } from 'react'; +import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'; +import { PaymentMethod } from "@stripe/stripe-js"; + +interface StripeFormProps { + onSubmit: () => void, + onSuccess: (paymentMethod: PaymentMethod) => void, + onError: (message: string) => void, +} + +export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onError, children }) => { + + const stripe = useStripe(); + const elements = useElements(); + + /** + * Handle the submission of the form. Depending on the configuration, it will create the payment method on Stripe, + * or it will process a payment with the inputted card. + */ + const handleSubmit = async (event: FormEvent): Promise<void> => { + event.preventDefault(); + onSubmit(); + + // Stripe.js has not loaded yet + if (!stripe || !elements) { return; } + + const cardElement = elements.getElement(CardElement); + const { error, paymentMethod } = await stripe.createPaymentMethod({ + type: 'card', + card: cardElement, + }); + + if (error) { + onError(error.message); + } else { + onSuccess(paymentMethod); + } + } + + /** + * Options for the Stripe's card input + */ + const cardOptions = { + style: { + base: { + fontSize: '16px', + color: '#424770', + '::placeholder': { color: '#aab7c4' } + }, + invalid: { + color: '#9e2146', + iconColor: '#9e2146' + }, + }, + hidePostalCode: true + }; + + return ( + <form onSubmit={handleSubmit} id="stripe-form"> + <CardElement options={cardOptions} /> + {children} + </form> + ); +} diff --git a/app/frontend/src/javascript/components/stripe-modal.tsx b/app/frontend/src/javascript/components/stripe-modal.tsx index 90b5f209c..ef048479e 100644 --- a/app/frontend/src/javascript/components/stripe-modal.tsx +++ b/app/frontend/src/javascript/components/stripe-modal.tsx @@ -2,8 +2,7 @@ * This component enables the user to input his card data. */ -import React, { ChangeEvent, FormEvent, ReactNode, useState } from 'react'; -import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'; +import React, { ChangeEvent, ReactNode, useEffect, useState } from 'react'; import { react2angular } from 'react2angular'; import { Loader } from './loader'; import { IApplication } from '../models/application'; @@ -19,6 +18,12 @@ import CustomAssetAPI from '../api/custom-asset'; import { CustomAssetName } from '../models/custom-asset'; import { PaymentSchedule } from '../models/payment-schedule'; import { IFablab } from '../models/fablab'; +import WalletLib from '../lib/wallet'; +import { StripeForm } from './stripe-form'; + +import stripeLogo from '../../../images/powered_by_stripe.png'; +import mastercardLogo from '../../../images/mastercard.png'; +import visaLogo from '../../../images/visa.png'; declare var Application: IApplication; declare var Fablab: IFablab; @@ -31,16 +36,22 @@ interface StripeModalProps { currentUser: User, wallet: Wallet, price: number, - remainingPrice: number, schedule: PaymentSchedule } const cgvFile = CustomAssetAPI.get(CustomAssetName.CgvFile); -const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice, schedule }) => { +const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, schedule }) => { + const [remainingPrice, setRemainingPrice] = useState(0); + + /** + * Refresh the remaining price on each display + */ + useEffect(() => { + const wLib = new WalletLib(wallet); + setRemainingPrice(wLib.computeRemainingPrice(price)); + }) - const stripe = useStripe(); - const elements = useElements(); const { t } = useTranslation('shared'); const cgv = cgvFile.read(); @@ -49,32 +60,6 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc const [submitState, setSubmitState] = useState(false); const [tos, setTos] = useState(false); - /** - * Handle the submission of the form. Depending on the configuration, it will create the payment method on Stripe, - * or it will process a payment with the inputted card. - */ - const handleSubmit = async (event: FormEvent): Promise<void> => { - event.preventDefault(); - setSubmitState(true); - - // Stripe.js has not loaded yet - if (!stripe || !elements) { return; } - - const cardElement = elements.getElement(CardElement); - const { error, paymentMethod } = await stripe.createPaymentMethod({ - type: 'card', - card: cardElement, - }); - - setSubmitState(false); - if (error) { - setErrors(error.message); - } else { - setErrors(null); - afterSuccess(paymentMethod); - } - } - /** * Check if there is currently an error to display */ @@ -82,13 +67,6 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc return errors !== null; } - /** - * Change the state of the submit button: enabled/disabled - */ - const toggleSubmitButton = (): void => { - setSubmitState(!submitState); - } - /** * Check if the Terms of Sales document is set */ @@ -120,7 +98,6 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc const submitButton = (): ReactNode => { return ( <button type="submit" - onClick={toggleSubmitButton} disabled={submitState} form="stripe-form" className="validate-btn"> @@ -129,59 +106,60 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc ); } - /** - * Options for the Stripe's card input - */ - const cardOptions = { - style: { - base: { - fontSize: '16px', - color: '#424770', - '::placeholder': { color: '#aab7c4' } - }, - invalid: { - color: '#9e2146', - iconColor: '#9e2146' - }, - }, - hidePostalCode: true - }; + const handleSubmit = (): void => { + setSubmitState(true); + } + + const handleFormSuccess = (paymentMethod: PaymentMethod): void => { + setSubmitState(false); + afterSuccess(paymentMethod); + } + + const handleFormError = (message: string): void => { + setSubmitState(false); + setErrors(message); + } + return ( <div className="stripe-modal"> <FabModal title={t('app.shared.stripe.online_payment')} isOpen={isOpen} toggleModal={toggleModal} confirmButton={submitButton()}> + <WalletInfo reservation={reservation} currentUser={currentUser} wallet={wallet} price={price} /> <StripeElements> - <form onSubmit={handleSubmit} id="stripe-form"> - <WalletInfo reservation={reservation} currentUser={currentUser} wallet={wallet} price={price} remainingPrice={remainingPrice} /> - <CardElement options={cardOptions} /> - </form> + <StripeForm onSubmit={handleSubmit} onSuccess={handleFormSuccess} onError={handleFormError}> + {hasCgv() && <div className="terms-of-sales"> + <input type="checkbox" id="acceptToS" name="acceptCondition" checked={tos} onChange={toggleTos} required /> + <label htmlFor="acceptToS">{ t('app.shared.stripe.i_have_read_and_accept_') } + <a href={cgv.custom_asset_file_attributes.attachment_url} target="_blank"> + { t('app.shared.stripe._the_general_terms_and_conditions') } + </a> + </label> + </div>} + </StripeForm> </StripeElements> {hasErrors() && <div className="stripe-errors"> {errors} </div>} - {hasCgv() && <div className="terms-of-sales"> - <input type="checkbox" id="acceptToS" name="acceptCondition" checked={tos} onChange={toggleTos} required /> - </div>} {isPaymentSchedule() && <div className="payment-schedule-info"> <p>{ t('app.shared.stripe.payment_schedule', { DEADLINES: schedule.items.length }) }</p> </div>} <div className="stripe-modal-icons"> <i className="fa fa-lock fa-2x m-r-sm pos-rlt" /> - <img src="../../../images/powered_by_stripe.png" alt="powered by stripe" /> - <img src="../../../images/mastercard.png" alt="mastercard" /> - <img src="../../../images/visa.png" alt="visa" /> + <img src={stripeLogo} alt="powered by stripe" /> + <img src={mastercardLogo} alt="mastercard" /> + <img src={visaLogo} alt="visa" /> </div> </FabModal> </div> ); } -const StripeModalWrapper: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice, schedule }) => { +const StripeModalWrapper: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, schedule }) => { return ( <Loader> - <StripeModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} reservation={reservation} currentUser={currentUser} wallet={wallet} price={price} remainingPrice={remainingPrice} schedule={schedule} /> + <StripeModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} reservation={reservation} currentUser={currentUser} wallet={wallet} price={price} schedule={schedule} /> </Loader> ); } -Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'reservation', 'currentUser', 'wallet', 'price', 'remainingPrice', 'schedule'])); +Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'reservation', 'currentUser', 'wallet', 'price', 'schedule'])); diff --git a/app/frontend/src/javascript/components/wallet-info.tsx b/app/frontend/src/javascript/components/wallet-info.tsx index 1baabc177..ac772b72e 100644 --- a/app/frontend/src/javascript/components/wallet-info.tsx +++ b/app/frontend/src/javascript/components/wallet-info.tsx @@ -2,7 +2,7 @@ * This component displays a summary of the amount paid with the virtual wallet, for the current transaction */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { react2angular } from 'react2angular'; import { IApplication } from '../models/application'; @@ -12,6 +12,7 @@ import { Reservation } from '../models/reservation'; import { User } from '../models/user'; import { Wallet } from '../models/wallet'; import { IFablab } from '../models/fablab'; +import WalletLib from '../lib/wallet'; declare var Application: IApplication; declare var Fablab: IFablab; @@ -21,11 +22,19 @@ interface WalletInfoProps { currentUser: User, wallet: Wallet, price: number, - remainingPrice: number, } -export const WalletInfo: React.FC<WalletInfoProps> = ({reservation, currentUser, wallet, price, remainingPrice}) => { - const {t} = useTranslation('shared'); +export const WalletInfo: React.FC<WalletInfoProps> = ({reservation, currentUser, wallet, price}) => { + const { t } = useTranslation('shared'); + const [remainingPrice, setRemainingPrice] = useState(0); + + /** + * Refresh the remaining price on each display + */ + useEffect(() => { + const wLib = new WalletLib(wallet); + setRemainingPrice(wLib.computeRemainingPrice(price)); + }) /** * Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €") @@ -111,13 +120,12 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({reservation, currentUser, ); } -const WalletInfoWrapper: React.FC<WalletInfoProps> = ({currentUser, reservation, price, remainingPrice, wallet}) => { +const WalletInfoWrapper: React.FC<WalletInfoProps> = ({currentUser, reservation, price, wallet}) => { return ( <Loader> - <WalletInfo currentUser={currentUser} reservation={reservation} price={price} - remainingPrice={remainingPrice} wallet={wallet}/> + <WalletInfo currentUser={currentUser} reservation={reservation} price={price} wallet={wallet}/> </Loader> ); } -Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'remainingPrice', 'reservation', 'wallet'])); +Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'reservation', 'wallet'])); diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index 9fb27622a..c87d8e6d6 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -808,7 +808,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', */ $scope.ok = function () { if ($scope.method.payment_method === 'stripe') { - return payByStripe(reservation); + return $scope.toggleStripeModal(); } $scope.attempting = true; // save subscription (if there's only a subscription selected) diff --git a/app/frontend/src/javascript/lib/wallet.ts b/app/frontend/src/javascript/lib/wallet.ts new file mode 100644 index 000000000..57338f20d --- /dev/null +++ b/app/frontend/src/javascript/lib/wallet.ts @@ -0,0 +1,20 @@ +import { Wallet } from '../models/wallet'; + +export default class WalletLib { + private wallet: Wallet; + + constructor (wallet: Wallet) { + this.wallet = wallet; + } + + /** + * Return the price remaining to pay, after we have used the maximum possible amount in the wallet + */ + computeRemainingPrice = (price: number): number => { + if (this.wallet.amount > price) { + return 0; + } else { + return price - this.wallet.amount; + } + } +} diff --git a/app/frontend/src/javascript/typings/import-png.d.ts b/app/frontend/src/javascript/typings/import-png.d.ts new file mode 100644 index 000000000..6a81ab5d7 --- /dev/null +++ b/app/frontend/src/javascript/typings/import-png.d.ts @@ -0,0 +1,4 @@ +declare module "*.png" { + const value: any; + export default value; +} diff --git a/app/frontend/templates/shared/valid_reservation_modal.html b/app/frontend/templates/shared/valid_reservation_modal.html index caf38e405..6f0c21bc4 100644 --- a/app/frontend/templates/shared/valid_reservation_modal.html +++ b/app/frontend/templates/shared/valid_reservation_modal.html @@ -43,13 +43,18 @@ <wallet-info current-user="currentUser" reservation="reservation" price="price" - remaining-price="amount" wallet="wallet"/> </div> </div> <div class="modal-footer"> <button class="btn btn-info" ng-click="ok()" ng-disabled="attempting" ng-bind-html="validButtonName"></button> <button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button> - <!--TODO, continuer l'intégration de la modal --> - <stripe-modal is-open="isOpenStripeModal" toggle-modal="toggleStripeModal" after-success="afterCreatePaymentMethod" reservation="reservation" current-user="currentUser" wallet="wallet" price="" /> + <stripe-modal is-open="isOpenStripeModal" + toggle-modal="toggleStripeModal" + after-success="afterCreatePaymentMethod" + reservation="reservation" + current-user="currentUser" + wallet="wallet" + price="price" + schedule="schedule" /> </div> diff --git a/app/frontend/templates/stripe/payment_modal.html b/app/frontend/templates/stripe/payment_modal.html index 1198fe235..0d174d8ca 100644 --- a/app/frontend/templates/stripe/payment_modal.html +++ b/app/frontend/templates/stripe/payment_modal.html @@ -14,7 +14,6 @@ <wallet-info current-user="currentUser" reservation="reservation" price="price" - remaining-price="amount" wallet="wallet"/> </div> <div id="card-element"></div> diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index cbeab9acc..e0bf0ecbc 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -117,7 +117,7 @@ en: #stripe payment modal stripe: online_payment: "Online payment" - i_have_read_and_accept_: "I have read, and accept" + i_have_read_and_accept_: "I have read, and accept " _the_general_terms_and_conditions: "the general terms and conditions." credit_amount_for_pay_reservation: "{amount} {currency} remains to be paid to confirm your reservation" client_credit_amount_for_pay_reservation: "{amount} {currency} remains to be paid to confirm reservation of client" diff --git a/config/locales/app.shared.es.yml b/config/locales/app.shared.es.yml index 7ebf121ab..ec53f78a0 100644 --- a/config/locales/app.shared.es.yml +++ b/config/locales/app.shared.es.yml @@ -117,7 +117,7 @@ es: #stripe payment modal stripe: online_payment: "Online payment" - i_have_read_and_accept_: "He leido y acepto" + i_have_read_and_accept_: "He leido y acepto " _the_general_terms_and_conditions: "Los términos y condiciones." credit_amount_for_pay_reservation: "{amount} {currency} falta por pagar para efectuar su reserva" client_credit_amount_for_pay_reservation: "{amount} {currency} falta por pagar para efectuar la reserva del cliente" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index d89540fdb..a706d473a 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -117,7 +117,7 @@ fr: #stripe payment modal stripe: online_payment: "Paiement en ligne" - i_have_read_and_accept_: "J'ai bien pris connaissance, et accepte" + i_have_read_and_accept_: "J'ai bien pris connaissance, et accepte " _the_general_terms_and_conditions: "les conditions générales de vente." credit_amount_for_pay_reservation: "Il vous reste {amount} {currency} à payer pour valider votre réservation" client_credit_amount_for_pay_reservation: "Il reste {amount} {currency} à payer pour valider la réservation" diff --git a/config/locales/app.shared.pt.yml b/config/locales/app.shared.pt.yml index fa6c5f8e2..bd15eabf5 100755 --- a/config/locales/app.shared.pt.yml +++ b/config/locales/app.shared.pt.yml @@ -117,7 +117,7 @@ pt: #stripe payment modal stripe: online_payment: "Online payment" - i_have_read_and_accept_: "Eu li e aceito" + i_have_read_and_accept_: "Eu li e aceito " _the_general_terms_and_conditions: "os termos e condições." credit_amount_for_pay_reservation: "{amount} {currency} a ser pago para confirmar sua inscrição" client_credit_amount_for_pay_reservation: "{amount} {currency} a ser pago para confirmar a inscrição do cliente" diff --git a/config/webpack/environment.js b/config/webpack/environment.js index 9546e42f4..4240dac80 100644 --- a/config/webpack/environment.js +++ b/config/webpack/environment.js @@ -32,7 +32,6 @@ environment.loaders.prepend('js', js); environment.loaders.append('html', html); environment.loaders.append('sass', sass); environment.loaders.append('uiTour', uiTour); -environment.loaders.insert('foo', jsErb, { alter: 'bar' }); environment.splitChunks();