Implemented Stripe Embedded Checkout Component;
Inclusion of .env files;
This commit is contained in:
4
web_server/frontend/.env.example
Normal file
4
web_server/frontend/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# Example frontend variables (for git)
|
||||
|
||||
VITE_API_URL=http://127.0.0.1:1234
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51112223...
|
||||
43
web_server/frontend/package-lock.json
generated
43
web_server/frontend/package-lock.json
generated
@@ -8,6 +8,8 @@
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@stripe/react-stripe-js": "^3.1.1",
|
||||
"@stripe/stripe-js": "^5.5.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.1.3"
|
||||
@@ -1356,6 +1358,29 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@stripe/react-stripe-js": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.1.1.tgz",
|
||||
"integrity": "sha512-+JzYFgUivVD7koqYV7LmLlt9edDMAwKH7XhZAHFQMo7NeRC+6D2JmQGzp9tygWerzwttwFLlExGp4rAOvD6l9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
|
||||
"react": ">=16.8.0 <20.0.0",
|
||||
"react-dom": ">=16.8.0 <20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/stripe-js": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.5.0.tgz",
|
||||
"integrity": "sha512-lkfjyAd34aeMpTKKcEVfy8IUyEsjuAT3t9EXr5yZDtdIUncnZpedl/xLV16Dkd4z+fQwixScsCCDxSMNtBOgpQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -3106,7 +3131,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -3443,6 +3467,17 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -3499,6 +3534,12 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"proxy": "http://localhost:5000",
|
||||
"proxy": "http://127.0.0.1:8080",
|
||||
"dependencies": {
|
||||
"@stripe/react-stripe-js": "^3.1.1",
|
||||
"@stripe/stripe-js": "^5.5.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.1.3"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import HomePage from './pages/HomePage';
|
||||
import VideoPage from './pages/VideoPage';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import SignupPage from './pages/SignupPage';
|
||||
import CheckoutPage from './pages/CheckoutPage';
|
||||
import NotFoundPage from './pages/NotFoundPage';
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import HomePage from "./pages/HomePage";
|
||||
import VideoPage from "./pages/VideoPage";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import SignupPage from "./pages/SignupPage";
|
||||
// import CheckoutPage from "./pages/CheckoutPage";
|
||||
import NotFoundPage from "./pages/NotFoundPage";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -14,11 +14,15 @@ function App() {
|
||||
<Route path="/video" element={<VideoPage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/signup" element={<SignupPage />} />
|
||||
<Route path="/checkout" element={<CheckoutPage />} />
|
||||
{/* <Route path="/checkout" element={<CheckoutPage />} /> */}
|
||||
|
||||
{/* <Route path="/checkout" element={<CheckoutForm />} /> */}
|
||||
{/* <Route path="/return" element={<Return />} /> */}
|
||||
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: hsl(242, 100%, 10%);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ff9900;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/*
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
|
||||
@@ -1 +1,95 @@
|
||||
// checkout.html
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import {
|
||||
EmbeddedCheckoutProvider,
|
||||
EmbeddedCheckout,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
// Initialize Stripe once
|
||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);
|
||||
|
||||
|
||||
export const Return: React.FC = () => {
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [customerEmail, setCustomerEmail] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const sessionId = urlParams.get("session_id");
|
||||
|
||||
if (sessionId) {
|
||||
fetch(`${API_URL}/session-status?session_id=${sessionId}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setStatus(data.status);
|
||||
setCustomerEmail(data.customer_email);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (status === "open") {
|
||||
return <Navigate to="/checkout" />;
|
||||
}
|
||||
|
||||
if (status === "complete") {
|
||||
return (
|
||||
<section id="success">
|
||||
<p>
|
||||
We appreciate your business! A confirmation email will be sent to{" "}
|
||||
{customerEmail}. If you have any questions, please email{" "}
|
||||
<a href="mailto:orders@example.com">orders@example.com</a>.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Main CheckoutForm component
|
||||
interface CheckoutFormProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const CheckoutForm: React.FC<CheckoutFormProps> = ({ onClose }) => {
|
||||
const fetchClientSecret = () => {
|
||||
return fetch(`${API_URL}/create-checkout-session`, {
|
||||
method: "POST",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.clientSecret);
|
||||
};
|
||||
|
||||
const options = { fetchClientSecret };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="blurring-layer fixed z-10 inset-0 w-screen h-screen backdrop-blur-sm"></div>
|
||||
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50 h-[70vh] m-auto w-fit py-[50px] px-[100px] rounded-[2rem]">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-[1rem] right-[3rem] text-[2rem] text-red-600 font-black hover:text-[2.5rem] transition-all"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<div className="bg-white p-6 rounded-lg w-full max-w-2xl relative h-full rounded-[2rem]">
|
||||
<div
|
||||
id="checkout"
|
||||
className="h-full overflow-auto min-w-[30vw]"
|
||||
style={{ width: "clamp(300px, 60vw, 800px)" }}
|
||||
>
|
||||
<EmbeddedCheckoutProvider stripe={stripePromise} options={options}>
|
||||
<EmbeddedCheckout />
|
||||
</EmbeddedCheckoutProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckoutForm;
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
interface ButtonProps {
|
||||
title?: string;
|
||||
alt?: string;
|
||||
onClick?: () => void;
|
||||
title?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const Button = ({ title, alt, onClick }: ButtonProps) => {
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
title = "Submit",
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
<button onClick={onClick}>
|
||||
{title}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button
|
||||
export default Button;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const CheckoutPage: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckoutPage;
|
||||
@@ -3,6 +3,8 @@ import Navbar from "../components/Layout/Navbar";
|
||||
import ListRow from "../components/Layout/ListRow";
|
||||
// import { data, Link } from "react-router-dom";
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
const handleStreamClick = (streamId: string) => {
|
||||
// Handle navigation to stream page
|
||||
console.log(`Navigating to stream ${streamId}`);
|
||||
@@ -22,13 +24,13 @@ const HomePage: React.FC = () => {
|
||||
|
||||
// ↓↓ runs twice when in development mode
|
||||
useEffect(() => {
|
||||
fetch("http://127.0.0.1:5000/get_loggedin_status")
|
||||
fetch(`${API_URL}/get_loggedin_status`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setLoggedInStatus(data);
|
||||
console.log(data);
|
||||
});
|
||||
fetch("http://127.0.0.1:5000/get_streams")
|
||||
fetch(`${API_URL}/get_streams`)
|
||||
.then((response) => response.json())
|
||||
.then((data: StreamItem[]) => {
|
||||
setFeaturedStreams(data);
|
||||
@@ -37,17 +39,20 @@ const HomePage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="home-page bg-repeat" style={{ backgroundImage: "url(/images/background-pattern.svg)" }}>
|
||||
<div
|
||||
className="home-page bg-repeat"
|
||||
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
|
||||
>
|
||||
<Navbar logged_in={loggedInStatus} />
|
||||
|
||||
<ListRow
|
||||
title="Live Now"
|
||||
<ListRow
|
||||
title="Live Now"
|
||||
description="Streamers that are currently live"
|
||||
streams={featuredStreams}
|
||||
onStreamClick={handleStreamClick}
|
||||
/>
|
||||
<ListRow
|
||||
title="Trending Categories"
|
||||
title="Trending Categories"
|
||||
description="Categories that have been 'popping off' lately"
|
||||
streams={featuredStreams}
|
||||
onStreamClick={handleStreamClick}
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Button from '../components/Layout/Button';
|
||||
import CheckoutForm, { Return } from '../components/Checkout/CheckoutForm';
|
||||
|
||||
const VideoPage: React.FC = () => {
|
||||
const [showCheckout, setShowCheckout] = useState(false);
|
||||
const showReturn = window.location.search.includes('session_id');
|
||||
|
||||
useEffect(() => {
|
||||
if (showCheckout) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "unset";
|
||||
}
|
||||
// Cleanup function to ensure overflow is restored when component unmounts
|
||||
return () => {
|
||||
document.body.style.overflow = "unset";
|
||||
};
|
||||
}, [showCheckout]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello!</h1>
|
||||
|
||||
<Button
|
||||
title="Payment Screen Test"
|
||||
onClick={() => setShowCheckout(true)}
|
||||
/>
|
||||
|
||||
{showCheckout && <CheckoutForm onClose={() => setShowCheckout(false)} />}
|
||||
{showReturn && <Return />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user