mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
create stripe's paymentIntent
associate the paymentMethod (the card) with the customer (the user) for futur usages in subscriptions
This commit is contained in:
parent
b6240c5046
commit
1d64c517c9
@ -68,6 +68,13 @@ class API::PaymentsController < API::ApiController
|
||||
render json: { status: false }
|
||||
end
|
||||
|
||||
def setup_intent
|
||||
user = User.find(params[:user_id])
|
||||
key = Setting.get('stripe_secret_key')
|
||||
@intent = Stripe::SetupIntent.create({ customer: user.stp_customer_id }, { api_key: key })
|
||||
render json: { client_secret: @intent.client_secret }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def on_reservation_success(intent, details)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import apiClient from './api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { CartItems, PaymentConfirmation } from '../models/payment';
|
||||
import { CartItems, IntentConfirmation, PaymentConfirmation } from '../models/payment';
|
||||
|
||||
export default class PaymentAPI {
|
||||
static async confirm (stp_payment_method_id: string, cart_items: CartItems): Promise<PaymentConfirmation> {
|
||||
@ -10,5 +10,10 @@ export default class PaymentAPI {
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async setupIntent (user_id: number): Promise<IntentConfirmation> {
|
||||
const res: AxiosResponse = await apiClient.get(`/api/payments/setup_intent/${user_id}`);
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
* This component initializes the stripe's Elements tag with the API key
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import SettingAPI from '../api/setting';
|
||||
@ -10,7 +10,7 @@ import { SettingName } from '../models/setting';
|
||||
|
||||
const stripePublicKey = SettingAPI.get(SettingName.StripePublicKey);
|
||||
|
||||
export const StripeElements: React.FC = ({ children }) => {
|
||||
export const StripeElements: React.FC = memo(({ children }) => {
|
||||
const publicKey = stripePublicKey.read();
|
||||
const stripePromise = loadStripe(publicKey.value);
|
||||
|
||||
@ -19,4 +19,4 @@ export const StripeElements: React.FC = ({ children }) => {
|
||||
{children}
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
@ -1,15 +1,16 @@
|
||||
import React, { FormEvent } from 'react';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { PaymentMethod } from "@stripe/stripe-js";
|
||||
import { PaymentIntent } from "@stripe/stripe-js";
|
||||
import PaymentAPI from '../api/payment';
|
||||
import { CartItems, PaymentConfirmation } from '../models/payment';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Reservation } from '../models/reservation';
|
||||
import { User } from '../models/user';
|
||||
|
||||
interface StripeFormProps {
|
||||
onSubmit: () => void,
|
||||
onSuccess: (result: PaymentMethod|PaymentConfirmation|any) => void,
|
||||
onSuccess: (result: PaymentIntent|PaymentConfirmation|any) => void,
|
||||
onError: (message: string) => void,
|
||||
customer: User,
|
||||
className?: string,
|
||||
processPayment?: boolean,
|
||||
cartItems?: CartItems
|
||||
@ -19,7 +20,7 @@ interface StripeFormProps {
|
||||
* 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, processPayment = true, cartItems}) => {
|
||||
export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onError, children, className, processPayment = true, cartItems, customer }) => {
|
||||
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
@ -51,8 +52,18 @@ export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onE
|
||||
const res = await PaymentAPI.confirm(paymentMethod.id, cartItems);
|
||||
await handleServerConfirmation(res);
|
||||
} else {
|
||||
// we don't want to process the payment, only return the payment method
|
||||
onSuccess(paymentMethod);
|
||||
// we don't want to process the payment, only associate the payment method with the user
|
||||
const { client_secret } = await PaymentAPI.setupIntent(customer.id);
|
||||
const { setupIntent, error } = await stripe.confirmCardSetup(client_secret, {
|
||||
payment_method: paymentMethod.id
|
||||
})
|
||||
if (error) {
|
||||
onError(error.message);
|
||||
} else {
|
||||
if (setupIntent.status === 'succeeded') {
|
||||
onSuccess(setupIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import { IApplication } from '../models/application';
|
||||
import { StripeElements } from './stripe-elements';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal, ModalSize } from './fab-modal';
|
||||
import { PaymentMethod } from '@stripe/stripe-js';
|
||||
import { PaymentIntent } from '@stripe/stripe-js';
|
||||
import { WalletInfo } from './wallet-info';
|
||||
import { User } from '../models/user';
|
||||
import CustomAssetAPI from '../api/custom-asset';
|
||||
@ -33,15 +33,16 @@ declare var Fablab: IFablab;
|
||||
interface StripeModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: (result: PaymentMethod|PaymentConfirmation) => void,
|
||||
afterSuccess: (result: PaymentIntent|PaymentConfirmation) => void,
|
||||
cartItems: CartItems,
|
||||
currentUser: User,
|
||||
schedule: PaymentSchedule,
|
||||
customer: User
|
||||
}
|
||||
|
||||
const cgvFile = CustomAssetAPI.get(CustomAssetName.CgvFile);
|
||||
|
||||
const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule }) => {
|
||||
const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer }) => {
|
||||
// customer's wallet
|
||||
const [wallet, setWallet] = useState(null);
|
||||
// server-computed price with all details
|
||||
@ -125,6 +126,7 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
<img src={stripeLogo} alt="powered by stripe" />
|
||||
<img src={mastercardLogo} alt="mastercard" />
|
||||
<img src={visaLogo} alt="visa" />
|
||||
{/* compile */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -139,7 +141,7 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
/**
|
||||
* After sending the form with success, process the resulting payment method
|
||||
*/
|
||||
const handleFormSuccess = async (result: PaymentMethod|PaymentConfirmation|any): Promise<void> => {
|
||||
const handleFormSuccess = async (result: PaymentIntent|PaymentConfirmation|any): Promise<void> => {
|
||||
setSubmitState(false);
|
||||
afterSuccess(result);
|
||||
}
|
||||
@ -178,6 +180,7 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
onError={handleFormError}
|
||||
className="stripe-form"
|
||||
cartItems={cartItems}
|
||||
customer={customer}
|
||||
processPayment={!isPaymentSchedule()}>
|
||||
{hasErrors() && <div className="stripe-errors">
|
||||
{errors}
|
||||
@ -211,12 +214,12 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
);
|
||||
}
|
||||
|
||||
const StripeModalWrapper: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cartItems}) => {
|
||||
const StripeModalWrapper: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cartItems, customer }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<StripeModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} currentUser={currentUser} schedule={schedule} cartItems={cartItems}/>
|
||||
<StripeModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} currentUser={currentUser} schedule={schedule} cartItems={cartItems} customer={customer} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess','currentUser', 'schedule', 'cartItems']));
|
||||
Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess','currentUser', 'schedule', 'cartItems', 'customer']));
|
||||
|
@ -332,7 +332,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
$scope.afterStripeSuccess = (result) => {
|
||||
$scope.toggleStripeModal();
|
||||
if ($scope.schedule.requested_schedule) {
|
||||
afterPaymentMethodCreation(result);
|
||||
afterPaymentIntentCreation(result);
|
||||
} else {
|
||||
afterPayment(result);
|
||||
}
|
||||
@ -742,10 +742,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
},
|
||||
schedule () {
|
||||
return $scope.schedule;
|
||||
},
|
||||
user () {
|
||||
return $scope.user;
|
||||
}
|
||||
},
|
||||
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) {
|
||||
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon', 'selectedPlan', 'schedule', 'cartItems', 'user',
|
||||
function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, Subscription, wallet, helpers, $filter, coupon, selectedPlan, schedule, cartItems, user) {
|
||||
// user wallet amount
|
||||
$scope.wallet = wallet;
|
||||
|
||||
@ -780,6 +783,9 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
// this is used to collect card data when a payment-schedule was selected, and paid with a card
|
||||
$scope.isOpenStripeModal = false;
|
||||
|
||||
// the customer
|
||||
$scope.user = user;
|
||||
|
||||
/**
|
||||
* Callback to process the local payment, triggered on button click
|
||||
*/
|
||||
@ -900,11 +906,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
};
|
||||
|
||||
/**
|
||||
* Actions to run after the payment method was created on Stripe. Used for payment schedules.
|
||||
* @param paymentMethod {PaymentMethod}
|
||||
* Actions to run after the payment intent was created on Stripe.
|
||||
* A payment intent associates a payment method with a stripe customer.
|
||||
* This is used for payment schedules.
|
||||
* @param intent {PaymentIntent}
|
||||
*/
|
||||
const afterPaymentMethodCreation = function (paymentMethod) {
|
||||
// TODO, create an API point for payment_schedule validation
|
||||
const afterPaymentIntentCreation = function (intent) {
|
||||
// TODO, create an API endpoint for payment_schedule validation
|
||||
// or: POST reservation || POST subscription (if admin/manager)
|
||||
};
|
||||
|
||||
|
@ -9,6 +9,10 @@ export interface PaymentConfirmation {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IntentConfirmation {
|
||||
client_secret: string
|
||||
}
|
||||
|
||||
export enum PaymentMethod {
|
||||
Stripe = 'stripe',
|
||||
Other = ''
|
||||
|
@ -205,5 +205,6 @@
|
||||
after-success="afterStripeSuccess"
|
||||
cart-items="stripe.cartItems"
|
||||
current-user="currentUser"
|
||||
customer="user"
|
||||
schedule="schedule.payment_schedule"/>
|
||||
</div>
|
||||
|
@ -55,5 +55,6 @@
|
||||
cart-items="cartItems"
|
||||
current-user="currentUser"
|
||||
schedule="schedule"
|
||||
customer="user"
|
||||
processPayment="false"/>
|
||||
</div>
|
||||
|
@ -413,7 +413,7 @@ en:
|
||||
you_ve_just_selected_a_subscription_html: "You've just selected a <strong>subscription</strong>:"
|
||||
monthly_payment: "Monthly payment"
|
||||
your_payment_schedule: "Your payment schedule"
|
||||
monthly_payment_NUMBER: "{NUMBER}{NUMBER, plural, =1{st} =2{nd} =3{rd} other{th}} monthly payment:"
|
||||
monthly_payment_NUMBER: "{NUMBER}{NUMBER, plural, =1{st} =2{nd} =3{rd} other{th}} monthly payment: "
|
||||
NUMBER_monthly_payment_of_AMOUNT: "{NUMBER} monthly {NUMBER, plural, =1{payment} other{payments}} of {AMOUNT}"
|
||||
first_debit: "First debit on the day of the order."
|
||||
debit: "Debit on the day of the order."
|
||||
|
@ -413,7 +413,7 @@ fr:
|
||||
you_ve_just_selected_a_subscription_html: "Vous venez de sélectionner un <strong>abonnement</strong> :"
|
||||
monthly_payment: "Paiement mensuel"
|
||||
your_payment_schedule: "Votre échéancier de paiement"
|
||||
monthly_payment_NUMBER: "{NUMBER}{NUMBER, plural, =1{ère} other{ème}} mensualité :"
|
||||
monthly_payment_NUMBER: "{NUMBER}{NUMBER, plural, =1{ère} other{ème}} mensualité : "
|
||||
NUMBER_monthly_payment_of_AMOUNT: "{NUMBER} {NUMBER, plural, =1{mensualité} other{mensualités}} de {AMOUNT}"
|
||||
first_debit: "Premier prélèvement le jour de la commande."
|
||||
debit: "Prélèvement le jour de la commande."
|
||||
|
@ -165,6 +165,7 @@ Rails.application.routes.draw do
|
||||
# payments handling
|
||||
post 'payments/confirm_payment' => 'payments/confirm_payment'
|
||||
get 'payments/online_payment_status' => 'payments/online_payment_status'
|
||||
get 'payments/setup_intent/:user_id' => 'payments#setup_intent'
|
||||
|
||||
# FabAnalytics
|
||||
get 'analytics/data' => 'analytics#data'
|
||||
|
Loading…
x
Reference in New Issue
Block a user