diff --git a/api/.env.docker.example b/api/.env.docker.example index ee2504f66..9f662f8dd 100644 --- a/api/.env.docker.example +++ b/api/.env.docker.example @@ -35,6 +35,8 @@ BC_MINIMUM_AGE=21 BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY BC_STRIPE_SESSION_EXPIRE_AT=82800 +BC_PAYPAL_CLIENT_ID=PAYPAL_CLIENT_ID +BC_PAYPAL_CLIENT_SECRET=PAYPAL_CLIENT_SECRET BC_ADMIN_EMAIL=admin@bookcars.ma BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET BC_WEBSITE_NAME=BookCars diff --git a/api/.env.example b/api/.env.example index 21245ea3b..d91a14e92 100644 --- a/api/.env.example +++ b/api/.env.example @@ -35,6 +35,8 @@ BC_MINIMUM_AGE=21 BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY BC_STRIPE_SESSION_EXPIRE_AT=82800 +BC_PAYPAL_CLIENT_ID=PAYPAL_CLIENT_ID +BC_PAYPAL_CLIENT_SECRET=PAYPAL_CLIENT_SECRET BC_ADMIN_EMAIL=admin@bookcars.ma BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET BC_WEBSITE_NAME=BookCars diff --git a/api/src/app.ts b/api/src/app.ts index 8e1bf661d..c2f1a41c3 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -15,6 +15,7 @@ import carRoutes from './routes/carRoutes' import userRoutes from './routes/userRoutes' import stripeRoutes from './routes/stripeRoutes' import countryRoutes from './routes/countryRoutes' +import paypalRoutes from './routes/paypalRoutes' import * as helper from './common/helper' const app = express() @@ -52,6 +53,7 @@ app.use('/', carRoutes) app.use('/', userRoutes) app.use('/', stripeRoutes) app.use('/', countryRoutes) +app.use('/', paypalRoutes) i18n.locale = env.DEFAULT_LANGUAGE diff --git a/api/src/common/helper.ts b/api/src/common/helper.ts index 9d1c2b1c8..5afd18829 100644 --- a/api/src/common/helper.ts +++ b/api/src/common/helper.ts @@ -263,3 +263,17 @@ export const validateAccessToken = async (socialSignInType: bookcarsTypes.Social return false } + +/** + * Format PayPal price. + * + * Example: + * 1 1.00 + * 1.2 1.20 + * 1.341 1.34 + * 1.345 1.35 + * + * @param {number} price + * @returns {string} + */ +export const formatPayPalPrice = (price: number) => (Math.round(price * 100) / 100).toFixed(2) diff --git a/api/src/config/env.config.ts b/api/src/config/env.config.ts index e65edf7b7..881306807 100644 --- a/api/src/config/env.config.ts +++ b/api/src/config/env.config.ts @@ -335,6 +335,20 @@ stripeSessionExpireAt = stripeSessionExpireAt <= 82800 ? stripeSessionExpireAt : */ export const STRIPE_SESSION_EXPIRE_AT = stripeSessionExpireAt +/** + * PayPal client ID. + * + * @type {string} + */ +export const PAYPAL_CLIENT_ID = __env__('BC_PAYPAL_CLIENT_ID', false, 'PAYPAL_CLIENT_ID') + +/** + * PayPal client secret. + * + * @type {string} + */ +export const PAYPAL_CLIENT_SECRET = __env__('BC_PAYPAL_CLIENT_SECRET', false, 'PAYPAL_CLIENT_SECRET') + /** * Booking expiration in seconds. * Bookings created from checkout with Stripe are temporary and are automatically deleted if the payment checkout session expires. @@ -485,6 +499,7 @@ export interface Booking extends Document { customerId?: string expireAt?: Date isDeposit: boolean + paypalOrderId?: string } /** diff --git a/api/src/config/paypalRoutes.config.ts b/api/src/config/paypalRoutes.config.ts new file mode 100644 index 000000000..ca7417ee0 --- /dev/null +++ b/api/src/config/paypalRoutes.config.ts @@ -0,0 +1,6 @@ +const routes = { + createPayPalOrder: '/api/create-paypal-order', + checkPayPalOrder: '/api/check-paypal-order/:bookingId/:orderId', +} + +export default routes diff --git a/api/src/controllers/bookingController.ts b/api/src/controllers/bookingController.ts index f685efdab..64d34112a 100644 --- a/api/src/controllers/bookingController.ts +++ b/api/src/controllers/bookingController.ts @@ -265,13 +265,15 @@ export const checkout = async (req: Request, res: Response) => { } if (!body.payLater) { - const { paymentIntentId, sessionId } = body + const { payPal, paymentIntentId, sessionId } = body - if (!paymentIntentId && !sessionId) { + if (!payPal && !paymentIntentId && !sessionId) { throw new Error('paymentIntentId and sessionId not found') } - body.booking.customerId = body.customerId + if (!payPal) { + body.booking.customerId = body.customerId + } if (paymentIntentId) { const paymentIntent = await stripeAPI.paymentIntents.retrieve(paymentIntentId) @@ -291,7 +293,7 @@ export const checkout = async (req: Request, res: Response) => { let expireAt = new Date() expireAt.setSeconds(expireAt.getSeconds() + env.BOOKING_EXPIRE_AT) - body.booking.sessionId = body.sessionId + body.booking.sessionId = !payPal ? body.sessionId : undefined body.booking.status = bookcarsTypes.BookingStatus.Void body.booking.expireAt = expireAt diff --git a/api/src/controllers/paypalController.ts b/api/src/controllers/paypalController.ts new file mode 100644 index 000000000..2fcff18e4 --- /dev/null +++ b/api/src/controllers/paypalController.ts @@ -0,0 +1,122 @@ +import { Request, Response } from 'express' +import * as paypal from '../paypal' +import i18n from '../lang/i18n' +import * as logger from '../common/logger' +import * as bookcarsTypes from ':bookcars-types' +import * as env from '../config/env.config' +import Booking from '../models/Booking' +import User from '../models/User' +import Car from '../models/Car' +import * as bookingController from './bookingController' + +export const createPayPalOrder = async (req: Request, res: Response) => { + try { + const { bookingId, amount, currency, name }: bookcarsTypes.CreatePayPalOrderPayload = req.body + + const orderId = await paypal.createOrder(bookingId, amount, currency, name) + + return res.json(orderId) + } catch (err) { + logger.error(`[paypal.createPayPalOrder] ${i18n.t('ERROR')}`, err) + return res.status(400).send(i18n.t('ERROR') + err) + } +} + +/** + * Check Paypal order and update booking if the payment succeeded. + * + * @async + * @param {Request} req + * @param {Response} res + * @returns {unknown} + */ +export const checkPayPalOrder = async (req: Request, res: Response) => { + try { + const { bookingId, orderId } = req.params + + // + // 1. Retrieve Checkout Sesssion and Booking + // + const booking = await Booking.findOne({ _id: bookingId, expireAt: { $ne: null } }) + if (!booking) { + const msg = `Booking with id ${bookingId} not found` + logger.info(`[paypal.checkPaypalOrder] ${msg}`) + return res.status(204).send(msg) + } + + let order + try { + order = await paypal.getOrder(orderId) + } catch (err) { + logger.error(`[paypal.checkPaypalOrder] retrieve paypal order error: ${orderId}`, err) + } + + if (!order) { + const msg = `Order ${order} not found` + logger.info(`[paypal.checkPaypalOrder] ${msg}`) + return res.status(204).send(msg) + } + + // + // 2. Update Booking if the payment succeeded + // (Set BookingStatus to Paid and remove expireAt TTL index) + // + if (order.status === 'APPROVED') { + const supplier = await User.findById(booking.supplier) + if (!supplier) { + throw new Error(`Supplier ${booking.supplier} not found`) + } + + booking.paypalOrderId = orderId + booking.expireAt = undefined + booking.status = booking.isDeposit ? bookcarsTypes.BookingStatus.Deposit : bookcarsTypes.BookingStatus.Paid + await booking.save() + + // Mark car as unavailable + // await Car.updateOne({ _id: booking.car }, { available: false }) + const car = await Car.findById(booking.car) + if (!car) { + throw new Error(`Car ${booking.car} not found`) + } + car.trips += 1 + await car.save() + + // Send confirmation email to customer + const user = await User.findById(booking.driver) + if (!user) { + throw new Error(`Driver ${booking.driver} not found`) + } + + user.expireAt = undefined + await user.save() + + if (!await bookingController.confirm(user, supplier, booking, false)) { + return res.sendStatus(400) + } + + // Notify supplier + i18n.locale = supplier.language + let message = i18n.t('BOOKING_PAID_NOTIFICATION') + await bookingController.notify(user, booking.id, supplier, message) + + // Notify admin + const admin = !!env.ADMIN_EMAIL && await User.findOne({ email: env.ADMIN_EMAIL, type: bookcarsTypes.UserType.Admin }) + if (admin) { + i18n.locale = admin.language + message = i18n.t('BOOKING_PAID_NOTIFICATION') + await bookingController.notify(user, booking.id, admin, message) + } + + return res.sendStatus(200) + } + + // + // 3. Delete Booking if the payment didn't succeed + // + await booking.deleteOne() + return res.status(400).send(order.status) + } catch (err) { + logger.error(`[paypal.checkPaypalOrder] ${i18n.t('ERROR')}`, err) + return res.status(400).send(i18n.t('ERROR') + err) + } +} diff --git a/api/src/models/Booking.ts b/api/src/models/Booking.ts index d6549068b..807f4695e 100644 --- a/api/src/models/Booking.ts +++ b/api/src/models/Booking.ts @@ -103,6 +103,9 @@ const bookingSchema = new Schema( type: Boolean, default: false, }, + paypalOrderId: { + type: String, + }, expireAt: { // // Bookings created from checkout with Stripe are temporary and diff --git a/api/src/paypal.ts b/api/src/paypal.ts new file mode 100644 index 000000000..09ef3376b --- /dev/null +++ b/api/src/paypal.ts @@ -0,0 +1,100 @@ +import axios from 'axios' +import * as env from './config/env.config' +import * as helper from './common/helper' + +export const getToken = async () => { + const res = await axios.post( + 'https://api-m.sandbox.paypal.com/v1/oauth2/token', + { grant_type: 'client_credentials' }, + { + auth: { + username: env.PAYPAL_CLIENT_ID, + password: env.PAYPAL_CLIENT_SECRET, + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ) + + return res.data.access_token +} + +export const createOrder = async (bookingId: string, amount: number, currency: string, name: string) => { + const price = helper.formatPayPalPrice(amount) + const token = await getToken() + const res = await axios.post( + 'https://api-m.sandbox.paypal.com/v2/checkout/orders', + { + intent: 'CAPTURE', + payment_source: { + paypal: { + experience_context: { + payment_method_preference: 'IMMEDIATE_PAYMENT_REQUIRED', + landing_page: 'LOGIN', + shipping_preference: 'GET_FROM_FILE', + user_action: 'PAY_NOW', + // return_url: `${helper.trimEnd(env.FRONTEND_HOST, '/')}/checkout-session/${bookingId}`, + // cancel_url: env.FRONTEND_HOST, + }, + }, + }, + purchase_units: [ + { + invoice_id: bookingId, + amount: { + currency_code: currency, + value: price, + breakdown: { + item_total: { + currency_code: currency, + value: price, + }, + }, + }, + items: [ + { + name, + description: name, + unit_amount: { + currency_code: currency, + value: price, + }, + quantity: '1', + // category: 'cars', + // sku: 'sku01', + // image_url: 'https://example.com/static/images/items/1/tshirt_green.jpg', + // url: 'https://example.com/url-to-the-item-being-purchased-1', + // upc: { + // type: 'UPC-A', + // code: '123456789012', + // }, + }, + ], + }, + ], + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }, + ) + + return res.data.id +} + +export const getOrder = async (orderId: string) => { + const token = await getToken() + const res = await axios.get( + `https://api-m.sandbox.paypal.com/v2/checkout/orders/${orderId}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ) + + return res.data +} diff --git a/api/src/routes/paypalRoutes.ts b/api/src/routes/paypalRoutes.ts new file mode 100644 index 000000000..b50f7ed91 --- /dev/null +++ b/api/src/routes/paypalRoutes.ts @@ -0,0 +1,10 @@ +import express from 'express' +import routeNames from '../config/paypalRoutes.config' +import * as paypalController from '../controllers/paypalController' + +const routes = express.Router() + +routes.route(routeNames.createPayPalOrder).post(paypalController.createPayPalOrder) +routes.route(routeNames.checkPayPalOrder).post(paypalController.checkPayPalOrder) + +export default routes diff --git a/frontend/.env.docker.example b/frontend/.env.docker.example index 5621caa2e..3aa79a10e 100644 --- a/frontend/.env.docker.example +++ b/frontend/.env.docker.example @@ -19,7 +19,9 @@ VITE_BC_CAR_IMAGE_WIDTH=300 VITE_BC_CAR_IMAGE_HEIGHT=200 VITE_BC_MINIMUM_AGE=21 VITE_BC_PAGINATION_MODE=classic +VITE_BC_PAYMENT_GATEWAY=Stripe VITE_BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY +VITE_BC_PAYPAL_CLIENT_ID=PAYPAL_CLIENT_ID VITE_BC_SET_LANGUAGE_FROM_IP=false VITE_BC_GOOGLE_ANALYTICS_ENABLED=false VITE_BC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX diff --git a/frontend/.env.example b/frontend/.env.example index ccada7944..0e75be320 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -20,7 +20,9 @@ VITE_BC_CAR_IMAGE_WIDTH=300 VITE_BC_CAR_IMAGE_HEIGHT=200 VITE_BC_MINIMUM_AGE=21 VITE_BC_PAGINATION_MODE=classic +VITE_BC_PAYMENT_GATEWAY=Stripe VITE_BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY +VITE_BC_PAYPAL_CLIENT_ID=PAYPAL_CLIENT_ID VITE_BC_SET_LANGUAGE_FROM_IP=false VITE_BC_GOOGLE_ANALYTICS_ENABLED=false VITE_BC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a3ee44bd7..109b4fa8a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@mui/material": "^6.4.1", "@mui/x-data-grid": "^7.24.1", "@mui/x-date-pickers": "^7.24.1", + "@paypal/react-paypal-js": "^8.8.1", "@stripe/react-stripe-js": "^3.1.1", "@stripe/stripe-js": "^5.5.0", "@types/leaflet": "^1.9.16", @@ -1795,6 +1796,38 @@ "node": ">= 8" } }, + "node_modules/@paypal/paypal-js": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@paypal/paypal-js/-/paypal-js-8.2.0.tgz", + "integrity": "sha512-hLg5wNORW3WiyMiRNJOm6cN2IqjPlClpxd971bEdm0LNpbbejQZYtesb0/0arTnySSbGcxg7MxjkZ/N5Z5qBNQ==", + "license": "Apache-2.0", + "dependencies": { + "promise-polyfill": "^8.3.0" + } + }, + "node_modules/@paypal/react-paypal-js": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@paypal/react-paypal-js/-/react-paypal-js-8.8.1.tgz", + "integrity": "sha512-Nz/1XkW71NbcV1RTp++9rICTV2xhnyue9iHPtO5ZGVjkwYaBkGeDO2WWumpt7TAFy1Eyh2reFGJ4NNEeYi/akw==", + "license": "Apache-2.0", + "dependencies": { + "@paypal/paypal-js": "^8.1.2", + "@paypal/sdk-constants": "^1.0.122" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19", + "react-dom": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/@paypal/sdk-constants": { + "version": "1.0.152", + "resolved": "https://registry.npmjs.org/@paypal/sdk-constants/-/sdk-constants-1.0.152.tgz", + "integrity": "sha512-B8Xhq79kbOsh1UmUFmPboDBuXqVm/JE+E3eCmrl+Z9i6QbjjjXDLw1MAIZQk62H3jj99c0VmvAH3QmZSYk0Lpw==", + "license": "Apache-2.0", + "dependencies": { + "hi-base32": "^0.5.0" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -5192,6 +5225,12 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hi-base32": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", + "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==", + "license": "MIT" + }, "node_modules/history": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", @@ -6731,6 +6770,12 @@ "node": ">= 0.8.0" } }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index aa780b63a..815dcaaaa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "@mui/material": "^6.4.1", "@mui/x-data-grid": "^7.24.1", "@mui/x-date-pickers": "^7.24.1", + "@paypal/react-paypal-js": "^8.8.1", "@stripe/react-stripe-js": "^3.1.1", "@stripe/stripe-js": "^5.5.0", "@types/leaflet": "^1.9.16", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 39dc2ad54..1be5380da 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import env from '@/config/env.config' import { GlobalProvider } from '@/context/GlobalContext' import { UserProvider } from '@/context/UserContext' import { RecaptchaProvider } from '@/context/RecaptchaContext' +import { PayPalProvider } from '@/context/PayPalContext' import { init as initGA } from '@/common/ga4' import ScrollToTop from '@/components/ScrollToTop' import NProgressIndicator from '@/components/NProgressIndicator' @@ -42,40 +43,43 @@ const App = () => ( - + + -
- }> -
+
+ }> +
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - } /> - - -
+ } /> + + +
+ +
diff --git a/frontend/src/common/helper.ts b/frontend/src/common/helper.ts index 43da02316..6c78855be 100644 --- a/frontend/src/common/helper.ts +++ b/frontend/src/common/helper.ts @@ -4,7 +4,7 @@ import * as bookcarsHelper from ':bookcars-helper' import { strings } from '@/lang/cars' import { strings as commonStrings } from '@/lang/common' import env from '@/config/env.config' -import * as StripeService from '@/services/StripeService' +import * as PaymentService from '@/services/PaymentService' import * as UserService from '@/services/UserService' /** @@ -284,7 +284,7 @@ export const getAdditionalDriver = async (additionalDriver: number, language: st } if (additionalDriver === 0) { return `${strings.ADDITIONAL_DRIVER}${fr ? ' : ' : ': '}${strings.INCLUDED}` } - const _additionalDriver = await StripeService.convertPrice(additionalDriver) + const _additionalDriver = await PaymentService.convertPrice(additionalDriver) return `${strings.ADDITIONAL_DRIVER}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_additionalDriver, commonStrings.CURRENCY, language)}${commonStrings.DAILY}` } @@ -303,7 +303,7 @@ export const getFullInsurance = async (fullInsurance: number, language: string) } if (fullInsurance === 0) { return `${strings.FULL_INSURANCE}${fr ? ' : ' : ': '}${strings.INCLUDED}${fr ? 'e' : ''}` } - const _fullInsurance = await StripeService.convertPrice(fullInsurance) + const _fullInsurance = await PaymentService.convertPrice(fullInsurance) return `${strings.FULL_INSURANCE}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_fullInsurance, commonStrings.CURRENCY, language)}${commonStrings.DAILY}` } @@ -322,7 +322,7 @@ export const getCollisionDamageWaiver = async (collisionDamageWaiver: number, la } if (collisionDamageWaiver === 0) { return `${strings.COLLISION_DAMAGE_WAVER}${fr ? ' : ' : ': '}${strings.INCLUDED}${fr ? 'e' : ''}` } - const _collisionDamageWaiver = await StripeService.convertPrice(collisionDamageWaiver) + const _collisionDamageWaiver = await PaymentService.convertPrice(collisionDamageWaiver) return `${strings.COLLISION_DAMAGE_WAVER}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_collisionDamageWaiver, commonStrings.CURRENCY, language)}${commonStrings.DAILY}` } @@ -341,7 +341,7 @@ export const getTheftProtection = async (theftProtection: number, language: stri } if (theftProtection === 0) { return `${strings.THEFT_PROTECTION}${fr ? ' : ' : ': '}${strings.INCLUDED}${fr ? 'e' : ''}` } - const _theftProtection = await StripeService.convertPrice(theftProtection) + const _theftProtection = await PaymentService.convertPrice(theftProtection) return `${strings.THEFT_PROTECTION}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_theftProtection, commonStrings.CURRENCY, language)}${commonStrings.DAILY}` } @@ -360,7 +360,7 @@ export const getAmendments = async (amendments: number, language: string) => { } if (amendments === 0) { return `${strings.AMENDMENTS}${fr ? ' : ' : ': '}${strings.INCLUDED}${fr ? 'es' : ''}` } - const _amendments = await StripeService.convertPrice(amendments) + const _amendments = await PaymentService.convertPrice(amendments) return `${strings.AMENDMENTS}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_amendments, commonStrings.CURRENCY, language)}` } @@ -379,7 +379,7 @@ export const getCancellation = async (cancellation: number, language: string) => } if (cancellation === 0) { return `${strings.CANCELLATION}${fr ? ' : ' : ': '}${strings.INCLUDED}${fr ? 'e' : ''}` } - const _cancellation = await StripeService.convertPrice(cancellation) + const _cancellation = await PaymentService.convertPrice(cancellation) return `${strings.CANCELLATION}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_cancellation, commonStrings.CURRENCY, language)}` } @@ -478,7 +478,7 @@ export const getCancellationOption = async (cancellation: number, language: stri } if (cancellation === 0) { return `${strings.INCLUDED}${fr ? 'e' : ''}` } - const _cancellation = await StripeService.convertPrice(cancellation) + const _cancellation = await PaymentService.convertPrice(cancellation) return `+ ${bookcarsHelper.formatPrice(_cancellation, commonStrings.CURRENCY, language)}` } @@ -497,7 +497,7 @@ export const getAmendmentsOption = async (amendments: number, language: string) } if (amendments === 0) { return `${strings.INCLUDED}${fr ? 'es' : ''}` } - const _amendments = await StripeService.convertPrice(amendments) + const _amendments = await PaymentService.convertPrice(amendments) return `+ ${bookcarsHelper.formatPrice(_amendments, commonStrings.CURRENCY, language)}` } @@ -517,7 +517,7 @@ export const getTheftProtectionOption = async (theftProtection: number, days: nu } if (theftProtection === 0) { return `${strings.INCLUDED}${fr ? 'e' : ''}` } - const _theftProtection = await StripeService.convertPrice(theftProtection) + const _theftProtection = await PaymentService.convertPrice(theftProtection) return `+ ${bookcarsHelper.formatPrice(_theftProtection * days, commonStrings.CURRENCY, language)} (${bookcarsHelper.formatPrice(_theftProtection, commonStrings.CURRENCY, language)}${commonStrings.DAILY})` } @@ -537,7 +537,7 @@ export const getCollisionDamageWaiverOption = async (collisionDamageWaiver: numb } if (collisionDamageWaiver === 0) { return `${strings.INCLUDED}${fr ? 'e' : ''}` } - const _collisionDamageWaiver = await StripeService.convertPrice(collisionDamageWaiver) + const _collisionDamageWaiver = await PaymentService.convertPrice(collisionDamageWaiver) return `+ ${bookcarsHelper.formatPrice(_collisionDamageWaiver * days, commonStrings.CURRENCY, language)} (${bookcarsHelper.formatPrice(collisionDamageWaiver, commonStrings.CURRENCY, language)}${commonStrings.DAILY})` } @@ -557,7 +557,7 @@ export const getFullInsuranceOption = async (fullInsurance: number, days: number } if (fullInsurance === 0) { return `${strings.INCLUDED}${fr ? 'e' : ''}` } - const _fullInsurance = await StripeService.convertPrice(fullInsurance) + const _fullInsurance = await PaymentService.convertPrice(fullInsurance) return `+ ${bookcarsHelper.formatPrice(_fullInsurance * days, commonStrings.CURRENCY, language)} (${bookcarsHelper.formatPrice(_fullInsurance, commonStrings.CURRENCY, language)}${commonStrings.DAILY})` } @@ -575,7 +575,7 @@ export const getAdditionalDriverOption = async (additionalDriver: number, days: } if (additionalDriver === 0) { return strings.INCLUDED } - const _additionalDriver = await StripeService.convertPrice(additionalDriver) + const _additionalDriver = await PaymentService.convertPrice(additionalDriver) return `+ ${bookcarsHelper.formatPrice(_additionalDriver * days, commonStrings.CURRENCY, language)} (${bookcarsHelper.formatPrice(_additionalDriver, commonStrings.CURRENCY, language)}${commonStrings.DAILY})` } diff --git a/frontend/src/components/BookingList.tsx b/frontend/src/components/BookingList.tsx index b26f2adcd..08e0bf56e 100644 --- a/frontend/src/components/BookingList.tsx +++ b/frontend/src/components/BookingList.tsx @@ -30,7 +30,7 @@ import { fr as dfnsFR, enUS as dfnsENUS } from 'date-fns/locale' import * as bookcarsTypes from ':bookcars-types' import * as bookcarsHelper from ':bookcars-helper' import * as BookingService from '@/services/BookingService' -import * as StripeService from '@/services/StripeService' +import * as PaymentService from '@/services/PaymentService' import * as helper from '@/common/helper' import { strings } from '@/lang/booking-list' import { strings as commonStrings } from '@/lang/common' @@ -128,7 +128,7 @@ const BookingList = ({ const totalRecords = Array.isArray(_data.pageInfo) && _data.pageInfo.length > 0 ? _data.pageInfo[0].totalRecords : 0 for (const booking of _data.resultData) { - booking.price = await StripeService.convertPrice(booking.price!) + booking.price = await PaymentService.convertPrice(booking.price!) } if (env.isMobile) { diff --git a/frontend/src/components/Car.tsx b/frontend/src/components/Car.tsx index 0fc91efec..9b3433e04 100644 --- a/frontend/src/components/Car.tsx +++ b/frontend/src/components/Car.tsx @@ -27,7 +27,7 @@ import { strings as commonStrings } from '@/lang/common' import { strings } from '@/lang/cars' import Badge from '@/components/Badge' import * as UserService from '@/services/UserService' -import * as StripeService from '@/services/StripeService' +import * as PaymentService from '@/services/PaymentService' import DoorsIcon from '@/assets/img/car-door.png' import DistanceIcon from '@/assets/img/distance-icon.png' @@ -85,7 +85,7 @@ const Car = ({ useEffect(() => { const fetchPrice = async () => { if (from && to) { - const _totalPrice = await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from as Date, to as Date)) + const _totalPrice = await PaymentService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from as Date, to as Date)) setTotalPrice(_totalPrice) setDays(bookcarsHelper.days(from, to)) } @@ -112,7 +112,7 @@ const Car = ({ setLoading(false) if (!hidePrice) { - const _totalPrice = await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from as Date, to as Date)) + const _totalPrice = await PaymentService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from as Date, to as Date)) setTotalPrice(_totalPrice) } // console.log('init car') diff --git a/frontend/src/components/CheckoutOptions.tsx b/frontend/src/components/CheckoutOptions.tsx index 1da702a1c..68377b012 100644 --- a/frontend/src/components/CheckoutOptions.tsx +++ b/frontend/src/components/CheckoutOptions.tsx @@ -12,7 +12,7 @@ import * as bookcarsHelper from ':bookcars-helper' import { strings as csStrings } from '@/lang/cars' import { strings } from '@/lang/checkout' import * as helper from '@/common/helper' -import * as StripeService from '@/services/StripeService' +import * as PaymentService from '@/services/PaymentService' import '@/assets/css/checkout-options.css' @@ -97,7 +97,7 @@ const CheckoutOptions = ({ fullInsurance, additionalDriver, } - const _price = await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) + const _price = await PaymentService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) setCancellation(_cancellation) onCancellationChange(_cancellation) @@ -116,7 +116,7 @@ const CheckoutOptions = ({ fullInsurance, additionalDriver, } - const _price = await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) + const _price = await PaymentService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) setAmendments(_amendments) onAmendmentsChange(_amendments) @@ -135,7 +135,7 @@ const CheckoutOptions = ({ fullInsurance, additionalDriver, } - const _price = await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) + const _price = await PaymentService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) setTheftProtection(_theftProtection) onTheftProtectionChange(_theftProtection) @@ -154,7 +154,7 @@ const CheckoutOptions = ({ fullInsurance, additionalDriver, } - const _price = await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) + const _price = await PaymentService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) setCollisionDamageWaiver(_collisionDamageWaiver) onCollisionDamageWaiverChange(_collisionDamageWaiver) @@ -173,7 +173,7 @@ const CheckoutOptions = ({ fullInsurance: _fullInsurance, additionalDriver, } - const _price = await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) + const _price = await PaymentService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) setFullInsurance(_fullInsurance) onFullInsuranceChange(_fullInsurance) @@ -192,7 +192,7 @@ const CheckoutOptions = ({ fullInsurance, additionalDriver: _additionalDriver, } - const _price = await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) + const _price = await PaymentService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from, to, options)) setAdditionalDriver(_additionalDriver) onAdditionalDriverChange(_additionalDriver) diff --git a/frontend/src/components/CheckoutStatus.tsx b/frontend/src/components/CheckoutStatus.tsx index 0b1c4ada9..0f9bd8558 100644 --- a/frontend/src/components/CheckoutStatus.tsx +++ b/frontend/src/components/CheckoutStatus.tsx @@ -6,7 +6,7 @@ import * as bookcarsTypes from ':bookcars-types' import * as bookcarsHelper from ':bookcars-helper' import * as helper from '@/common/helper' import * as BookingService from '@/services/BookingService' -import * as StripeService from '@/services/StripeService' +import * as PaymentService from '@/services/PaymentService' import { strings } from '@/lang/checkout-status' import { strings as commonStrings } from '@/lang/common' import { strings as checkoutStrings } from '@/lang/checkout' @@ -39,7 +39,7 @@ const CheckoutStatus = ( const init = async () => { const _booking = await BookingService.getBooking(bookingId) setBooking(_booking) - setPrice(await StripeService.convertPrice(_booking.price!)) + setPrice(await PaymentService.convertPrice(_booking.price!)) setLoading(false) } diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 5b8ab4cbe..b28996fb7 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -42,12 +42,12 @@ import { strings as commonStrings } from '@/lang/common' import { strings as suStrings } from '@/lang/sign-up' import { strings } from '@/lang/header' import * as UserService from '@/services/UserService' +import * as PaymentService from '@/services/PaymentService' import * as NotificationService from '@/services/NotificationService' import Avatar from './Avatar' import * as langHelper from '@/common/langHelper' import * as helper from '@/common/helper' import { useGlobalContext, GlobalContextType } from '@/context/GlobalContext' -import * as StripeService from '@/services/StripeService' import { useUserContext, UserContextType } from '@/context/UserContext' import { useInit } from '@/common/customHooks' @@ -247,10 +247,10 @@ const Header = ({ const { code } = event.currentTarget.dataset if (code) { - const currentCurrency = StripeService.getCurrency() + const currentCurrency = PaymentService.getCurrency() if (code && code !== currentCurrency) { - StripeService.setCurrency(code) + PaymentService.setCurrency(code) // Refresh page refreshPage() } @@ -529,7 +529,7 @@ const Header = ({
{isLoaded && !loading && ( )} {isLoaded && !loading && ( @@ -565,7 +565,7 @@ const Header = ({
{!loading && ( )} {!loading && ( diff --git a/frontend/src/config/env.config.ts b/frontend/src/config/env.config.ts index 9dabd1b92..53ebbb147 100644 --- a/frontend/src/config/env.config.ts +++ b/frontend/src/config/env.config.ts @@ -54,6 +54,19 @@ const CURRENCIES: Currency[] = [ }, ] +const getPaymentGateway = () => { + const paymentGateway = String(import.meta.env.VITE_BC_PAYMENT_GATEWAY || 'stripe').toUpperCase() + + if (paymentGateway === 'PAYPAL') { + return bookcarsTypes.PaymentGateway.PayPal + } + + // Default is Stripe + return bookcarsTypes.PaymentGateway.Stripe +} + +const PAYMENT_GATEWAY = getPaymentGateway() + const env = { isMobile: window.innerWidth <= 960, isProduction: import.meta.env.VITE_NODE_ENV === 'production', @@ -98,7 +111,9 @@ const env = { (import.meta.env.VITE_BC_PAGINATION_MODE && import.meta.env.VITE_BC_PAGINATION_MODE.toUpperCase()) === Const.PAGINATION_MODE.INFINITE_SCROLL ? Const.PAGINATION_MODE.INFINITE_SCROLL : Const.PAGINATION_MODE.CLASSIC, + PAYMENT_GATEWAY, STRIPE_PUBLISHABLE_KEY: String(import.meta.env.VITE_BC_STRIPE_PUBLISHABLE_KEY), + PAYPAL_CLIENT_ID: String(import.meta.env.VITE_BC_PAYPAL_CLIENT_ID), SET_LANGUAGE_FROM_IP: (import.meta.env.VITE_BC_SET_LANGUAGE_FROM_IP && import.meta.env.VITE_BC_SET_LANGUAGE_FROM_IP.toLowerCase()) === 'true', GOOGLE_ANALYTICS_ENABLED: (import.meta.env.VITE_BC_GOOGLE_ANALYTICS_ENABLED && import.meta.env.VITE_BC_GOOGLE_ANALYTICS_ENABLED.toLowerCase()) === 'true', GOOGLE_ANALYTICS_ID: String(import.meta.env.VITE_BC_GOOGLE_ANALYTICS_ID), diff --git a/frontend/src/context/PayPalContext.tsx b/frontend/src/context/PayPalContext.tsx new file mode 100644 index 000000000..31ec1be8e --- /dev/null +++ b/frontend/src/context/PayPalContext.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { PayPalScriptProvider } from '@paypal/react-paypal-js' +import * as bookcarsTypes from ':bookcars-types' +import env from '@/config/env.config' +import * as PaymentService from '@/services/PaymentService' +import * as PayPalService from '@/services/PayPalService' + +interface PayPalProviderProps { + children: React.ReactNode +} + +const PayPalProvider = ({ children }: PayPalProviderProps) => ( + env.PAYMENT_GATEWAY === bookcarsTypes.PaymentGateway.PayPal ? ( + + {children} + + ) : children +) + +export { PayPalProvider } diff --git a/frontend/src/lang/cars.ts b/frontend/src/lang/cars.ts index 8995ab557..f8f7613af 100644 --- a/frontend/src/lang/cars.ts +++ b/frontend/src/lang/cars.ts @@ -1,13 +1,13 @@ import LocalizedStrings from 'localized-strings' import * as langHelper from '@/common/langHelper' import env from '@/config/env.config' -import * as StripeService from '@/services/StripeService' +import * as PaymentService from '@/services/PaymentService' -const currency = StripeService.getCurrencySymbol() +const currency = PaymentService.getCurrencySymbol() -const depositFilterValue1 = await StripeService.convertPrice(env.DEPOSIT_FILTER_VALUE_1) -const depositFilterValue2 = await StripeService.convertPrice(env.DEPOSIT_FILTER_VALUE_2) -const depositFilterValue3 = await StripeService.convertPrice(env.DEPOSIT_FILTER_VALUE_3) +const depositFilterValue1 = await PaymentService.convertPrice(env.DEPOSIT_FILTER_VALUE_1) +const depositFilterValue2 = await PaymentService.convertPrice(env.DEPOSIT_FILTER_VALUE_2) +const depositFilterValue3 = await PaymentService.convertPrice(env.DEPOSIT_FILTER_VALUE_3) const strings = new LocalizedStrings({ fr: { @@ -79,9 +79,9 @@ const strings = new LocalizedStrings({ GEARBOX: 'Transmission', ENGINE: 'Moteur', DEPOSIT: 'Dépôt de garantie', - LESS_THAN_VALUE_1: `Moins de ${StripeService.currencyRTL() ? currency : ''}${depositFilterValue1}${!StripeService.currencyRTL() ? (` ${currency}`) : ''}`, - LESS_THAN_VALUE_2: `Moins de ${StripeService.currencyRTL() ? currency : ''}${depositFilterValue2}${!StripeService.currencyRTL() ? (` ${currency}`) : ''}`, - LESS_THAN_VALUE_3: `Moins de ${StripeService.currencyRTL() ? currency : ''}${depositFilterValue3}${!StripeService.currencyRTL() ? (` ${currency}`) : ''}`, + LESS_THAN_VALUE_1: `Moins de ${PaymentService.currencyRTL() ? currency : ''}${depositFilterValue1}${!PaymentService.currencyRTL() ? (` ${currency}`) : ''}`, + LESS_THAN_VALUE_2: `Moins de ${PaymentService.currencyRTL() ? currency : ''}${depositFilterValue2}${!PaymentService.currencyRTL() ? (` ${currency}`) : ''}`, + LESS_THAN_VALUE_3: `Moins de ${PaymentService.currencyRTL() ? currency : ''}${depositFilterValue3}${!PaymentService.currencyRTL() ? (` ${currency}`) : ''}`, TRIPS: 'locations', CO2: 'Effet CO2', FROM_YOU: ' de vous', @@ -166,9 +166,9 @@ const strings = new LocalizedStrings({ GEARBOX: 'Gearbox', ENGINE: 'Engine', DEPOSIT: 'Deposit at pick-up', - LESS_THAN_VALUE_1: `Less than ${StripeService.currencyRTL() ? currency : ''}${depositFilterValue1}${!StripeService.currencyRTL() ? (` ${currency}`) : ''}`, - LESS_THAN_VALUE_2: `Less than ${StripeService.currencyRTL() ? currency : ''}${depositFilterValue2}${!StripeService.currencyRTL() ? (` ${currency}`) : ''}`, - LESS_THAN_VALUE_3: `Less than ${StripeService.currencyRTL() ? currency : ''}${depositFilterValue3}${!StripeService.currencyRTL() ? (` ${currency}`) : ''}`, + LESS_THAN_VALUE_1: `Less than ${PaymentService.currencyRTL() ? currency : ''}${depositFilterValue1}${!PaymentService.currencyRTL() ? (` ${currency}`) : ''}`, + LESS_THAN_VALUE_2: `Less than ${PaymentService.currencyRTL() ? currency : ''}${depositFilterValue2}${!PaymentService.currencyRTL() ? (` ${currency}`) : ''}`, + LESS_THAN_VALUE_3: `Less than ${PaymentService.currencyRTL() ? currency : ''}${depositFilterValue3}${!PaymentService.currencyRTL() ? (` ${currency}`) : ''}`, TRIPS: 'trips', CO2: 'CO2 effect', FROM_YOU: ' from you', @@ -253,9 +253,9 @@ const strings = new LocalizedStrings({ GEARBOX: 'Caja de cambios', ENGINE: 'Motor', DEPOSIT: 'Depósito al recoger', - LESS_THAN_VALUE_1: `Menos de ${StripeService.currencyRTL() ? currency : ''}${depositFilterValue1}${!StripeService.currencyRTL() ? (` ${currency}`) : ''}`, - LESS_THAN_VALUE_2: `Menos de ${StripeService.currencyRTL() ? currency : ''}${depositFilterValue2}${!StripeService.currencyRTL() ? (` ${currency}`) : ''}`, - LESS_THAN_VALUE_3: `Menos de ${StripeService.currencyRTL() ? currency : ''}${depositFilterValue3}${!StripeService.currencyRTL() ? (` ${currency}`) : ''}`, + LESS_THAN_VALUE_1: `Menos de ${PaymentService.currencyRTL() ? currency : ''}${depositFilterValue1}${!PaymentService.currencyRTL() ? (` ${currency}`) : ''}`, + LESS_THAN_VALUE_2: `Menos de ${PaymentService.currencyRTL() ? currency : ''}${depositFilterValue2}${!PaymentService.currencyRTL() ? (` ${currency}`) : ''}`, + LESS_THAN_VALUE_3: `Menos de ${PaymentService.currencyRTL() ? currency : ''}${depositFilterValue3}${!PaymentService.currencyRTL() ? (` ${currency}`) : ''}`, TRIPS: 'viajes', CO2: 'Efecto CO2', FROM_YOU: ' de ti', diff --git a/frontend/src/lang/common.ts b/frontend/src/lang/common.ts index a0c6dd81f..3cc13d432 100644 --- a/frontend/src/lang/common.ts +++ b/frontend/src/lang/common.ts @@ -1,7 +1,7 @@ import LocalizedStrings from 'localized-strings' import env from '@/config/env.config' import * as langHelper from '@/common/langHelper' -import * as StripeService from '@/services/StripeService' +import * as PaymentService from '@/services/PaymentService' const strings = new LocalizedStrings({ fr: { @@ -32,7 +32,7 @@ const strings = new LocalizedStrings({ SAVE: 'Sauvegarder', CANCEL: 'Annuler', RESET_PASSWORD: 'Changer le mot de passe', - CURRENCY: StripeService.getCurrencySymbol(), + CURRENCY: PaymentService.getCurrencySymbol(), DAILY: '/jour', DELETE_AVATAR_CONFIRM: 'Êtes-vous sûr de vouloir supprimer la photo ?', DELETE_IMAGE: "Supprimer l'image", @@ -118,7 +118,7 @@ const strings = new LocalizedStrings({ SAVE: 'Save', CANCEL: 'Cancel', RESET_PASSWORD: 'Change Password', - CURRENCY: StripeService.getCurrencySymbol(), + CURRENCY: PaymentService.getCurrencySymbol(), DAILY: '/day', DELETE_AVATAR_CONFIRM: 'Are you sure you want to delete the picture?', UPLOAD_IMAGE: 'Upload image', @@ -204,7 +204,7 @@ const strings = new LocalizedStrings({ SAVE: 'Guardar', CANCEL: 'Cancelar', RESET_PASSWORD: 'Cambiar contraseña', - CURRENCY: StripeService.getCurrencySymbol(), + CURRENCY: PaymentService.getCurrencySymbol(), DAILY: '/día', DELETE_AVATAR_CONFIRM: '¿Está seguro de que desea eliminar la foto?', DELETE_IMAGE: 'Eliminar imagen', diff --git a/frontend/src/pages/Booking.tsx b/frontend/src/pages/Booking.tsx index 4b5454407..e2608082f 100644 --- a/frontend/src/pages/Booking.tsx +++ b/frontend/src/pages/Booking.tsx @@ -18,7 +18,7 @@ import Layout from '@/components/Layout' import * as UserService from '@/services/UserService' import * as BookingService from '@/services/BookingService' import * as CarService from '@/services/CarService' -import * as StripeService from '@/services/StripeService' +import * as PaymentService from '@/services/PaymentService' import Backdrop from '@/components/SimpleBackdrop' import NoMatch from './NoMatch' import Error from './Error' @@ -256,7 +256,7 @@ const Booking = () => { const _booking = await BookingService.getBooking(id) if (_booking) { setBooking(_booking) - setPrice(await StripeService.convertPrice(_booking.price!)) + setPrice(await PaymentService.convertPrice(_booking.price!)) setLoading(false) setVisible(true) const cmp = _booking.supplier as bookcarsTypes.User diff --git a/frontend/src/pages/Checkout.tsx b/frontend/src/pages/Checkout.tsx index 9be19528f..79164a6f8 100644 --- a/frontend/src/pages/Checkout.tsx +++ b/frontend/src/pages/Checkout.tsx @@ -29,6 +29,7 @@ import { EmbeddedCheckout, } from '@stripe/react-stripe-js' import { loadStripe } from '@stripe/stripe-js' +import { PayPalButtons } from '@paypal/react-paypal-js' import CarList from '@/components/CarList' import * as bookcarsTypes from ':bookcars-types' import * as bookcarsHelper from ':bookcars-helper' @@ -41,7 +42,9 @@ import * as helper from '@/common/helper' import * as UserService from '@/services/UserService' import * as CarService from '@/services/CarService' import * as LocationService from '@/services/LocationService' +import * as PaymentService from '@/services/PaymentService' import * as StripeService from '@/services/StripeService' +import * as PayPalService from '@/services/PayPalService' import { useRecaptchaContext, RecaptchaContextType } from '@/context/RecaptchaContext' import Layout from '@/components/Layout' import Error from '@/components/Error' @@ -56,6 +59,7 @@ import CheckoutOptions from '@/components/CheckoutOptions' import Footer from '@/components/Footer' import ViewOnMapButton from '@/components/ViewOnMapButton' import MapDialog from '@/components/MapDialog' +import Backdrop from '@/components/SimpleBackdrop' import '@/assets/css/checkout.css' @@ -63,7 +67,7 @@ import '@/assets/css/checkout.css' // Make sure to call `loadStripe` outside of a component’s render to avoid // recreating the `Stripe` object on every render. // -const stripePromise = loadStripe(env.STRIPE_PUBLISHABLE_KEY) +const stripePromise = env.PAYMENT_GATEWAY === bookcarsTypes.PaymentGateway.Stripe ? loadStripe(env.STRIPE_PUBLISHABLE_KEY) : null const Checkout = () => { const location = useLocation() @@ -128,6 +132,9 @@ const Checkout = () => { const [licenseRequired, setLicenseRequired] = useState(false) const [license, setLicense] = useState(null) const [openMapDialog, setOpenMapDialog] = useState(false) + const [payPalLoaded, setPayPalLoaded] = useState(false) + const [payPalInit, setPayPalInit] = useState(false) + const [payPalProcessing, setPayPalProcessing] = useState(false) const _fr = language === 'fr' const _locale = _fr ? fr : enUS @@ -365,7 +372,7 @@ const Checkout = () => { } } - const basePrice = await bookcarsHelper.convertPrice(price, StripeService.getCurrency(), env.BASE_CURRENCY) + const basePrice = await bookcarsHelper.convertPrice(price, PaymentService.getCurrency(), env.BASE_CURRENCY) const booking: bookcarsTypes.Booking = { supplier: car.supplier._id as string, @@ -400,21 +407,25 @@ const Checkout = () => { let _customerId: string | undefined let _sessionId: string | undefined if (!payLater) { - const payload: bookcarsTypes.CreatePaymentPayload = { - amount: payDeposit ? depositPrice : price, - currency: StripeService.getCurrency(), - locale: language, - receiptEmail: (!authenticated ? driver?.email : user?.email) as string, - name: `${car.name} + if (env.PAYMENT_GATEWAY === bookcarsTypes.PaymentGateway.Stripe) { + const payload: bookcarsTypes.CreatePaymentPayload = { + amount: payDeposit ? depositPrice : price, + currency: PaymentService.getCurrency(), + locale: language, + receiptEmail: (!authenticated ? driver?.email : user?.email) as string, + name: `${car.name} - ${daysLabel} - ${pickupLocation._id === dropOffLocation._id ? pickupLocation.name : `${pickupLocation.name} - ${dropOffLocation.name}`}`, - description: `${env.WEBSITE_NAME} Web Service`, - customerName: (!authenticated ? driver?.fullName : user?.fullName) as string, + description: `${env.WEBSITE_NAME} Web Service`, + customerName: (!authenticated ? driver?.fullName : user?.fullName) as string, + } + const res = await StripeService.createCheckoutSession(payload) + setClientSecret(res.clientSecret) + _sessionId = res.sessionId + _customerId = res.customerId + } else { + setPayPalLoaded(true) } - const res = await StripeService.createCheckoutSession(payload) - setClientSecret(res.clientSecret) - _sessionId = res.sessionId - _customerId = res.customerId } booking.isDeposit = payDeposit @@ -425,7 +436,8 @@ const Checkout = () => { additionalDriver: _additionalDriver, payLater, sessionId: _sessionId, - customerId: _customerId + customerId: _customerId, + payPal: env.PAYMENT_GATEWAY === bookcarsTypes.PaymentGateway.PayPal, } const { status, bookingId: _bookingId } = await BookingService.checkout(payload) @@ -507,8 +519,8 @@ const Checkout = () => { return } - const _price = await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(_car, _from, _to)) - const _depositPrice = _car.deposit > 0 ? await StripeService.convertPrice(_car.deposit) : 0 + const _price = await PaymentService.convertPrice(bookcarsHelper.calculateTotalPrice(_car, _from, _to)) + const _depositPrice = _car.deposit > 0 ? await PaymentService.convertPrice(_car.deposit) : 0 const included = (val: number) => val === 0 @@ -566,7 +578,7 @@ const Checkout = () => { hidePrice sizeAuto onLoad={() => setLoadingPage(false)} - hideSupplier + // hideSupplier /> {
{(!car.supplier.payLater || !payLater) && ( - clientSecret && ( -
- - - -
- ) + env.PAYMENT_GATEWAY === bookcarsTypes.PaymentGateway.Stripe + ? ( + clientSecret && ( +
+ + + +
+ ) + ) + : payPalLoaded ? ( +
+ { + const name = `${car.name} - ${daysLabel} - ${pickupLocation._id === dropOffLocation._id ? pickupLocation.name : `${pickupLocation.name} - ${dropOffLocation.name}`}` + const amount = payDeposit ? depositPrice : price + const orderId = await PayPalService.createOrder(bookingId!, amount, PaymentService.getCurrency(), name) + return orderId + }} + onApprove={async (data) => { + try { + setPayPalProcessing(true) + const { orderID } = data + const status = await PayPalService.checkOrder(bookingId!, orderID) + + if (status === 200) { + setVisible(false) + setSuccess(true) + } else { + setPaymentFailed(true) + } + } catch (err) { + helper.error(err) + } finally { + setPayPalProcessing(false) + } + }} + onInit={() => { + setPayPalInit(true) + }} + /> +
+ ) : null )}
- {(!clientSecret || payLater) && ( - - )} + {( + (env.PAYMENT_GATEWAY === bookcarsTypes.PaymentGateway.Stripe && !clientSecret) + || (env.PAYMENT_GATEWAY === bookcarsTypes.PaymentGateway.PayPal && !payPalInit) + || payLater) && ( + + )}