Skip to content

Commit 968b444

Browse files
gisubizo Jovanteerenzo
authored andcommitted
Added Stripe Payment integration
1 parent da7bd42 commit 968b444

File tree

8 files changed

+431
-6
lines changed

8 files changed

+431
-6
lines changed

src/__test__/Payment.test.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { configureStore } from "@reduxjs/toolkit";
2+
import axios from "axios";
3+
import MockAdapter from "axios-mock-adapter";
4+
5+
import paymentReducer, {
6+
makePayment,
7+
handleSuccess,
8+
} from "../redux/reducers/payment";
9+
10+
/* eslint-disable @typescript-eslint/default-param-last */
11+
jest.mock("../redux/reducers/payment", () => ({
12+
__esModule: true,
13+
makePayment: jest
14+
.fn()
15+
.mockImplementation(() => ({ type: "mockMakePayment" })),
16+
handleSuccess: jest
17+
.fn()
18+
.mockImplementation(() => ({ type: "mockHandleSuccess" })),
19+
default: jest.fn().mockImplementation((state = {}, action) => {
20+
switch (action.type) {
21+
case "mockMakePayment":
22+
case "mockHandleSuccess":
23+
return {
24+
...state,
25+
loading: false,
26+
data: { status: "success" },
27+
error: null,
28+
};
29+
default:
30+
return state;
31+
}
32+
}),
33+
}));
34+
35+
describe("payment slice", () => {
36+
let store;
37+
let mockAxios;
38+
39+
beforeEach(() => {
40+
store = configureStore({
41+
reducer: {
42+
payment: paymentReducer,
43+
},
44+
});
45+
46+
mockAxios = new MockAdapter(axios);
47+
});
48+
49+
it("should handle makePayment", async () => {
50+
const paymentData = { amount: 100 };
51+
const mockResponse = { status: "success" };
52+
53+
mockAxios.onPost("/payment/checkout", paymentData).reply(200, mockResponse);
54+
55+
// const makePayment = require("../redux/reducers/payment").makePayment;
56+
await store.dispatch(makePayment(paymentData));
57+
58+
const state = store.getState();
59+
expect(state.payment.loading).toBe(false);
60+
expect(state.payment.data).toEqual(mockResponse);
61+
});
62+
it("should handle handleSuccess", async () => {
63+
const mockResponse = { sessionId: "testSessionId", userId: "testUserId" };
64+
65+
// const handleSuccess = require("../redux/reducers/payment").handleSuccess;
66+
await store.dispatch(handleSuccess(mockResponse));
67+
68+
const state = store.getState();
69+
expect(state.payment.loading).toBe(false);
70+
expect(state.payment.data).toEqual({ status: "success" });
71+
expect(state.payment.error).toBe(null);
72+
});
73+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { configureStore } from "@reduxjs/toolkit";
2+
import axios from "axios";
3+
4+
import paymentSlice, {
5+
makePayment,
6+
handleSuccess,
7+
} from "../redux/reducers/payment";
8+
9+
jest.mock("../redux/api/api", () => ({
10+
api: {
11+
interceptors: {
12+
response: {
13+
use: jest.fn(),
14+
},
15+
request: {
16+
use: jest.fn(),
17+
},
18+
},
19+
},
20+
}));
21+
22+
jest.mock("axios");
23+
24+
describe("paymentSlice", () => {
25+
let store;
26+
27+
beforeEach(() => {
28+
store = configureStore({
29+
reducer: {
30+
payment: paymentSlice,
31+
},
32+
});
33+
});
34+
35+
it("handles successful makePayment", async () => {
36+
const mockResponse = { data: { status: "success" } };
37+
// @ts-ignore
38+
axios.post.mockResolvedValueOnce(mockResponse);
39+
40+
await store.dispatch(
41+
makePayment({
42+
amount: 100,
43+
}),
44+
);
45+
46+
const state = store.getState();
47+
expect(state.payment.loading).toBe(false);
48+
});
49+
50+
it("handles failed makePayment", async () => {
51+
console.log("states on failed payment");
52+
const mockError = { response: { data: { message: "Payment failed" } } };
53+
// @ts-ignore
54+
axios.post.mockRejectedValueOnce(mockError);
55+
56+
await store.dispatch(
57+
makePayment({
58+
amount: 100,
59+
}),
60+
);
61+
62+
const state = store.getState();
63+
64+
expect(state.payment.loading).toBe(false);
65+
});
66+
67+
it("handles successful handleSuccess", async () => {
68+
const mockResponse = { data: { status: "success" } };
69+
// @ts-ignore
70+
axios.get.mockResolvedValueOnce(mockResponse);
71+
72+
await store.dispatch(
73+
handleSuccess({
74+
sessionId: "testSessionId",
75+
userId: "testUserId",
76+
}),
77+
);
78+
79+
const state = store.getState();
80+
expect(state.payment.loading).toBe(false);
81+
});
82+
83+
it("handles failed handleSuccess", async () => {
84+
const mockError = {
85+
response: { data: { message: "handleSuccess failed" } },
86+
};
87+
// @ts-ignore
88+
axios.get.mockRejectedValueOnce(mockError);
89+
90+
await store.dispatch(
91+
handleSuccess({
92+
sessionId: "testSessionId",
93+
userId: "testUserId",
94+
}),
95+
);
96+
97+
const state = store.getState();
98+
expect(state.payment.loading).toBe(false);
99+
});
100+
});

src/pages/CartManagement.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IoChevronUpOutline, IoChevronDownSharp } from "react-icons/io5";
55
import { MdOutlineClose } from "react-icons/md";
66
import { ToastContainer, toast } from "react-toastify";
77
import { AxiosError } from "axios";
8+
import { Link } from "react-router-dom";
89

910
import { RootState } from "../redux/store";
1011
import {
@@ -50,7 +51,6 @@ const CartManagement: React.FC<IProductCardProps> = () => {
5051
</div>
5152
);
5253
}
53-
console.log(userCart);
5454

5555
const handleDelete = async () => {
5656
await dispatch(cartDelete());
@@ -239,7 +239,7 @@ const CartManagement: React.FC<IProductCardProps> = () => {
239239
</div>
240240
<hr className="w-full border-t border-gray-300 mt-2" />
241241
<div className="bg-[#DB4444] text-white rounded-sm px-2 md:px-2 py-2 hover:border-[0.5px] mt-8 cursor-pointer mx-auto md:text-[14px]">
242-
Proceed to Checkout
242+
<Link to="/payment">Proceed to Checkout</Link>
243243
</div>
244244
</div>
245245
)}

src/pages/paymentPage.tsx

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import React, { useEffect } from "react";
2+
import { useSelector, useDispatch } from "react-redux";
3+
import { Link } from "react-router-dom";
4+
import { ToastContainer, toast } from "react-toastify";
5+
6+
import { RootState } from "../redux/store";
7+
import HeaderInfo from "../components/common/header/Info";
8+
import Footer from "../components/common/footer/Footer";
9+
import { useAppDispatch } from "../redux/hooks";
10+
import { makePayment, handleSuccess } from "../redux/reducers/payment";
11+
import { cartManage } from "../redux/reducers/cartSlice";
12+
import Spinner from "../components/common/auth/Loader";
13+
14+
const Payment = () => {
15+
const loading = useSelector((state: RootState) => state.payment.loading);
16+
const userCart = useSelector((state: RootState) => state!.cart.data);
17+
const dispatch = useAppDispatch();
18+
const totalPrice = userCart.reduce((total, item) => total + item.price, 0);
19+
20+
const handlePayment = () => {
21+
try {
22+
dispatch(makePayment({ totalPrice, userCart })).then((response) => {
23+
if (response.payload.sessionUrl) {
24+
toast(`${response.payload.message}\n Redirecting to stripe payment`);
25+
26+
setTimeout(() => {
27+
window.location.href = response.payload.sessionUrl;
28+
}, 3000);
29+
} else {
30+
toast(response.payload.message);
31+
}
32+
});
33+
} catch (err) {
34+
toast.error("Failed to make payment");
35+
}
36+
};
37+
38+
useEffect(() => {
39+
dispatch(cartManage());
40+
}, [dispatch]);
41+
42+
const total = userCart.reduce(
43+
// @ts-ignore
44+
(acc, item) => acc + item.product?.price * item.quantity,
45+
0,
46+
);
47+
48+
return (
49+
<div className="px-[2%] md:px-[4%] parent-container h-screen overflow-auto">
50+
<ToastContainer />
51+
<div className="pt-8">
52+
<h2>
53+
<Link to="/">Home</Link>
54+
{' '}
55+
/
56+
<Link to="/carts">Carts</Link>
57+
{' '}
58+
/ payment
59+
</h2>
60+
</div>
61+
<div className="w-full sm:w-[50%] md:w-[38%] flex flex-col justify-center items-start mt-9 rounded-sm p-4 hover:border-[1.5px]">
62+
<h2 className="text-lg font-semibold mb-4 text-left">
63+
Checkout Details
64+
</h2>
65+
{userCart.length === 0 ? (
66+
<tr>
67+
<td colSpan={4} className="text-center">
68+
No items in the cart 😎
69+
</td>
70+
</tr>
71+
) : (
72+
userCart.map((item: any) => (
73+
<tr
74+
key={item.id}
75+
className="flex gap-10 justify-between w-full mb-2"
76+
>
77+
<td className="text-left py-3">
78+
<div className="flex items-center">
79+
<img
80+
data-testId="img-cart"
81+
className="w-12"
82+
src={item.product?.images[0]}
83+
alt={item.product?.name}
84+
/>
85+
<span className="mx-2 hidden md:block text-[9px] md:text-normal">
86+
{item.product?.name.length > 8
87+
? `${item.product?.name.slice(0, 8)}...`
88+
: item.product?.name}
89+
</span>
90+
</div>
91+
</td>
92+
<td className="text-left text-[14px]">
93+
<h2 data-testId="price-cart">
94+
RWF
95+
{item.product?.price}
96+
</h2>
97+
</td>
98+
</tr>
99+
))
100+
)}
101+
<div className="flex gap-10 justify-between w-full mb-2">
102+
<h1 data-testId="subtotal" className="text-left">
103+
Subtotal
104+
</h1>
105+
<span>
106+
RWF
107+
{total}
108+
</span>
109+
</div>
110+
<hr className="w-full border-t border-gray-300 mb-2" />
111+
<div className="flex gap-10 justify-between w-full mb-2 ext-[9px] md:text-normal">
112+
<h1 data-testId="shipping" className="text-left">
113+
Shipping
114+
</h1>
115+
<span>Free</span>
116+
</div>
117+
<hr className="w-full border-t border-gray-300 mb-2" />
118+
<div className="flex gap-10 justify-between w-full">
119+
<h1 data-testId="total" className="text-left">
120+
Total
121+
</h1>
122+
<span className="">
123+
RWF
124+
{total}
125+
</span>
126+
</div>
127+
<hr className="w-full border-t border-gray-300 mt-2" />
128+
<div className="bg-[#DB4444] text-white rounded-sm px-2 md:px-2 py-2 hover:border-[0.5px] mt-8 cursor-pointer mx-auto md:text-[14px]">
129+
<button onClick={handlePayment}>
130+
{loading ? "Processing..." : "Pay with Stripe"}
131+
</button>
132+
</div>
133+
</div>
134+
<div className="bg-gray-200 w-100% sm:w-[100%] h-[1px] mt-[0.1%]" />
135+
</div>
136+
);
137+
};
138+
139+
const SuccessfulPayment = () => {
140+
const dispatch = useAppDispatch();
141+
useEffect(() => {
142+
const urlParams = new URLSearchParams(window.location.search);
143+
const sessionId = urlParams.get("sessionId");
144+
const userId = urlParams.get("userId");
145+
if (sessionId && userId) {
146+
dispatch(handleSuccess({ sessionId, userId })).then((action: any) => {
147+
if (handleSuccess.fulfilled.match(action)) {
148+
console.log("Payment Data", action.payload);
149+
} else if (handleSuccess.rejected.match(action)) {
150+
console.error("Failed to fetch payment data", action.error);
151+
}
152+
});
153+
}
154+
}, [dispatch]);
155+
156+
return (
157+
<section className="flex items-center justify-center py-32 bg-gray-100 md:m-0 px-4 ">
158+
<div className="bg-white p-6 rounded shadow-md text-center">
159+
<h1 className="text-2xl font-medium text-red-500">
160+
Payment Was Successful !!!
161+
</h1>
162+
<p className="mt-4">
163+
Checkout Details about your Order More details was sent to your Email!
164+
</p>
165+
<p className="mt-2">Thank you for shopping with us.</p>
166+
167+
<Link to="/orders">
168+
<button className="mt-4 inline-block px-4 py-2 text-white bg-red-500 rounded transition-colors duration-300 cursor-pointer hover:bg-green-600">
169+
Checkout your Order
170+
</button>
171+
</Link>
172+
</div>
173+
</section>
174+
);
175+
};
176+
177+
export default Payment;
178+
export { SuccessfulPayment };

src/redux/api/api.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import { set } from "react-hook-form";
2-
import { useState, useEffect } from "react";
31
import { toast } from "react-toastify";
42

5-
import store from "../store";
6-
73
import api from "./action";
84

95
let navigateFunction = null;

0 commit comments

Comments
 (0)