Skip to content

Commit

Permalink
Added Stripe Payment integration
Browse files Browse the repository at this point in the history
  • Loading branch information
gisubizo Jovan authored and teerenzo committed Jul 29, 2024
1 parent da7bd42 commit 968b444
Show file tree
Hide file tree
Showing 8 changed files with 431 additions and 6 deletions.
73 changes: 73 additions & 0 deletions src/__test__/Payment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { configureStore } from "@reduxjs/toolkit";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";

import paymentReducer, {
makePayment,
handleSuccess,
} from "../redux/reducers/payment";

/* eslint-disable @typescript-eslint/default-param-last */
jest.mock("../redux/reducers/payment", () => ({
__esModule: true,
makePayment: jest
.fn()
.mockImplementation(() => ({ type: "mockMakePayment" })),
handleSuccess: jest
.fn()
.mockImplementation(() => ({ type: "mockHandleSuccess" })),
default: jest.fn().mockImplementation((state = {}, action) => {
switch (action.type) {
case "mockMakePayment":
case "mockHandleSuccess":
return {
...state,
loading: false,
data: { status: "success" },
error: null,
};
default:
return state;
}
}),
}));

describe("payment slice", () => {
let store;
let mockAxios;

beforeEach(() => {
store = configureStore({
reducer: {
payment: paymentReducer,
},
});

mockAxios = new MockAdapter(axios);
});

it("should handle makePayment", async () => {
const paymentData = { amount: 100 };
const mockResponse = { status: "success" };

mockAxios.onPost("/payment/checkout", paymentData).reply(200, mockResponse);

// const makePayment = require("../redux/reducers/payment").makePayment;
await store.dispatch(makePayment(paymentData));

const state = store.getState();
expect(state.payment.loading).toBe(false);
expect(state.payment.data).toEqual(mockResponse);
});
it("should handle handleSuccess", async () => {
const mockResponse = { sessionId: "testSessionId", userId: "testUserId" };

// const handleSuccess = require("../redux/reducers/payment").handleSuccess;
await store.dispatch(handleSuccess(mockResponse));

const state = store.getState();
expect(state.payment.loading).toBe(false);
expect(state.payment.data).toEqual({ status: "success" });
expect(state.payment.error).toBe(null);
});
});
100 changes: 100 additions & 0 deletions src/__test__/paymentApiSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { configureStore } from "@reduxjs/toolkit";
import axios from "axios";

import paymentSlice, {
makePayment,
handleSuccess,
} from "../redux/reducers/payment";

jest.mock("../redux/api/api", () => ({
api: {
interceptors: {
response: {
use: jest.fn(),
},
request: {
use: jest.fn(),
},
},
},
}));

jest.mock("axios");

describe("paymentSlice", () => {
let store;

beforeEach(() => {
store = configureStore({
reducer: {
payment: paymentSlice,
},
});
});

it("handles successful makePayment", async () => {
const mockResponse = { data: { status: "success" } };
// @ts-ignore
axios.post.mockResolvedValueOnce(mockResponse);

await store.dispatch(
makePayment({
amount: 100,
}),
);

const state = store.getState();
expect(state.payment.loading).toBe(false);
});

it("handles failed makePayment", async () => {
console.log("states on failed payment");
const mockError = { response: { data: { message: "Payment failed" } } };
// @ts-ignore
axios.post.mockRejectedValueOnce(mockError);

await store.dispatch(
makePayment({
amount: 100,
}),
);

const state = store.getState();

expect(state.payment.loading).toBe(false);
});

it("handles successful handleSuccess", async () => {
const mockResponse = { data: { status: "success" } };
// @ts-ignore
axios.get.mockResolvedValueOnce(mockResponse);

await store.dispatch(
handleSuccess({
sessionId: "testSessionId",
userId: "testUserId",
}),
);

const state = store.getState();
expect(state.payment.loading).toBe(false);
});

it("handles failed handleSuccess", async () => {
const mockError = {
response: { data: { message: "handleSuccess failed" } },
};
// @ts-ignore
axios.get.mockRejectedValueOnce(mockError);

await store.dispatch(
handleSuccess({
sessionId: "testSessionId",
userId: "testUserId",
}),
);

const state = store.getState();
expect(state.payment.loading).toBe(false);
});
});
4 changes: 2 additions & 2 deletions src/pages/CartManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IoChevronUpOutline, IoChevronDownSharp } from "react-icons/io5";
import { MdOutlineClose } from "react-icons/md";
import { ToastContainer, toast } from "react-toastify";
import { AxiosError } from "axios";
import { Link } from "react-router-dom";

import { RootState } from "../redux/store";
import {
Expand Down Expand Up @@ -50,7 +51,6 @@ const CartManagement: React.FC<IProductCardProps> = () => {
</div>
);
}
console.log(userCart);

const handleDelete = async () => {
await dispatch(cartDelete());
Expand Down Expand Up @@ -239,7 +239,7 @@ const CartManagement: React.FC<IProductCardProps> = () => {
</div>
<hr className="w-full border-t border-gray-300 mt-2" />
<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]">
Proceed to Checkout
<Link to="/payment">Proceed to Checkout</Link>
</div>
</div>
)}
Expand Down
178 changes: 178 additions & 0 deletions src/pages/paymentPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { Link } from "react-router-dom";
import { ToastContainer, toast } from "react-toastify";

