From 7d0abebc223397e498172d0311ae5abd2401f9a6 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 24 Nov 2020 16:26:18 +0100 Subject: [PATCH] WIP: stripe modal --- .../src/javascript/api/custom-asset.ts | 6 +- app/frontend/src/javascript/api/setting.ts | 16 ++- .../src/javascript/components/fab-modal.tsx | 3 +- .../javascript/components/stripe-elements.tsx | 6 +- .../javascript/components/stripe-modal.tsx | 103 +++++++++++++----- .../src/javascript/directives/cart.js | 18 +++ .../src/javascript/models/custom-asset.ts | 11 +- app/frontend/src/javascript/models/setting.ts | 103 +++++++++++++++++- .../shared/valid_reservation_modal.html | 2 + 9 files changed, 231 insertions(+), 37 deletions(-) diff --git a/app/frontend/src/javascript/api/custom-asset.ts b/app/frontend/src/javascript/api/custom-asset.ts index a069ab0ba..ecde60ab4 100644 --- a/app/frontend/src/javascript/api/custom-asset.ts +++ b/app/frontend/src/javascript/api/custom-asset.ts @@ -1,15 +1,15 @@ import apiClient from './api-client'; import { AxiosResponse } from 'axios'; -import { CustomAsset } from '../models/custom-asset'; +import { CustomAsset, CustomAssetName } from '../models/custom-asset'; import wrapPromise, { IWrapPromise } from '../lib/wrap-promise'; export default class CustomAssetAPI { - async get (name: string): Promise { + async get (name: CustomAssetName): Promise { const res: AxiosResponse = await apiClient.get(`/api/custom_assets/${name}`); return res?.data?.custom_asset; } - static get (name: string): IWrapPromise { + static get (name: CustomAssetName): IWrapPromise { const api = new CustomAssetAPI(); return wrapPromise(api.get(name)); } diff --git a/app/frontend/src/javascript/api/setting.ts b/app/frontend/src/javascript/api/setting.ts index fcdcb4c9b..2bea6bf1d 100644 --- a/app/frontend/src/javascript/api/setting.ts +++ b/app/frontend/src/javascript/api/setting.ts @@ -1,17 +1,27 @@ import apiClient from './api-client'; import { AxiosResponse } from 'axios'; -import { Setting } from '../models/setting'; +import { Setting, SettingName } from '../models/setting'; import wrapPromise, { IWrapPromise } from '../lib/wrap-promise'; export default class SettingAPI { - async get (name: string): Promise { + async get (name: SettingName): Promise { const res: AxiosResponse = await apiClient.get(`/api/settings/${name}`); return res?.data?.setting; } - static get (name: string): IWrapPromise { + async query (names: Array): Promise> { + const res: AxiosResponse = await apiClient.get(`/api/settings/?names=[${names.join(',')}]`); + return res?.data; + } + + static get (name: SettingName): IWrapPromise { const api = new SettingAPI(); return wrapPromise(api.get(name)); } + + static query(names: Array): IWrapPromise> { + const api = new SettingAPI(); + return wrapPromise(api.query(names)); + } } diff --git a/app/frontend/src/javascript/components/fab-modal.tsx b/app/frontend/src/javascript/components/fab-modal.tsx index 0f6154017..723dd31c1 100644 --- a/app/frontend/src/javascript/components/fab-modal.tsx +++ b/app/frontend/src/javascript/components/fab-modal.tsx @@ -7,6 +7,7 @@ import Modal from 'react-modal'; import { useTranslation } from 'react-i18next'; import { Loader } from './loader'; import CustomAssetAPI from '../api/custom-asset'; +import { CustomAssetName } from '../models/custom-asset'; Modal.setAppElement('body'); @@ -17,7 +18,7 @@ interface FabModalProps { confirmButton?: ReactNode } -const blackLogoFile = CustomAssetAPI.get('logo-black-file'); +const blackLogoFile = CustomAssetAPI.get(CustomAssetName.LogoBlackFile); export const FabModal: React.FC = ({ title, isOpen, toggleModal, children, confirmButton }) => { const { t } = useTranslation('shared'); diff --git a/app/frontend/src/javascript/components/stripe-elements.tsx b/app/frontend/src/javascript/components/stripe-elements.tsx index c2e76c3d7..81c733b9d 100644 --- a/app/frontend/src/javascript/components/stripe-elements.tsx +++ b/app/frontend/src/javascript/components/stripe-elements.tsx @@ -4,11 +4,11 @@ import React from 'react'; import { Elements } from '@stripe/react-stripe-js'; -import { IApplication } from '../models/application'; -import SettingAPI from '../api/setting'; import { loadStripe } from "@stripe/stripe-js"; +import SettingAPI from '../api/setting'; +import { SettingName } from '../models/setting'; -const stripePublicKey = SettingAPI.get('stripe_public_key'); +const stripePublicKey = SettingAPI.get(SettingName.StripePublicKey); export const StripeElements: React.FC = ({ children }) => { const publicKey = stripePublicKey.read(); diff --git a/app/frontend/src/javascript/components/stripe-modal.tsx b/app/frontend/src/javascript/components/stripe-modal.tsx index fef4c9f06..90b5f209c 100644 --- a/app/frontend/src/javascript/components/stripe-modal.tsx +++ b/app/frontend/src/javascript/components/stripe-modal.tsx @@ -1,8 +1,8 @@ /** - * This component enables the user to type his card data. + * This component enables the user to input his card data. */ -import React, { FormEvent, ReactNode, useState } from 'react'; +import React, { ChangeEvent, FormEvent, ReactNode, useState } from 'react'; import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'; import { react2angular } from 'react2angular'; import { Loader } from './loader'; @@ -15,8 +15,13 @@ import { WalletInfo } from './wallet-info'; import { Reservation } from '../models/reservation'; import { User } from '../models/user'; import { Wallet } from '../models/wallet'; +import CustomAssetAPI from '../api/custom-asset'; +import { CustomAssetName } from '../models/custom-asset'; +import { PaymentSchedule } from '../models/payment-schedule'; +import { IFablab } from '../models/fablab'; declare var Application: IApplication; +declare var Fablab: IFablab; interface StripeModalProps { isOpen: boolean, @@ -27,23 +32,30 @@ interface StripeModalProps { wallet: Wallet, price: number, remainingPrice: number, + schedule: PaymentSchedule } -const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice }) => { +const cgvFile = CustomAssetAPI.get(CustomAssetName.CgvFile); + +const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice, schedule }) => { const stripe = useStripe(); const elements = useElements(); const { t } = useTranslation('shared'); + const cgv = cgvFile.read(); + const [errors, setErrors] = useState(null); 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, + * 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 => { event.preventDefault(); + setSubmitState(true); // Stripe.js has not loaded yet if (!stripe || !elements) { return; } @@ -54,6 +66,7 @@ const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuc card: cardElement, }); + setSubmitState(false); if (error) { setErrors(error.message); } else { @@ -77,7 +90,32 @@ const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuc } /** - * Return the form submission button. This button will be shown into the modal footer + * Check if the Terms of Sales document is set + */ + const hasCgv = (): boolean => { + return cgv != null; + } + + const toggleTos = (event: ChangeEvent): void => { + setTos(!tos); + } + + /** + * Check if we are currently creating a payment schedule + */ + const isPaymentSchedule = (): boolean => { + return schedule !== undefined; + } + + /** + * Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €") + */ + const formatPrice = (amount: number): string => { + return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(amount); + } + + /** + * Return the form submission button. This button will be shown into the modal footer. */ const submitButton = (): ReactNode => { return ( @@ -86,49 +124,64 @@ const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuc disabled={submitState} form="stripe-form" className="validate-btn"> - {t('app.shared.buttons.confirm')} + {t('app.shared.stripe.confirm_payment_of_', { AMOUNT: formatPrice(remainingPrice) })} ); } + /** + * 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 (
- +
{hasErrors() &&
{errors}
} + {hasCgv() &&
+ +
} + {isPaymentSchedule() &&
+

{ t('app.shared.stripe.payment_schedule', { DEADLINES: schedule.items.length }) }

+
} +
+ + powered by stripe + mastercard + visa +
); } -const StripeModalWrapper: React.FC = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice }) => { +const StripeModalWrapper: React.FC = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice, schedule }) => { return ( - + ); } -Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'reservation', 'currentUser', 'wallet', 'price', 'remainingPrice'])); +Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'reservation', 'currentUser', 'wallet', 'price', 'remainingPrice', 'schedule'])); diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index 6a0933886..9fb27622a 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -799,6 +799,10 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', // "valid" Button label $scope.validButtonName = ''; + // stripe modal state + // this is used to collect card data when a payment-schedule was selected, and paid with a card + $scope.isOpenStripeModal = false; + /** * Callback to process the local payment, triggered on button click */ @@ -837,6 +841,20 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', */ $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + /** + * Asynchronously updates the status of the stripe modal + */ + $scope.toggleStripeModal = function () { + setTimeout(() => { + $scope.isOpenStripeModal = !$scope.isOpenStripeModal; + $scope.$apply(); + }, 50); + }; + + $scope.afterCreatePaymentMethod = function (a) { + console.log('TODO', a); + }; + /* PRIVATE SCOPE */ /** diff --git a/app/frontend/src/javascript/models/custom-asset.ts b/app/frontend/src/javascript/models/custom-asset.ts index 21db65406..be4c1cd63 100644 --- a/app/frontend/src/javascript/models/custom-asset.ts +++ b/app/frontend/src/javascript/models/custom-asset.ts @@ -1,6 +1,15 @@ +export enum CustomAssetName { + LogoFile = 'logo-file', + LogoBlackFile = 'logo-black-file', + CguFile = 'cgu-file', + CgvFile = 'cgv-file', + ProfileImageFile = 'profile-image-file', + FaviconFile = 'favicon-file' +} + export interface CustomAsset { id: number, - name: string, + name: CustomAssetName, custom_asset_file_attributes: { id: number, attachment: string diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index 87011b777..b42653b85 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -1,7 +1,108 @@ import { HistoryValue } from './history-value'; +export enum SettingName { + AboutTitle = 'about_title', + AboutBody = 'about_body', + AboutContacts = 'about_contacts', + PrivacyDraft = 'privacy_draft', + PrivacyBody = 'privacy_body', + PrivacyDpo = 'privacy_dpo', + TwitterName = 'twitter_name', + HomeBlogpost = 'home_blogpost', + MachineExplicationsAlert = 'machine_explications_alert', + TrainingExplicationsAlert = 'training_explications_alert', + TrainingInformationMessage = 'training_information_message', + SubscriptionExplicationsAlert = 'subscription_explications_alert', + InvoiceLogo = 'invoice_logo', + InvoiceReference = 'invoice_reference', + InvoiceCodeActive = 'invoice_code-active', + InvoiceCodeValue = 'invoice_code-value', + InvoiceOrderNb = 'invoice_order-nb', + InvoiceVATActive = 'invoice_VAT-active', + InvoiceVATRate = 'invoice_VAT-rate', + InvoiceText = 'invoice_text', + InvoiceLegals = 'invoice_legals', + BookingWindowStart = 'booking_window_start', + BookingWindowEnd = 'booking_window_end', + BookingSlotDuration = 'booking_slot_duration', + BookingMoveEnable = 'booking_move_enable', + BookingMoveDelay = 'booking_move_delay', + BookingCancelEnable = 'booking_cancel_enable', + BookingCancelDelay = 'booking_cancel_delay', + MainColor = 'main_color', + SecondaryColor = 'secondary_color', + FablabName = 'fablab_name', + NameGenre = 'name_genre', + ReminderEnable = 'reminder_enable', + ReminderDelay = 'reminder_delay', + EventExplicationsAlert = 'event_explications_alert', + SpaceExplicationsAlert = 'space_explications_alert', + VisibilityYearly = 'visibility_yearly', + VisibilityOthers = 'visibility_others', + DisplayNameEnable = 'display_name_enable', + MachinesSortBy = 'machines_sort_by', + AccountingJournalCode = 'accounting_journal_code', + AccountingCardClientCode = 'accounting_card_client_code', + AccountingCardClientLabel = 'accounting_card_client_label', + AccountingWalletClientCode = 'accounting_wallet_client_code', + AccountingWalletClientLabel = 'accounting_wallet_client_label', + AccountingOtherClientCode = 'accounting_other_client_code', + AccountingOtherClientLabel = 'accounting_other_client_label', + AccountingWalletCode = 'accounting_wallet_code', + AccountingWalletLabel = 'accounting_wallet_label', + AccountingVATCode = 'accounting_VAT_code', + AccountingVATLabel = 'accounting_VAT_label', + AccountingSubscriptionCode = 'accounting_subscription_code', + AccountingSubscriptionLabel = 'accounting_subscription_label', + AccountingMachineCode = 'accounting_Machine_code', + AccountingMachineLabel = 'accounting_Machine_label', + AccountingTrainingCode = 'accounting_Training_code', + AccountingTrainingLabel = 'accounting_Training_label', + AccountingEventCode = 'accounting_Event_code', + AccountingEventLabel = 'accounting_Event_label', + AccountingSpaceCode = 'accounting_Space_code', + AccountingSpaceLabel = 'accounting_Space_label', + HubLastVersion = 'hub_last_version', + HubPublicKey = 'hub_public_key', + FabAnalytics = 'fab_analytics', + LinkName = 'link_name', + HomeContent = 'home_content', + HomeCss = 'home_css', + Origin = 'origin', + Uuid = 'uuid', + PhoneRequired = 'phone_required', + TrackingId = 'tracking_id', + BookOverlappingSlots = 'book_overlapping_slots', + SlotDuration = 'slot_duration', + EventsInCalendar = 'events_in_calendar', + SpacesModule = 'spaces_module', + PlansModule = 'plans_module', + InvoicingModule = 'invoicing_module', + FacebookAppId = 'facebook_app_id', + TwitterAnalytics = 'twitter_analytics', + RecaptchaSiteKey = 'recaptcha_site_key', + RecaptchaSecretKey = 'recaptcha_secret_key', + FeatureTourDisplay = 'feature_tour_display', + EmailFrom = 'email_from', + DisqusShortname = 'disqus_shortname', + AllowedCadExtensions = 'allowed_cad_extensions', + AllowedCadMimeTypes = 'allowed_cad_mime_types', + OpenlabAppId = 'openlab_app_id', + OpenlabAppSecret = 'openlab_app_secret', + OpenlabDefault = 'openlab_default', + OnlinePaymentModule = 'online_payment_module', + StripePublicKey = 'stripe_public_key', + StripeSecretKey = 'stripe_secret_key', + StripeCurrency = 'stripe_currency', + InvoicePrefix = 'invoice_prefix', + ConfirmationRequired = 'confirmation_required', + WalletModule = 'wallet_module', + StatisticsModule = 'statistics_module', + UpcomingEventsShown = 'upcoming_events_shown' +} + export interface Setting { - name: string, + name: SettingName, value: string, last_update: Date, history: Array diff --git a/app/frontend/templates/shared/valid_reservation_modal.html b/app/frontend/templates/shared/valid_reservation_modal.html index b7b00a8ed..caf38e405 100644 --- a/app/frontend/templates/shared/valid_reservation_modal.html +++ b/app/frontend/templates/shared/valid_reservation_modal.html @@ -50,4 +50,6 @@