diff --git a/CHANGELOG.md b/CHANGELOG.md index 47930ad5a..8ff0dc77a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Fix a bug: unable to set date formats during installation - Fix a bug: unable to cancel the upgrade before it begins - Fix a bug: in the admin calendar, the trainings' info panel shows "duration: null minutes" +- Fix a bug: on the subscriptions page, not logged-in users do not see the action button - `SUPERADMIN_EMAIL` renamed to `ADMINSYS_EMAIL` - `scripts/run-tests.sh` renamed to `scripts/tests.sh` - [BREAKING CHANGE] GET `open_api/v1/invoices` won't return `stp_invoice_id` OR `stp_payment_intent_id` anymore. The new field `payment_gateway_object` will contain some similar data if the invoice was paid online by card. diff --git a/app/frontend/src/javascript/api/payzen.ts b/app/frontend/src/javascript/api/payzen.ts index 4d1c6966b..a055d66fc 100644 --- a/app/frontend/src/javascript/api/payzen.ts +++ b/app/frontend/src/javascript/api/payzen.ts @@ -4,11 +4,12 @@ import { ShoppingCart } from '../models/payment'; import { User } from '../models/user'; import { CheckHashResponse, - ConfirmPaymentResponse, CreatePaymentResponse, CreateTokenResponse, SdkTestResponse } from '../models/payzen'; +import { Invoice } from '../models/invoice'; +import { PaymentSchedule } from '../models/payment-schedule'; export default class PayzenAPI { @@ -32,8 +33,8 @@ export default class PayzenAPI { return res?.data; } - static async confirm(orderId: string, cart: ShoppingCart): Promise { - const res: AxiosResponse = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cart, order_id: orderId }); + static async confirm(orderId: string, cart: ShoppingCart): Promise { + const res: AxiosResponse = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cart, order_id: orderId }); return res?.data; } } diff --git a/app/frontend/src/javascript/api/stripe.ts b/app/frontend/src/javascript/api/stripe.ts index 4e7a000d0..d8061d4e8 100644 --- a/app/frontend/src/javascript/api/stripe.ts +++ b/app/frontend/src/javascript/api/stripe.ts @@ -1,10 +1,12 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; import { ShoppingCart, IntentConfirmation, PaymentConfirmation, UpdateCardResponse } from '../models/payment'; +import { PaymentSchedule } from '../models/payment-schedule'; +import { Invoice } from '../models/invoice'; export default class StripeAPI { - static async confirm (stp_payment_method_id: string, cart_items: ShoppingCart): Promise { - const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment`, { + static async confirm (stp_payment_method_id: string, cart_items: ShoppingCart): Promise { + const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment`, { payment_method_id: stp_payment_method_id, cart_items }); @@ -12,13 +14,12 @@ export default class StripeAPI { } static async setupIntent (user_id: number): Promise { - const res: AxiosResponse = await apiClient.get(`/api/stripe/setup_intent/${user_id}`); + const res: AxiosResponse = await apiClient.get(`/api/stripe/setup_intent/${user_id}`); return res?.data; } - // TODO, type the response - static async confirmPaymentSchedule (setup_intent_id: string, cart_items: ShoppingCart): Promise { - const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment_schedule`, { + static async confirmPaymentSchedule (setup_intent_id: string, cart_items: ShoppingCart): Promise { + const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment_schedule`, { setup_intent_id, cart_items }); @@ -26,7 +27,7 @@ export default class StripeAPI { } static async updateCard (user_id: number, stp_payment_method_id: string): Promise { - const res: AxiosResponse = await apiClient.post(`/api/stripe/update_card`, { + const res: AxiosResponse = await apiClient.post(`/api/stripe/update_card`, { user_id, payment_method_id: stp_payment_method_id, }); 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 a4fa4fc28..e95b460c3 100644 --- a/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx +++ b/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx @@ -12,13 +12,14 @@ import { User } from '../../models/user'; import CustomAssetAPI from '../../api/custom-asset'; import PriceAPI from '../../api/price'; import WalletAPI from '../../api/wallet'; +import { Invoice } from '../../models/invoice'; declare var Fablab: IFablab; export interface GatewayFormProps { onSubmit: () => void, - onSuccess: (result: any) => void, + onSuccess: (result: Invoice|PaymentSchedule) => void, onError: (message: string) => void, customer: User, operator: User, @@ -31,7 +32,7 @@ export interface GatewayFormProps { interface AbstractPaymentModalProps { isOpen: boolean, toggleModal: () => void, - afterSuccess: (result: any) => void, + afterSuccess: (result: Invoice|PaymentSchedule) => void, cart: ShoppingCart, currentUser: User, schedule: PaymentSchedule, @@ -137,7 +138,7 @@ export const AbstractPaymentModal: React.FC = ({ isOp /** * After sending the form with success, process the resulting payment method */ - const handleFormSuccess = async (result: any): Promise => { + const handleFormSuccess = async (result: Invoice|PaymentSchedule): Promise => { setSubmitState(false); afterSuccess(result); } diff --git a/app/frontend/src/javascript/components/payment/payment-modal.tsx b/app/frontend/src/javascript/components/payment/payment-modal.tsx index ccdb17dfe..9aa27aa3a 100644 --- a/app/frontend/src/javascript/components/payment/payment-modal.tsx +++ b/app/frontend/src/javascript/components/payment/payment-modal.tsx @@ -1,21 +1,24 @@ -import React, { ReactElement, ReactNode } from 'react'; +import React, { ReactElement } from 'react'; +import { react2angular } from 'react2angular'; +import { Loader } from '../base/loader'; +import { StripeModal } from './stripe/stripe-modal'; +import { PayZenModal } from './payzen/payzen-modal'; import { IApplication } from '../../models/application'; import { ShoppingCart } from '../../models/payment'; import { User } from '../../models/user'; import { PaymentSchedule } from '../../models/payment-schedule'; -import { Loader } from '../base/loader'; -import { react2angular } from 'react2angular'; -import SettingAPI from '../../api/setting'; import { SettingName } from '../../models/setting'; -import { StripeModal } from './stripe/stripe-modal'; -import { PayZenModal } from './payzen/payzen-modal'; +import { Invoice } from '../../models/invoice'; +import SettingAPI from '../../api/setting'; +import { useTranslation } from 'react-i18next'; declare var Application: IApplication; interface PaymentModalProps { isOpen: boolean, toggleModal: () => void, - afterSuccess: (result: any) => void, + afterSuccess: (result: Invoice|PaymentSchedule) => void, + onError: (message: string) => void, cart: ShoppingCart, currentUser: User, schedule: PaymentSchedule, @@ -29,7 +32,8 @@ 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 , cart, customer }) => { +const PaymentModal: React.FC = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule , cart, customer }) => { + const { t } = useTranslation('shared'); const gateway = paymentGateway.read(); /** @@ -66,19 +70,24 @@ const PaymentModal: React.FC = ({ isOpen, toggleModal, afterS return renderStripeModal(); case 'payzen': return renderPayZenModal(); + case null: + case undefined: + onError(t('app.shared.payment_modal.online_payment_disabled')); + return
; default: + onError(t('app.shared.payment_modal.unexpected_error')); console.error(`[PaymentModal] Unimplemented gateway: ${gateway.value}`); - return
+ return
; } } -const PaymentModalWrapper: React.FC = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cart, customer }) => { +const PaymentModalWrapper: React.FC = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule , cart, customer }) => { return ( - + ); } -Application.Components.component('paymentModal', react2angular(PaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess','currentUser', 'schedule', 'cart', 'customer'])); +Application.Components.component('paymentModal', react2angular(PaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer'])); 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 f848f3428..c7f4f5873 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,9 @@ import React, { FunctionComponent, ReactNode } from 'react'; import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal'; -import { ShoppingCart, PaymentConfirmation } from '../../../models/payment'; +import { ShoppingCart } from '../../../models/payment'; import { PaymentSchedule } from '../../../models/payment-schedule'; import { User } from '../../../models/user'; +import { Invoice } from '../../../models/invoice'; import payzenLogo from '../../../../../images/payzen-secure.png'; import mastercardLogo from '../../../../../images/mastercard.png'; @@ -13,7 +14,7 @@ import { PayzenForm } from './payzen-form'; interface PayZenModalProps { isOpen: boolean, toggleModal: () => void, - afterSuccess: (result: PaymentConfirmation) => void, + afterSuccess: (result: Invoice|PaymentSchedule) => void, cart: ShoppingCart, currentUser: User, schedule: PaymentSchedule, 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 890895b86..52b71f462 100644 --- a/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx @@ -1,20 +1,16 @@ import React, { FormEvent } from 'react'; import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'; -import { SetupIntent } from "@stripe/stripe-js"; import { useTranslation } from 'react-i18next'; import { GatewayFormProps } from '../abstract-payment-modal'; import { PaymentConfirmation } from '../../../models/payment'; import StripeAPI from '../../../api/stripe'; - -interface StripeFormProps extends GatewayFormProps { - onSuccess: (result: SetupIntent|PaymentConfirmation) => void, -} +import { Invoice } from '../../../models/invoice'; /** * 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 StripeForm: React.FC = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => { +export const StripeForm: React.FC = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => { const { t } = useTranslation('shared'); @@ -79,19 +75,17 @@ export const StripeForm: React.FC = ({ onSubmit, onSuccess, onE /** * Process the server response about the Strong-customer authentication (SCA) - * @param response can be a PaymentConfirmation, or a Reservation (if the reservation succeeded), or a Subscription (if the subscription succeeded) - * @see app/controllers/api/payments_controller.rb#on_reservation_success - * @see app/controllers/api/payments_controller.rb#on_subscription_success - * @see app/controllers/api/payments_controller.rb#generate_payment_response + * @param response can be a PaymentConfirmation, or an Invoice (if the payment succeeded) + * @see app/controllers/api/stripe_controller.rb#confirm_payment */ - const handleServerConfirmation = async (response: PaymentConfirmation|any) => { - if (response.error) { + const handleServerConfirmation = async (response: PaymentConfirmation|Invoice) => { + if ('error' in response) { if (response.error.statusText) { onError(response.error.statusText); } else { onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`); } - } else if (response.requires_action) { + } else if ('requires_action' in response) { // Use Stripe.js to handle required card action const result = await stripe.handleCardAction(response.payment_intent_client_secret); if (result.error) { @@ -106,8 +100,10 @@ export const StripeForm: React.FC = ({ onSubmit, onSuccess, onE onError(e); } } - } else { + } else if ('id' in response) { onSuccess(response); + } else { + console.error(`[StripeForm] unknown response received: ${response}`); } } 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 7161abc78..899d56868 100644 --- a/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx @@ -1,21 +1,21 @@ import React, { FunctionComponent, ReactNode } from 'react'; -import { SetupIntent } from '@stripe/stripe-js'; import { StripeElements } from './stripe-elements'; import { StripeForm } from './stripe-form'; import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal'; -import { ShoppingCart, PaymentConfirmation } from '../../../models/payment'; +import { ShoppingCart } from '../../../models/payment'; import { PaymentSchedule } from '../../../models/payment-schedule'; import { User } from '../../../models/user'; import stripeLogo from '../../../../../images/powered_by_stripe.png'; import mastercardLogo from '../../../../../images/mastercard.png'; import visaLogo from '../../../../../images/visa.png'; +import { Invoice } from '../../../models/invoice'; interface StripeModalProps { isOpen: boolean, toggleModal: () => void, - afterSuccess: (result: SetupIntent|PaymentConfirmation) => void, + afterSuccess: (result: Invoice|PaymentSchedule) => void, cart: ShoppingCart, currentUser: User, schedule: PaymentSchedule, diff --git a/app/frontend/src/javascript/components/plan-card.tsx b/app/frontend/src/javascript/components/plan-card.tsx index ab5956249..92de9615c 100644 --- a/app/frontend/src/javascript/components/plan-card.tsx +++ b/app/frontend/src/javascript/components/plan-card.tsx @@ -20,12 +20,13 @@ interface PlanCardProps { operator: User, isSelected: boolean, onSelectPlan: (plan: Plan) => void, + onLoginRequested: () => void, } /** * This component is a "card" (visually), publicly presenting the details of a plan and allowing a user to subscribe. */ -const PlanCard: React.FC = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected }) => { +const PlanCard: React.FC = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => { const { t } = useTranslation('public'); /** * Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €") @@ -46,6 +47,12 @@ const PlanCard: React.FC = ({ plan, userId, subscribedPlanId, ope const duration = (): string => { return moment.duration(plan.interval_count, plan.interval).humanize(); } + /** + * Check if no users are currently logged-in + */ + const mustLogin = (): boolean => { + return _.isNil(operator); + } /** * Check if the user can subscribe to the current plan, for himself */ @@ -88,6 +95,12 @@ const PlanCard: React.FC = ({ plan, userId, subscribedPlanId, ope const handleSelectPlan = (): void => { onSelectPlan(plan); } + /** + * Callback triggered when a visitor (not logged-in user) select a plan + */ + const handleLoginRequest = (): void => { + onLoginRequested(); + } return (

{plan.base_name}

@@ -108,12 +121,14 @@ const PlanCard: React.FC = ({ plan, userId, subscribedPlanId, ope
{hasDescription() &&
} {hasAttachment() && { t('app.public.plans.more_information') }} + {mustLogin() &&
+ +
} {canSubscribeForMe() &&
{!hasSubscribedToThisPlan() && } {hasSubscribedToThisPlan() &&
diff --git a/app/frontend/templates/shared/_cart.html b/app/frontend/templates/shared/_cart.html index 2e56a2dfa..35dc247f0 100644 --- a/app/frontend/templates/shared/_cart.html +++ b/app/frontend/templates/shared/_cart.html @@ -203,6 +203,7 @@