@pretzelai/react
Version:
A React Stripe Payment Modal Component
688 lines (672 loc) • 60.1 kB
JavaScript
import * as React from 'react';
import React__default, { useState, useEffect, useRef } from 'react';
import { Elements, useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import styled, { keyframes } from 'styled-components';
import { AlertCircle, Check } from 'lucide-react';
import { z } from 'zod';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
function __makeTemplateObject(cooked, raw) {
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
return cooked;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var STRIPE_ROUTE = "/v1/public/stripe_public";
var WS_ROUTE = "/v1/public/ws";
var PaymentModal = function App(_a) {
var _this = this;
var isOpen = _a.isOpen, publicKey = _a.publicKey, priceId = _a.priceId, userEmail = _a.userEmail, userId = _a.userId, onClose = _a.onClose, onPaymentSuccess = _a.onPaymentSuccess, onPaymentError = _a.onPaymentError,
// TODO: Change to production URL
_b = _a.apiUrl,
// TODO: Change to production URL
apiUrl = _b === void 0 ? "https://api.withpretzel.com" : _b;
var _c = useState(null), stripePromise = _c[0], setStripePromise = _c[1];
var _d = useState(null), clientSecret = _d[0], setClientSecret = _d[1];
var _e = useState(null), integrationError = _e[0], setIntegrationError = _e[1];
useEffect(function () {
var fetchClientSecret = function () { return __awaiter(_this, void 0, void 0, function () {
var response, data, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 3, , 4]);
return [4 /*yield*/, fetch(window.location.hostname.includes("localhost")
? "http://localhost:8000".concat(STRIPE_ROUTE)
: "".concat(apiUrl).concat(STRIPE_ROUTE), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
publicKey: publicKey,
priceId: priceId,
userId: userId,
}),
})];
case 1:
response = _a.sent();
return [4 /*yield*/, response.json()];
case 2:
data = _a.sent();
if (!data.stripePublishableKey) {
setIntegrationError("Failed to initialize payment: ".concat(data.error));
return [2 /*return*/];
}
if (!data.clientSecret) {
setIntegrationError("Failed to initialize payment: ".concat(data.error));
return [2 /*return*/];
}
setClientSecret(data.clientSecret);
setStripePromise(loadStripe(data.stripePublishableKey));
return [3 /*break*/, 4];
case 3:
error_1 = _a.sent();
console.error("Error fetching client secret:", error_1);
setIntegrationError("Failed to initialize payment. Please try again.");
return [3 /*break*/, 4];
case 4: return [2 /*return*/];
}
});
}); };
if (isOpen && !clientSecret) {
fetchClientSecret();
}
}, [isOpen, publicKey, clientSecret, priceId, apiUrl]);
if (!isOpen) {
return null;
}
if (!stripePromise || !clientSecret) {
return (React__default.createElement(ModalOverlay$1, null,
React__default.createElement(ModalContent$1, null, !integrationError ? (React__default.createElement(LoadingWrapper$1, null,
React__default.createElement(SpinnerSvg$1, { width: "40", height: "40", viewBox: "0 0 40 40", fill: "none", xmlns: "http://www.w3.org/2000/svg" },
React__default.createElement("path", { d: "M20 4C11.163 4 4 11.163 4 20C4 28.837 11.163 36 20 36C28.837 36 36 28.837 36 20C36 11.163 28.837 4 20 4ZM20 32C12.268 32 6 25.732 6 18C6 10.268 12.268 4 20 4C27.732 4 34 10.268 34 18C34 25.732 27.732 32 20 32Z", fill: "#E6E6E6" }),
React__default.createElement("path", { d: "M20 4C11.163 4 4 11.163 4 20C4 28.837 11.163 36 20 36", stroke: "#5469D4", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" })))) : (React__default.createElement(CardError$1, { role: "alert" }, integrationError)))));
}
return (React__default.createElement("div", { className: "App" },
React__default.createElement(Elements, { stripe: stripePromise, options: { clientSecret: clientSecret } },
React__default.createElement(CheckoutForm$1, { onClose: onClose, clientSecret: clientSecret, userEmail: userEmail, apiUrl: apiUrl, onPaymentSuccess: onPaymentSuccess, onPaymentError: onPaymentError }))));
};
var spin$1 = keyframes(templateObject_1$1 || (templateObject_1$1 = __makeTemplateObject(["\n to {\n transform: rotate(360deg);\n }\n"], ["\n to {\n transform: rotate(360deg);\n }\n"])));
var ModalOverlay$1 = styled.div(templateObject_2$1 || (templateObject_2$1 = __makeTemplateObject(["\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 1000;\n"], ["\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 1000;\n"])));
var ModalContent$1 = styled.div(templateObject_3$1 || (templateObject_3$1 = __makeTemplateObject(["\n background: white;\n padding: 2rem;\n border-radius: 8px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n width: 100%;\n max-width: 500px;\n position: relative;\n\n h2 {\n margin-top: 2rem;\n text-align: left;\n }\n"], ["\n background: white;\n padding: 2rem;\n border-radius: 8px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n width: 100%;\n max-width: 500px;\n position: relative;\n\n h2 {\n margin-top: 2rem;\n text-align: left;\n }\n"])));
var PaymentForm$1 = styled.form(templateObject_4$1 || (templateObject_4$1 = __makeTemplateObject(["\n display: flex;\n flex-direction: column;\n gap: 1rem;\n"], ["\n display: flex;\n flex-direction: column;\n gap: 1rem;\n"])));
styled.input(templateObject_5$1 || (templateObject_5$1 = __makeTemplateObject(["\n padding: 12px;\n border: 1px solid ", ";\n border-radius: 4px;\n font-size: 16px;\n width: 100%;\n box-sizing: border-box;\n &:focus {\n outline: none;\n border-color: ", ";\n }\n"], ["\n padding: 12px;\n border: 1px solid ", ";\n border-radius: 4px;\n font-size: 16px;\n width: 100%;\n box-sizing: border-box;\n &:focus {\n outline: none;\n border-color: ", ";\n }\n"])), function (props) { return (props.$haserror ? "#fa755a" : "#e6e6e6"); }, function (props) { return (props.$haserror ? "#fa755a" : "#5469d4"); });
var SubmitButton$1 = styled.button(templateObject_6$1 || (templateObject_6$1 = __makeTemplateObject(["\n background: #5469d4;\n color: white;\n padding: 12px 16px;\n border-radius: 4px;\n border: 0;\n font-weight: 600;\n cursor: pointer;\n width: 100%;\n transition: all 0.2s ease;\n\n &:disabled {\n opacity: 0.5;\n cursor: default;\n }\n"], ["\n background: #5469d4;\n color: white;\n padding: 12px 16px;\n border-radius: 4px;\n border: 0;\n font-weight: 600;\n cursor: pointer;\n width: 100%;\n transition: all 0.2s ease;\n\n &:disabled {\n opacity: 0.5;\n cursor: default;\n }\n"])));
var CardError$1 = styled.div(templateObject_7$1 || (templateObject_7$1 = __makeTemplateObject(["\n color: #fa755a;\n text-align: left;\n font-size: 14px;\n margin-top: 12px;\n"], ["\n color: #fa755a;\n text-align: left;\n font-size: 14px;\n margin-top: 12px;\n"])));
var ResultMessage$1 = styled.p(templateObject_8$1 || (templateObject_8$1 = __makeTemplateObject(["\n color: #32cd32;\n text-align: center;\n margin-top: 12px;\n display: ", ";\n"], ["\n color: #32cd32;\n text-align: center;\n margin-top: 12px;\n display: ", ";\n"])), function (props) { return (props.hidden ? "none" : "block"); });
var Spinner$1 = styled.div(templateObject_9$1 || (templateObject_9$1 = __makeTemplateObject(["\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 3px solid rgba(255, 255, 255, 0.3);\n border-radius: 50%;\n border-top-color: white;\n animation: ", " 1s ease-in-out infinite;\n"], ["\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 3px solid rgba(255, 255, 255, 0.3);\n border-radius: 50%;\n border-top-color: white;\n animation: ", " 1s ease-in-out infinite;\n"])), spin$1);
var SpinnerSvg$1 = styled.svg(templateObject_10$1 || (templateObject_10$1 = __makeTemplateObject(["\n animation: ", " 1s linear infinite;\n"], ["\n animation: ", " 1s linear infinite;\n"])), spin$1);
var LoadingWrapper$1 = styled.div(templateObject_11$1 || (templateObject_11$1 = __makeTemplateObject(["\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 200px;\n"], ["\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 200px;\n"])));
var CloseButton$1 = styled.button(templateObject_12$1 || (templateObject_12$1 = __makeTemplateObject(["\n position: absolute;\n top: 1rem;\n right: 1rem;\n background: none;\n border: none;\n font-size: 1.5rem;\n cursor: pointer;\n color: #666;\n padding: 0.5rem;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: all 0.2s ease;\n width: 2rem;\n height: 2rem;\n\n &:hover {\n background-color: #f5f5f5;\n color: #333;\n }\n"], ["\n position: absolute;\n top: 1rem;\n right: 1rem;\n background: none;\n border: none;\n font-size: 1.5rem;\n cursor: pointer;\n color: #666;\n padding: 0.5rem;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: all 0.2s ease;\n width: 2rem;\n height: 2rem;\n\n &:hover {\n background-color: #f5f5f5;\n color: #333;\n }\n"])));
function CheckoutForm$1(_a) {
var _this = this;
var onClose = _a.onClose, clientSecret = _a.clientSecret, userEmail = _a.userEmail, apiUrl = _a.apiUrl, onPaymentSuccess = _a.onPaymentSuccess, onPaymentError = _a.onPaymentError;
var _b = useState(false), succeeded = _b[0], setSucceeded = _b[1];
var _c = useState(null), cardError = _c[0], setCardError = _c[1];
var _d = useState(false), processing = _d[0], setProcessing = _d[1];
var _e = useState(null), paymentStatus = _e[0], setPaymentStatus = _e[1];
var _f = useState(null), paymentIntentId = _f[0], setPaymentIntentId = _f[1];
var _g = useState(false), wsConnected = _g[0], setWsConnected = _g[1];
var wsRef = useRef(null);
var reconnectAttempts = useRef(0);
var maxReconnectAttempts = 3;
var stripe = useStripe();
var elements = useElements();
// Extract payment intent ID from client secret
useEffect(function () {
setPaymentIntentId(clientSecret.split("_secret_")[0]);
}, [clientSecret]);
// WebSocket connection logic
useEffect(function () {
if (!paymentIntentId)
return;
var connectWebSocket = function () {
// Determine WebSocket URL based on environment
var wsUrl = window.location.hostname.includes("localhost")
? "ws://localhost:8000".concat(WS_ROUTE)
: "wss://".concat(apiUrl.replace(/^https?:\/\//, "")).concat(WS_ROUTE);
console.log("Connecting to WebSocket:", wsUrl);
// Create WebSocket connection
var ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = function () {
console.log("WebSocket connection established");
setWsConnected(true);
reconnectAttempts.current = 0;
// Send client ID to server (using payment intent ID as client ID)
ws.send(JSON.stringify({ type: "init", paymentIntentId: paymentIntentId }));
console.log("Sent client ID to server:", paymentIntentId);
};
ws.onmessage = function (event) {
var _a, _b;
try {
var data = JSON.parse(event.data);
console.log("WebSocket message received:", data);
// Handle payment intent events
if (data.type && data.type.startsWith("payment_intent.")) {
var paymentIntent = data.data.object;
console.log("Payment intent event:", data.type, paymentIntent.id);
// Check if this is our payment intent
if (paymentIntentId && paymentIntent.id === paymentIntentId) {
console.log("Matched payment intent ID:", paymentIntent.id);
setPaymentStatus(data.type);
if (data.type === "payment_intent.succeeded") {
console.log("Payment succeeded!");
onPaymentSuccess(paymentIntent.id);
}
else if (data.type === "payment_intent.payment_failed") {
console.log("Payment failed:", (_a = paymentIntent.last_payment_error) === null || _a === void 0 ? void 0 : _a.message);
onPaymentError(((_b = paymentIntent.last_payment_error) === null || _b === void 0 ? void 0 : _b.message) || "Payment failed");
}
}
else {
console.log("Payment intent ID mismatch:", paymentIntent.id, "vs", paymentIntentId);
}
}
}
catch (error) {
console.error("Error parsing WebSocket message:", error);
}
};
ws.onerror = function (error) {
console.error("WebSocket error:", error);
setWsConnected(false);
};
ws.onclose = function () {
console.log("WebSocket connection closed");
setWsConnected(false);
// Attempt to reconnect if not at max attempts
if (reconnectAttempts.current < maxReconnectAttempts) {
reconnectAttempts.current += 1;
console.log("Reconnecting (attempt ".concat(reconnectAttempts.current, "/").concat(maxReconnectAttempts, ")..."));
setTimeout(connectWebSocket, 2000); // Wait 2 seconds before reconnecting
}
};
};
// Initial connection
connectWebSocket();
// Clean up WebSocket connection when component unmounts
return function () {
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
};
}, [paymentIntentId, apiUrl, onPaymentSuccess, onPaymentError]);
// Update UI based on payment status from WebSocket
useEffect(function () {
if (paymentStatus === "payment_intent.succeeded") {
setSucceeded(true);
setProcessing(false);
setCardError(null);
}
else if (paymentStatus === "payment_intent.payment_failed") {
setProcessing(false);
setCardError("Payment failed. Please try again.");
}
}, [paymentStatus]);
var handleSubmit = function (ev) { return __awaiter(_this, void 0, void 0, function () {
var submitError, error, wsUrl, ws_1, err_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
ev.preventDefault();
if (!stripe || !elements)
return [2 /*return*/];
setProcessing(true);
_a.label = 1;
case 1:
_a.trys.push([1, 4, , 5]);
return [4 /*yield*/, elements.submit()];
case 2:
submitError = (_a.sent()).error;
if (submitError) {
setCardError("Form submission failed: ".concat(submitError.message));
setProcessing(false);
return [2 /*return*/];
}
return [4 /*yield*/, stripe.confirmPayment({
elements: elements,
confirmParams: {
receipt_email: userEmail,
return_url: window.location.href,
},
redirect: "if_required",
})];
case 3:
error = (_a.sent()).error;
if (error) {
setCardError("Payment failed ".concat(error.message));
setProcessing(false);
onPaymentError(error.message || "Payment failed");
}
else {
// Payment intent was created but not yet confirmed
// We'll wait for the WebSocket notification for the final status
setCardError(null);
// If we have a payment intent ID but no WebSocket connection, try to reconnect
if (paymentIntentId && !wsConnected) {
console.log("Payment submitted but WebSocket not connected. Attempting to reconnect...");
if (wsRef.current) {
wsRef.current.close();
}
reconnectAttempts.current = 0;
wsUrl = window.location.hostname.includes("localhost")
? "ws://localhost:8000".concat(WS_ROUTE)
: "wss://".concat(apiUrl.replace(/^https?:\/\//, "")).concat(WS_ROUTE);
ws_1 = new WebSocket(wsUrl);
wsRef.current = ws_1;
ws_1.onopen = function () {
console.log("WebSocket reconnected after payment submission");
setWsConnected(true);
ws_1.send(JSON.stringify({ type: "init", clientId: paymentIntentId }));
};
ws_1.onmessage = function (event) {
var _a;
try {
var data = JSON.parse(event.data);
console.log("WebSocket message after reconnection:", data);
if (data.type && data.type.startsWith("payment_intent.")) {
var paymentIntent = data.data.object;
if (paymentIntentId && paymentIntent.id === paymentIntentId) {
setPaymentStatus(data.type);
if (data.type === "payment_intent.succeeded") {
onPaymentSuccess(paymentIntent.id);
}
else if (data.type === "payment_intent.payment_failed") {
onPaymentError(((_a = paymentIntent.last_payment_error) === null || _a === void 0 ? void 0 : _a.message) ||
"Payment failed");
}
}
}
}
catch (error) {
console.error("Error parsing WebSocket message after reconnection:", error);
}
};
}
}
return [3 /*break*/, 5];
case 4:
err_1 = _a.sent();
setCardError("An unexpected error occurred. Please try again.");
setProcessing(false);
console.error("Payment error:", err_1);
onPaymentError("An unexpected error occurred. Please try again.");
return [3 /*break*/, 5];
case 5: return [2 /*return*/];
}
});
}); };
return (React__default.createElement(ModalOverlay$1, null,
React__default.createElement(ModalContent$1, null,
onClose && (React__default.createElement(CloseButton$1, { onClick: onClose, "aria-label": "Close modal" }, "\u00D7")),
React__default.createElement("h2", null, "Complete Payment"),
React__default.createElement(PaymentForm$1, { onSubmit: handleSubmit },
React__default.createElement(PaymentElement, { id: "payment-element" }),
React__default.createElement(SubmitButton$1, null,
React__default.createElement("span", null, processing ? React__default.createElement(Spinner$1, null) : "Pay now")),
cardError && React__default.createElement(CardError$1, { role: "alert" }, cardError),
React__default.createElement(ResultMessage$1, { hidden: !succeeded }, "Payment succeeded! Thank you for your purchase.")))));
}
var templateObject_1$1, templateObject_2$1, templateObject_3$1, templateObject_4$1, templateObject_5$1, templateObject_6$1, templateObject_7$1, templateObject_8$1, templateObject_9$1, templateObject_10$1, templateObject_11$1, templateObject_12$1;
function cn() {
var inputs = [];
for (var _i = 0; _i < arguments.length; _i++) {
inputs[_i] = arguments[_i];
}
return twMerge(clsx(inputs));
}
var currencySymbol = function (currency) {
switch (currency.toLowerCase()) {
case "usd":
return "$";
case "eur":
return "€";
case "gbp":
return "£";
case "cad":
return "C$";
case "aud":
return "A$";
default:
return currency.toUpperCase();
}
};
var buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", {
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
function Button(_a) {
var className = _a.className, variant = _a.variant, size = _a.size, _b = _a.asChild, asChild = _b === void 0 ? false : _b, props = __rest(_a, ["className", "variant", "size", "asChild"]);
var Comp = asChild ? Slot : "button";
return (React.createElement(Comp, __assign({ "data-slot": "button", className: cn(buttonVariants({ variant: variant, size: size, className: className })) }, props)));
}
function Card(_a) {
var className = _a.className, props = __rest(_a, ["className"]);
return (React.createElement("div", __assign({ "data-slot": "card", className: cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className) }, props)));
}
function CardHeader(_a) {
var className = _a.className, props = __rest(_a, ["className"]);
return (React.createElement("div", __assign({ "data-slot": "card-header", className: cn("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", className) }, props)));
}
function CardTitle(_a) {
var className = _a.className, props = __rest(_a, ["className"]);
return (React.createElement("div", __assign({ "data-slot": "card-title", className: cn("leading-none font-semibold", className) }, props)));
}
function CardDescription(_a) {
var className = _a.className, props = __rest(_a, ["className"]);
return (React.createElement("div", __assign({ "data-slot": "card-description", className: cn("text-muted-foreground text-sm", className) }, props)));
}
function CardContent(_a) {
var className = _a.className, props = __rest(_a, ["className"]);
return (React.createElement("div", __assign({ "data-slot": "card-content", className: cn("px-6", className) }, props)));
}
function CardFooter(_a) {
var className = _a.className, props = __rest(_a, ["className"]);
return (React.createElement("div", __assign({ "data-slot": "card-footer", className: cn("flex items-center px-6 [.border-t]:pt-6", className) }, props)));
}
var badgeVariants = cva("inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", {
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary: "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive: "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
function Badge(_a) {
var className = _a.className, variant = _a.variant, _b = _a.asChild, asChild = _b === void 0 ? false : _b, props = __rest(_a, ["className", "variant", "asChild"]);
var Comp = asChild ? Slot : "span";
return (React.createElement(Comp, __assign({ "data-slot": "badge", className: cn(badgeVariants({ variant: variant }), className) }, props)));
}
var alertVariants = cva("relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", {
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive: "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
});
function Alert(_a) {
var className = _a.className, variant = _a.variant, props = __rest(_a, ["className", "variant"]);
return (React.createElement("div", __assign({ "data-slot": "alert", role: "alert", className: cn(alertVariants({ variant: variant }), className) }, props)));
}
function AlertTitle(_a) {
var className = _a.className, props = __rest(_a, ["className"]);
return (React.createElement("div", __assign({ "data-slot": "alert-title", className: cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className) }, props)));
}
function AlertDescription(_a) {
var className = _a.className, props = __rest(_a, ["className"]);
return (React.createElement("div", __assign({ "data-slot": "alert-description", className: cn("text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", className) }, props)));
}
var validateEmail = function (email) {
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
var PaymentModalDemo = function App(_a) {
var _this = this;
var isOpen = _a.isOpen, publicKey = _a.publicKey, plan = _a.plan, userEmail = _a.userEmail; _a.userId; var onClose = _a.onClose, onPaymentSuccess = _a.onPaymentSuccess, onPaymentError = _a.onPaymentError,
// TODO: Change to production URL
_b = _a.apiUrl,
// TODO: Change to production URL
apiUrl = _b === void 0 ? "https://www.withpretzel.com/api" : _b;
var _c = useState(null), stripePromise = _c[0], setStripePromise = _c[1];
var _d = useState(null), clientSecret = _d[0], setClientSecret = _d[1];
var _e = useState(null), integrationError = _e[0], setIntegrationError = _e[1];
var _f = useState(userEmail || ""), customerEmail = _f[0], setCustomerEmail = _f[1];
var _g = useState(""), customerName = _g[0], setCustomerName = _g[1];
var _h = useState(""), businessName = _h[0], setBusinessName = _h[1];
var _j = useState(""), customerAddress = _j[0], setCustomerAddress = _j[1];
var _k = useState(""), customerZip = _k[0], setcustomerZip = _k[1];
var _l = useState(""), customerCity = _l[0], setCustomerCity = _l[1];
var _m = useState(""), customerCountry = _m[0], setCustomerCountry = _m[1];
var _o = useState(false), showPaymentForm = _o[0], setShowPaymentForm = _o[1];
var _p = useState(false), emailSubmitting = _p[0], setEmailSubmitting = _p[1];
if (!plan) {
return null;
}
var handleEmailSubmit = function (e) { return __awaiter(_this, void 0, void 0, function () {
var response, data, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
e.preventDefault();
if (!validateEmail(customerEmail)) {
setIntegrationError("Please enter a valid email address");
return [2 /*return*/];
}
if (!customerName ||
!businessName ||
!customerAddress ||
!customerZip ||
!customerCity ||
!customerCountry) {
setIntegrationError("Please fill in all required fields");
return [2 /*return*/];
}
setEmailSubmitting(true);
_a.label = 1;
case 1:
_a.trys.push([1, 4, 5, 6]);
return [4 /*yield*/, fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
publicKey: publicKey,
amount: plan.price * 100,
customerEmail: customerEmail,
customerName: customerName,
businessName: businessName,
customerAddress: customerAddress,
customerZip: customerZip,
customerCity: customerCity,
customerCountry: customerCountry,
}),
})];
case 2:
response = _a.sent();
return [4 /*yield*/, response.json()];
case 3:
data = _a.sent();
if (!data.stripePublishableKey) {
setIntegrationError("Failed to initialize payment: ".concat(data.error));
setEmailSubmitting(false);
return [2 /*return*/];
}
if (!data.clientSecret) {
setIntegrationError("Failed to initialize payment: ".concat(data.error));
setEmailSubmitting(false);
return [2 /*return*/];
}
setClientSecret(data.clientSecret);
setStripePromise(loadStripe(data.stripePublishableKey));
setShowPaymentForm(true);
return [3 /*break*/, 6];
case 4:
error_1 = _a.sent();
console.error("Error fetching client secret:", error_1);
setIntegrationError("Failed to initialize payment. Please try again.");
return [3 /*break*/, 6];
case 5:
setEmailSubmitting(false);
return [7 /*endfinally*/];
case 6: return [2 /*return*/];
}
});
}); };
if (!isOpen) {
return null;
}
if (!showPaymentForm) {
return (React__default.createElement(ModalOverlay, null,
React__default.createElement(ModalContent, null,
onClose && (React__default.createElement(CloseButton, { onClick: onClose, "aria-label": "Close modal" }, "\u00D7")),
emailSubmitting ? (React__default.createElement(LoadingWrapper, null,
React__default.createElement(SpinnerSvg, { width: "40", height: "40", viewBox: "0 0 40 40", fill: "none", xmlns: "http://www.w3.org/2000/svg" },
React__default.createElement("path", { d: "M20 4C11.163 4 4 11.163 4 20C4 28.837 11.163 36 20 36C28.837 36 36 28.837 36 20C36 11.163 28.837 4 20 4ZM20 32C12.268 32 6 25.732 6 18C6 10.268 12.268 4 20 4C27.732 4 34 10.268 34 18C34 25.732 27.732 32 20 32Z", fill: "#E6E6E6" }),
React__default.createElement("path", { d: "M20 4C11.163 4 4 11.163 4 20C4 28.837 11.163 36 20 36", stroke: "#5469D4", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" })))) : (React__default.createElement("form", { onSubmit: handleEmailSubmit },
React__default.createElement(Input, { type: "email", placeholder: "Email address", value: customerEmail, onChange: function (e) {
setCustomerEmail(e.target.value);
setIntegrationError(null);
}, required: true, "$haserror": !!integrationError, onKeyDown: function (e) {
var _a;
if (e.key === "Enter") {
(_a = e.currentTarget.form) === null || _a === void 0 ? void 0 : _a.requestSubmit();
}
} }),
React__default.createElement(Input, { type: "text", placeholder: "Full name", value: customerName, onChange: function (e) {
setCustomerName(e.target.value);
setIntegrationError(null);
}, required: true, "$haserror": !!integrationError }),
React__default.createElement(Input, { type: "text", placeholder: "Business name", value: businessName, onChange: function (e) {
setBusinessName(e.target.value);
setIntegrationError(null);
}, required: true, "$haserror": !!integrationError }),
React__default.createElement(Input, { type: "text", placeholder: "Address", value: customerAddress, onChange: function (e) {
setCustomerAddress(e.target.value);
setIntegrationError(null);
}, required: true, "$haserror": !!integrationError }),
React__default.createElement(Input, { type: "text", placeholder: "ZIP code", value: customerZip, onChange: function (e) {
setcustomerZip(e.target.value);
setIntegrationError(null);
}, required: true, "$haserror": !!integrationError }),
React__default.createElement(Input, { type: "text", placeholder: "City", value: customerCity, onChange: function (e) {
setCustomerCity(e.target.value);
setIntegrationError(null);
}, required: true, "$haserror": !!integrationError }),
React__default.createElement(Input, { type: "text", placeholder: "Country", value: customerCountry, onChange: function (e) {
setCustomerCountry(e.target.value);
setIntegrationError(null);
}, required: true, "$haserror": !!integrationError }),
integrationError && (React__default.createElement(CardError, { role: "alert" }, integrationError)),
React__default.createElement(SubmitButton, { type: "submit" }, "Continue to payment"))))));
}
if (!stripePromise || !clientSecret) {
return (React__default.createElement(ModalOverlay, null,
React__default.createElement(ModalContent, null,
onClose && (React__default.createElement(CloseButton, { onClick: onClose, "aria-label": "Close modal" }, "\u00D7")),
React__default.createElement(LoadingWrapper, null,
React__default.createElement(SpinnerSvg, { width: "40", height: "40", viewBox: "0 0 40 40", fill: "none", xmlns: "http://www.w3.org/2000/svg" },
React__default.createElement("path", { d: "M20 4C11.163 4 4 11.163 4 20C4 28.837 11.163 36 20 36C28.837 36 36 28.837 36 20C36 11.163 28.837 4 20 4ZM20 32C12.268 32 6 25.732 6 18C6 10.268 12.268 4 20 4C27.732 4 34 10.268 34 18C34 25.732 27.732 32 20 32Z", fill: "#E6E6E6" }),
React__default.createElement("path", { d: "M20 4C11.163 4 4 11.163 4 20C4 28.837 11.163 36 20 36", stroke: "#5469D4", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" }))))));
}
return (React__default.createElement("div", { className: "App" },
React__default.createElement(Elements, { stripe: stripePromise, options: { clientSecret: clientSecret } },
React__default.createElement(CheckoutForm, { onClose: onClose, clientSecret: clientSecret, userEmail: customerEmail, plan: plan, apiUrl: apiUrl, onPaymentSuccess: onPaymentSuccess || (function () { return console.log("payment success"); }), onPaymentError: onPaymentError || (function () { return console.log("payment error"); }) }))));
};
var spin = keyframes(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n to {\n transform: rotate(360deg);\n }\n"], ["\n to {\n transform: rotate(360deg);\n }\n"])));
var ModalOverlay = styled.div(templateObject_2 || (templateObject_2 = __makeTemplateObject(["\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 1000;\n"], ["\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 1000;\n"])));
var ModalContent = styled.div(templateObject_3 || (templateObject_3 = __makeTemplateObject(["\n background: white;\n padding: 2rem;\n border-radius: 8px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n width: 100%;\n max-width: 500px;\n position: relative;\n\n h2 {\n margin-top: 2rem;\n text-align: left;\n }\n"], ["\n background: white;\n padding: 2rem;\n border-radius: 8px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n width: 100%;\n max-width: 500px;\n position: relative;\n\n h2 {\n margin-top: 2rem;\n text-align: left;\n }\n"])));
var PaymentForm = styled.form(templateObject_4 || (templateObject_4 = __makeTemplateObject(["\n display: flex;\n flex-direction: column;\n gap: 1rem;\n"], ["\n display: flex;\n flex-direction: column;\n gap: 1rem;\n"])));
var Input = styled.input(templateObject_5 || (templateObject_5 = __makeTemplateObject(["\n padding: 12px;\n margin-top: 2rem;\n margin-bottom: 1rem;\n border: 1px solid ", ";\n border-radius: 4px;\n font-size: 16px;\n width: 100%;\n box-sizing: border-box;\n &:focus {\n outline: none;\n border-color: ", ";\n }\n"], ["\n padding: 12px;\n margin-top: 2rem;\n margin-bottom: 1rem;\n border: 1px solid ", ";\n border-radius: 4px;\n font-size: 16px;\n width: 100%;\n box-sizing: border-box;\n &:focus {\n outline: none;\n border-color: ", ";\n }\n"])), function (props) { return (props.$haserror ? "#fa755a" : "#e6e6e6"); }, function (props) { return (props.$haserror ? "#fa755a" : "#5469d4"); });
var SubmitButton = styled.button(templateObject_6 || (templateObject_6 = __makeTemplateObject(["\n background: #5469d4;\n color: white;\n padding: 12px 16px;\n border-radius: 4px;\n border: 0;\n font-weight: 600;\n cursor: pointer;\n width: 100%;\n transition: all 0.2s ease;\n\n &:disabled {\n opacity: 0.5;\n cursor: default;\n }\n"], ["\n background: #5469d4;\n color: white;\n padding: 12px 16px;\n border-radius: 4px;\n border: 0;\n font-weight: 600;\n cursor: pointer;\n width: 100%;\n transition: all 0.2s ease;\n\n &:disabled {\n opacity: 0.5;\n cursor: default;\n }\n"])));
var CardError = styled.div(templateObject_7 || (templateObject_7 = __makeTemplateObject(["\n color: #fa755a;\n text-align: left;\n font-size: 14px;\n margin-top: 12px;\n"], ["\n color: #fa755a;\n text-align: left;\n font-size: 14px;\n margin-top: 12px;\n"])));
var ResultMessage = styled.p(templateObject_8 || (templateObject_8 = __makeTemplateObject(["\n color: #32cd32;\n text-align: center;\n margin-top: 12px;\n display: ", ";\n"], ["\n color: #32cd32;\n text-align: center;\n margin-top: 12px;\n display: ", ";\n"])), function (props) { return (props.hidden ? "none" : "block"); });
var Spinner = styled.div(templateObject_9 || (templateObject_9 = __makeTemplateObject(["\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 3px solid rgba(255, 255, 255, 0.3);\n border-radius: 50%;\n border-top-color: white;\n animation: ", " 1s ease-in-out infinite;\n"], ["\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 3px solid rgba(255, 255, 255, 0.3);\n border-radius: 50%;\n border-top-color: white;\n animation: ", " 1s ease-in-out infinite;\n"])), spin);
var SpinnerSvg = styled.svg(templateObject_10 || (templateObject_10 = __makeTemplateObject(["\n animation: ", " 1s linear infinite;\n"], ["\n animation: ", " 1s linear infinite;\n"])), spin);
var LoadingWrapper = styled.div(templateObject_11 || (templateObject_11 = __makeTemplateObject(["\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 200px;\n"], ["\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 200px;\n"])));
var CloseButton = styled.button(templateObject_12 || (templateObject_12 = __makeTemplateObject(["\n position: absolute;\n top: 1rem;\n right: 1rem;\n background: none;\n border: none;\n font-size: 1.5rem;\n cursor: pointer;\n color: #666;\n padding: 0.5rem;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: all 0.2s ease;\n width: 2rem;\n height: 2rem;\n\n &:hover {\n background-color: #f5f5f5;\n color: #333;\n }\n"], ["\n position: absolute;\n top: 1rem;\n right: 1rem;\n background: none;\n border: none;\n font-size: 1.5rem;\n cursor: pointer;\n color: #666;\n padding: 0.5rem;\n line-height: 1;\n display: flex;\n align-items: center;\n justif