mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
WIP: using <stripe-modal> in cart directive
This commit is contained in:
parent
9813c5d27b
commit
4ca2299776
14
app/frontend/src/javascript/api/payment.ts
Normal file
14
app/frontend/src/javascript/api/payment.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import apiClient from './api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { CartItems, PaymentConfirmation } from '../models/payment';
|
||||
|
||||
export default class PaymentAPI {
|
||||
static async confirm (stp_payment_method_id: string, cart_items: CartItems): Promise<PaymentConfirmation> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payment/confirm`, {
|
||||
payment_method_id: stp_payment_method_id,
|
||||
cart_items
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
18
app/frontend/src/javascript/api/price.ts
Normal file
18
app/frontend/src/javascript/api/price.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import apiClient from './api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
|
||||
import { CartItems } from '../models/payment';
|
||||
import { ComputePriceResult } from '../models/price';
|
||||
|
||||
export default class PriceAPI {
|
||||
async compute (cartItems: CartItems): Promise<ComputePriceResult> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/prices/compute`, cartItems);
|
||||
return res?.data?.custom_asset;
|
||||
}
|
||||
|
||||
static compute (cartItems: CartItems): IWrapPromise<ComputePriceResult> {
|
||||
const api = new PriceAPI();
|
||||
return wrapPromise(api.compute(cartItems));
|
||||
}
|
||||
}
|
||||
|
17
app/frontend/src/javascript/api/wallet.ts
Normal file
17
app/frontend/src/javascript/api/wallet.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import apiClient from './api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
|
||||
import { Wallet } from '../models/wallet';
|
||||
|
||||
export default class WalletAPI {
|
||||
async getByUser (user_id: number): Promise<Wallet> {
|
||||
const res: AxiosResponse = await apiClient.get(`/api/wallet/by_user/${user_id}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static getByUser (user_id: number): IWrapPromise<Wallet> {
|
||||
const api = new WalletAPI();
|
||||
return wrapPromise(api.getByUser(user_id));
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,26 @@
|
||||
import React, { FormEvent } from 'react';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { PaymentMethod } from "@stripe/stripe-js";
|
||||
import PaymentAPI from '../api/payment';
|
||||
import { CartItems, PaymentConfirmation } from '../models/payment';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface StripeFormProps {
|
||||
onSubmit: () => void,
|
||||
onSuccess: (paymentMethod: PaymentMethod) => void,
|
||||
onError: (message: string) => void,
|
||||
className?: string,
|
||||
processPayment?: boolean,
|
||||
cartItems?: CartItems
|
||||
}
|
||||
|
||||
/**
|
||||
* 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="stripe-form".
|
||||
*/
|
||||
export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onError, children, className }) => {
|
||||
export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onError, children, className, processPayment , cartItems}) => {
|
||||
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
@ -37,11 +44,49 @@ export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onE
|
||||
|
||||
if (error) {
|
||||
onError(error.message);
|
||||
} else {
|
||||
if (processPayment) {
|
||||
// process the full payment pipeline, including SCA validation
|
||||
const res = await PaymentAPI.confirm(paymentMethod.id, cartItems);
|
||||
await handleServerConfirmation(res, paymentMethod);
|
||||
} else {
|
||||
// we don't want to process the payment, only return the payment method
|
||||
onSuccess(paymentMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the server response about the Strong-customer authentication (SCA)
|
||||
*/
|
||||
const handleServerConfirmation = async (response: PaymentConfirmation, paymentMethod: PaymentMethod) => {
|
||||
if (response.error) {
|
||||
if (response.error.statusText) {
|
||||
onError(response.error.statusText);
|
||||
} else {
|
||||
onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`);
|
||||
}
|
||||
} else if (response.requires_action) {
|
||||
// Use Stripe.js to handle required card action
|
||||
const result = await stripe.handleCardAction(response.payment_intent_client_secret);
|
||||
if (result.error) {
|
||||
onError(result.error.message);
|
||||
} else {
|
||||
// The card action has been handled
|
||||
// The PaymentIntent can be confirmed again on the server
|
||||
try {
|
||||
const confirmation = await PaymentAPI.confirm(result.paymentIntent.id, cartItems);
|
||||
await handleServerConfirmation(confirmation, paymentMethod);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onSuccess(paymentMethod);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Options for the Stripe's card input
|
||||
*/
|
||||
|
@ -1,5 +1,6 @@
|
||||
/**
|
||||
* This component enables the user to input his card data.
|
||||
* This component enables the user to input his card data or process payments.
|
||||
* Supports Strong-Customer Authentication (SCA).
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, ReactNode, useEffect, useState } from 'react';
|
||||
@ -11,9 +12,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { FabModal, ModalSize } from './fab-modal';
|
||||
import { PaymentMethod } from '@stripe/stripe-js';
|
||||
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';
|
||||
@ -24,6 +23,9 @@ 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';
|
||||
import { CartItems } from '../models/payment';
|
||||
import WalletAPI from '../api/wallet';
|
||||
import PriceAPI from '../api/price';
|
||||
|
||||
declare var Application: IApplication;
|
||||
declare var Fablab: IFablab;
|
||||
@ -32,34 +34,37 @@ interface StripeModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: (paymentMethod: PaymentMethod) => void,
|
||||
reservation: Reservation,
|
||||
cartItems: CartItems,
|
||||
currentUser: User,
|
||||
wallet: Wallet,
|
||||
price: number,
|
||||
schedule: PaymentSchedule
|
||||
schedule: PaymentSchedule,
|
||||
processPayment?: boolean,
|
||||
}
|
||||
|
||||
const cgvFile = CustomAssetAPI.get(CustomAssetName.CgvFile);
|
||||
|
||||
const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, schedule }) => {
|
||||
const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule , processPayment = true }) => {
|
||||
const [remainingPrice, setRemainingPrice] = useState(0);
|
||||
const userWallet = WalletAPI.getByUser(cartItems.reservation?.user_id || cartItems.subscription?.user_id);
|
||||
const priceInfo = PriceAPI.compute(cartItems);
|
||||
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const cgv = cgvFile.read();
|
||||
const wallet = userWallet.read();
|
||||
const price = priceInfo.read();
|
||||
|
||||
const [errors, setErrors] = useState(null);
|
||||
const [submitState, setSubmitState] = useState(false);
|
||||
const [tos, setTos] = useState(false);
|
||||
|
||||
/**
|
||||
* Refresh the remaining price on each display
|
||||
*/
|
||||
useEffect(() => {
|
||||
const wLib = new WalletLib(wallet);
|
||||
setRemainingPrice(wLib.computeRemainingPrice(price));
|
||||
setRemainingPrice(wLib.computeRemainingPrice(price.price));
|
||||
})
|
||||
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const cgv = cgvFile.read();
|
||||
|
||||
const [errors, setErrors] = useState(null);
|
||||
const [submitState, setSubmitState] = useState(false);
|
||||
const [tos, setTos] = useState(false);
|
||||
|
||||
/**
|
||||
* Check if there is currently an error to display
|
||||
*/
|
||||
@ -68,12 +73,15 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Terms of Sales document is set
|
||||
* Check if the user accepts the Terms of Sales document
|
||||
*/
|
||||
const hasCgv = (): boolean => {
|
||||
return cgv != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when the user accepts or declines the Terms of Sales
|
||||
*/
|
||||
const toggleTos = (event: ChangeEvent): void => {
|
||||
setTos(!tos);
|
||||
}
|
||||
@ -110,16 +118,32 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
setSubmitState(true);
|
||||
}
|
||||
|
||||
const handleFormSuccess = (paymentMethod: PaymentMethod): void => {
|
||||
/**
|
||||
* After sending the form with success, process the resulting payment method
|
||||
*/
|
||||
const handleFormSuccess = async (paymentMethod: PaymentMethod): Promise<void> => {
|
||||
setSubmitState(false);
|
||||
afterSuccess(paymentMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* When stripe-form raise an error, it is handled by this callback which display it in the modal.
|
||||
*/
|
||||
const handleFormError = (message: string): void => {
|
||||
setSubmitState(false);
|
||||
setErrors(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the form can be submitted.
|
||||
* => We're not currently already submitting the form, and, if the terms of service are enabled, the user agrees with them.
|
||||
*/
|
||||
const canSubmit = (): boolean => {
|
||||
let terms = true;
|
||||
if (hasCgv()) { terms = tos; }
|
||||
return !submitState && terms;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.shared.stripe.online_payment')}
|
||||
@ -129,9 +153,14 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
closeButton={false}
|
||||
customFooter={logoFooter()}
|
||||
className="stripe-modal">
|
||||
<WalletInfo reservation={reservation} currentUser={currentUser} wallet={wallet} price={price} />
|
||||
<WalletInfo reservation={cartItems.reservation} currentUser={currentUser} wallet={wallet} price={price.price} />
|
||||
<StripeElements>
|
||||
<StripeForm onSubmit={handleSubmit} onSuccess={handleFormSuccess} onError={handleFormError} className="stripe-form">
|
||||
<StripeForm onSubmit={handleSubmit}
|
||||
onSuccess={handleFormSuccess}
|
||||
onError={handleFormError}
|
||||
className="stripe-form"
|
||||
cartItems={cartItems}
|
||||
processPayment={processPayment}>
|
||||
{hasErrors() && <div className="stripe-errors">
|
||||
{errors}
|
||||
</div>}
|
||||
@ -149,7 +178,7 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
</div>}
|
||||
</StripeForm>
|
||||
<button type="submit"
|
||||
disabled={submitState}
|
||||
disabled={!canSubmit()}
|
||||
form="stripe-form"
|
||||
className="validate-btn">
|
||||
{t('app.shared.stripe.confirm_payment_of_', { AMOUNT: formatPrice(remainingPrice) })}
|
||||
@ -159,12 +188,12 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
);
|
||||
}
|
||||
|
||||
const StripeModalWrapper: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, schedule }) => {
|
||||
const StripeModalWrapper: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cartItems, processPayment}) => {
|
||||
return (
|
||||
<Loader>
|
||||
<StripeModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} reservation={reservation} currentUser={currentUser} wallet={wallet} price={price} schedule={schedule} />
|
||||
<StripeModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} currentUser={currentUser} schedule={schedule} processPayment={processPayment} cartItems={cartItems}/>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'reservation', 'currentUser', 'wallet', 'price', 'schedule']));
|
||||
Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess','currentUser', 'schedule', 'cartItems', 'processPayment']));
|
||||
|
@ -73,6 +73,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
payment_schedule: null // the effective computed payment schedule
|
||||
};
|
||||
|
||||
// online payments (stripe)
|
||||
$scope.stripe = {
|
||||
showModal: false,
|
||||
cartItems: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the provided slot to the shopping cart (state transition from free to 'about to be reserved')
|
||||
* and increment the total amount of the cart if needed.
|
||||
@ -303,6 +309,24 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
}, 50);
|
||||
};
|
||||
|
||||
/**
|
||||
* This will open/close the stripe payment modal
|
||||
*/
|
||||
$scope.toggleStripeModal = () => {
|
||||
setTimeout(() => {
|
||||
$scope.stripe.showModal = !$scope.stripe.showModal;
|
||||
$scope.$apply();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoked atfer a successful Stripe payment
|
||||
*/
|
||||
$scope.afterStripeSuccess = () => {
|
||||
$scope.toggleStripeModal();
|
||||
afterPayment($scope.reservation);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
@ -669,77 +693,8 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
* Open a modal window that allows the user to process a credit card payment for his current shopping cart.
|
||||
*/
|
||||
const payByStripe = function (reservation) {
|
||||
$uibModal.open({
|
||||
templateUrl: '/stripe/payment_modal.html',
|
||||
size: 'md',
|
||||
resolve: {
|
||||
reservation () {
|
||||
return reservation;
|
||||
},
|
||||
price () {
|
||||
return Price.compute(mkRequestParams({ reservation }, $scope.coupon.applied)).$promise;
|
||||
},
|
||||
wallet () {
|
||||
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
|
||||
},
|
||||
cgv () {
|
||||
return CustomAsset.get({ name: 'cgv-file' }).$promise;
|
||||
},
|
||||
coupon () {
|
||||
return $scope.coupon.applied;
|
||||
},
|
||||
cartItems () {
|
||||
let request = { reservation };
|
||||
if (reservation.slots_attributes.length === 0 && reservation.plan_id) {
|
||||
request = mkSubscription($scope.selectedPlan.id, reservation.user_id, $scope.schedule.requested_schedule, 'stripe');
|
||||
} else {
|
||||
request.reservation.payment_method = 'stripe';
|
||||
}
|
||||
return mkRequestParams(request, $scope.coupon.applied);
|
||||
},
|
||||
schedule () {
|
||||
return $scope.schedule.payment_schedule;
|
||||
},
|
||||
stripeKey: ['Setting', function (Setting) { return Setting.get({ name: 'stripe_public_key' }).$promise; }]
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', 'stripeKey', 'schedule',
|
||||
function ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, wallet, helpers, $filter, coupon, cartItems, stripeKey, schedule) {
|
||||
// user wallet amount
|
||||
$scope.wallet = wallet;
|
||||
|
||||
// Global price (total of all items)
|
||||
$scope.price = price.price;
|
||||
|
||||
// Price
|
||||
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount);
|
||||
|
||||
// Cart items
|
||||
$scope.cartItems = cartItems;
|
||||
|
||||
// CGV
|
||||
$scope.cgv = cgv.custom_asset;
|
||||
|
||||
// Reservation
|
||||
$scope.reservation = reservation;
|
||||
|
||||
// Used in wallet info template to interpolate some translations
|
||||
$scope.numberFilter = $filter('number');
|
||||
|
||||
// stripe publishable key
|
||||
$scope.stripeKey = stripeKey.setting.value;
|
||||
|
||||
// Shows the schedule info in the modal
|
||||
$scope.schedule = schedule;
|
||||
|
||||
/**
|
||||
* Callback to handle the post-payment and reservation
|
||||
*/
|
||||
$scope.onPaymentSuccess = function (response) {
|
||||
$uibModalInstance.close(response);
|
||||
};
|
||||
}
|
||||
]
|
||||
}).result.finally(null).then(function (reservation) { afterPayment(reservation); });
|
||||
$scope.stripe.cartItems = mkRequestParams({ reservation }, $scope.coupon.applied);
|
||||
$scope.toggleStripeModal();
|
||||
};
|
||||
/**
|
||||
* Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
|
||||
@ -755,6 +710,9 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
price () {
|
||||
return Price.compute(mkRequestParams({ reservation }, $scope.coupon.applied)).$promise;
|
||||
},
|
||||
cartItems () {
|
||||
return mkRequestParams({ reservation }, $scope.coupon.applied);
|
||||
},
|
||||
wallet () {
|
||||
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
|
||||
},
|
||||
@ -768,8 +726,8 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
return $scope.schedule;
|
||||
}
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule',
|
||||
function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, Subscription, wallet, helpers, $filter, coupon, selectedPlan, schedule) {
|
||||
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule', 'cartItems',
|
||||
function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, Subscription, wallet, helpers, $filter, coupon, selectedPlan, schedule, cartItems) {
|
||||
// user wallet amount
|
||||
$scope.wallet = wallet;
|
||||
|
||||
@ -779,8 +737,9 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
// Price to pay (wallet deducted)
|
||||
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount);
|
||||
|
||||
// Reservation
|
||||
// Reservation (simple & cartItems format)
|
||||
$scope.reservation = reservation;
|
||||
$scope.cartItems = cartItems;
|
||||
|
||||
// Subscription
|
||||
$scope.plan = selectedPlan;
|
||||
|
26
app/frontend/src/javascript/models/payment.ts
Normal file
26
app/frontend/src/javascript/models/payment.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Reservation } from './reservation';
|
||||
|
||||
export interface PaymentConfirmation {
|
||||
requires_action?: boolean,
|
||||
payment_intent_client_secret?: string,
|
||||
success?: boolean,
|
||||
error?: {
|
||||
statusText: string
|
||||
},
|
||||
}
|
||||
|
||||
export enum PaymentMethod {
|
||||
Stripe = 'stripe',
|
||||
Other = ''
|
||||
}
|
||||
|
||||
export interface CartItems {
|
||||
reservation: Reservation,
|
||||
subscription: {
|
||||
plan_id: number,
|
||||
user_id: number,
|
||||
payment_schedule: boolean,
|
||||
payment_method: PaymentMethod
|
||||
},
|
||||
coupon_code: string
|
||||
}
|
@ -1,8 +1,27 @@
|
||||
export interface Price {
|
||||
id: number,
|
||||
group_id: number,
|
||||
plan_id: number,
|
||||
priceable_type: string,
|
||||
priceable_id: number,
|
||||
amount: number
|
||||
id: number,
|
||||
group_id: number,
|
||||
plan_id: number,
|
||||
priceable_type: string,
|
||||
priceable_id: number,
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface ComputePriceResult {
|
||||
price: number,
|
||||
price_without_coupon: number,
|
||||
details?: {
|
||||
slots: Array<{
|
||||
start_at: Date,
|
||||
price: number,
|
||||
promo: boolean
|
||||
}>
|
||||
plan?: number
|
||||
},
|
||||
schedule?: {
|
||||
items: Array<{
|
||||
amount: number,
|
||||
due_date: Date
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface ReservationSlot {
|
||||
id?: number,
|
||||
start_at: Date,
|
||||
end_at: Date,
|
||||
availability_id: number,
|
||||
@ -10,6 +11,11 @@ export interface Reservation {
|
||||
reservable_id: number,
|
||||
reservable_type: string,
|
||||
slots_attributes: Array<ReservationSlot>,
|
||||
plan_id: number
|
||||
payment_schedule: boolean
|
||||
plan_id?: number,
|
||||
nb_reserve_places?: number,
|
||||
payment_schedule?: boolean,
|
||||
tickets_attributes?: {
|
||||
event_price_category_id: number,
|
||||
booked: boolean,
|
||||
},
|
||||
}
|
||||
|
@ -199,3 +199,11 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div ng-if="stripe.showModal">
|
||||
<stripe-modal is-open="stripe.showModal"
|
||||
toggle-modal="toggleStripeModal"
|
||||
after-success="afterStripeSuccess"
|
||||
cartItems="stripe.cartItems"
|
||||
current-user="currentUser"
|
||||
schedule="schedule.payment_schedule"/>
|
||||
</div>
|
||||
|
@ -52,9 +52,8 @@
|
||||
<stripe-modal is-open="isOpenStripeModal"
|
||||
toggle-modal="toggleStripeModal"
|
||||
after-success="afterCreatePaymentMethod"
|
||||
reservation="reservation"
|
||||
cart-items="cartItems"
|
||||
current-user="currentUser"
|
||||
wallet="wallet"
|
||||
price="price"
|
||||
schedule="schedule" />
|
||||
schedule="schedule"
|
||||
processPayment="false"/>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user