import { RootState } from "../redux/store";
import HeaderInfo from "../components/common/header/Info";
import Footer from "../components/common/footer/Footer";
import { useAppDispatch } from "../redux/hooks";
import { makePayment, handleSuccess } from "../redux/reducers/payment";
import { cartManage } from "../redux/reducers/cartSlice";
import Spinner from "../components/common/auth/Loader";

const Payment = () => {
const loading = useSelector((state: RootState) => state.payment.loading);
const userCart = useSelector((state: RootState) => state!.cart.data);
const dispatch = useAppDispatch();
const totalPrice = userCart.reduce((total, item) => total + item.price, 0);

const handlePayment = () => {
try {
dispatch(makePayment({ totalPrice, userCart })).then((response) => {
if (response.payload.sessionUrl) {
toast(`${response.payload.message}\n Redirecting to stripe payment`);

setTimeout(() => {
window.location.href = response.payload.sessionUrl;
}, 3000);
} else {
toast(response.payload.message);
}
});
} catch (err) {
toast.error("Failed to make payment");
}
};

useEffect(() => {
dispatch(cartManage());
}, [dispatch]);

const total = userCart.reduce(
// @ts-ignore
(acc, item) => acc + item.product?.price * item.quantity,
0,
);

return (
<div className="px-[2%] md:px-[4%] parent-container h-screen overflow-auto">
<ToastContainer />
<div className="pt-8">
<h2>
<Link to="/">Home</Link>
{' '}
/
<Link to="/carts">Carts</Link>
{' '}
/ payment
</h2>
</div>
<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]">
<h2 className="text-lg font-semibold mb-4 text-left">
Checkout Details
</h2>
{userCart.length === 0 ? (
<tr>
<td colSpan={4} className="text-center">
No items in the cart 😎
</td>
</tr>
) : (
userCart.map((item: any) => (
<tr
key={item.id}
className="flex gap-10 justify-between w-full mb-2"
>
<td className="text-left py-3">
<div className="flex items-center">
<img
data-testId="img-cart"
className="w-12"
src={item.product?.images[0]}
alt={item.product?.name}
/>
<span className="mx-2 hidden md:block text-[9px] md:text-normal">
{item.product?.name.length > 8
? `${item.product?.name.slice(0, 8)}...`
: item.product?.name}
</span>
</div>
</td>
<td className="text-left text-[14px]">
<h2 data-testId="price-cart">
RWF
{item.product?.price}
</h2>
</td>
</tr>
))
)}
<div className="flex gap-10 justify-between w-full mb-2">
<h1 data-testId="subtotal" className="text-left">
Subtotal
</h1>
<span>
RWF
{total}
</span>
</div>
<hr className="w-full border-t border-gray-300 mb-2" />
<div className="flex gap-10 justify-between w-full mb-2 ext-[9px] md:text-normal">
<h1 data-testId="shipping" className="text-left">
Shipping
</h1>
<span>Free</span>
</div>
<hr className="w-full border-t border-gray-300 mb-2" />
<div className="flex gap-10 justify-between w-full">
<h1 data-testId="total" className="text-left">
Total
</h1>
<span className="">
RWF
{total}
</span>
</div>
<hr className="w-full border-t border-gray-300 mt-2" />
<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]">
<button onClick={handlePayment}>
{loading ? "Processing..." : "Pay with Stripe"}
</button>
</div>
</div>
<div className="bg-gray-200 w-100% sm:w-[100%] h-[1px] mt-[0.1%]" />
</div>
);
};

const SuccessfulPayment = () => {
const dispatch = useAppDispatch();
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get("sessionId");
const userId = urlParams.get("userId");
if (sessionId && userId) {
dispatch(handleSuccess({ sessionId, userId })).then((action: any) => {
if (handleSuccess.fulfilled.match(action)) {
console.log("Payment Data", action.payload);
} else if (handleSuccess.rejected.match(action)) {
console.error("Failed to fetch payment data", action.error);
}
});
}
}, [dispatch]);

return (
<section className="flex items-center justify-center py-32 bg-gray-100 md:m-0 px-4 ">
<div className="bg-white p-6 rounded shadow-md text-center">
<h1 className="text-2xl font-medium text-red-500">
Payment Was Successful !!!
</h1>
<p className="mt-4">
Checkout Details about your Order More details was sent to your Email!
</p>
<p className="mt-2">Thank you for shopping with us.</p>

<Link to="/orders">
<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">
Checkout your Order
</button>
</Link>
</div>
</section>
);
};

export default Payment;
export { SuccessfulPayment };
4 changes: 0 additions & 4 deletions src/redux/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { set } from "react-hook-form";
import { useState, useEffect } from "react";
import { toast } from "react-toastify";

import store from "../store";

import api from "./action";

let navigateFunction = null;
Expand Down
Loading

0 comments on commit 968b444

Please sign in to comment.