1
0
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:
Sylvain 2020-12-08 12:26:03 +01:00
parent b6240c5046
commit 1d64c517c9
12 changed files with 67 additions and 26 deletions

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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>
);
}
})

View File

@ -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);
}
}
}
}
}

View File

@ -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']));

View File

@ -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)
};

View File

@ -9,6 +9,10 @@ export interface PaymentConfirmation {
}
}
export interface IntentConfirmation {
client_secret: string
}
export enum PaymentMethod {
Stripe = 'stripe',
Other = ''

View File

@ -205,5 +205,6 @@
after-success="afterStripeSuccess"
cart-items="stripe.cartItems"
current-user="currentUser"
customer="user"
schedule="schedule.payment_schedule"/>
</div>

View File

@ -55,5 +55,6 @@
cart-items="cartItems"
current-user="currentUser"
schedule="schedule"
customer="user"
processPayment="false"/>
</div>

View File

@ -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."

View File

@ -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."

View File

@ -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'