@splito/sdk
Version:
Official JavaScript/TypeScript SDK for Splito - Split payments across multiple recipients
958 lines (942 loc) • 40.5 kB
JavaScript
// src/sdk/modal-initializer.tsx
import { createRoot } from "react-dom/client";
// src/sdk/SplitModal.tsx
import { useState as useState2, useEffect as useEffect2 } from "react";
import { DollarSign, Users, CreditCard, CheckCircle, Clock, AlertTriangle, UserPlus, Mail, Send, Building2, RefreshCw, QrCode, Copy, Share2, Loader2 } from "lucide-react";
// src/components/ui/button.tsx
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva } from "class-variance-authority";
// src/lib/utils.ts
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs) {
return twMerge(clsx(inputs));
}
// src/components/ui/button.tsx
import { jsx } from "react/jsx-runtime";
var buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
);
var Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return /* @__PURE__ */ jsx(Comp, { className: cn(buttonVariants({ variant, size, className })), ref, ...props });
}
);
Button.displayName = "Button";
// src/components/ui/card.tsx
import * as React2 from "react";
import { jsx as jsx2 } from "react/jsx-runtime";
var Card = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx2("div", { ref, className: cn("rounded-lg border bg-card text-card-foreground shadow-sm", className), ...props }));
Card.displayName = "Card";
var CardHeader = React2.forwardRef(
({ className, ...props }, ref) => /* @__PURE__ */ jsx2("div", { ref, className: cn("flex flex-col space-y-1.5 p-6", className), ...props })
);
CardHeader.displayName = "CardHeader";
var CardTitle = React2.forwardRef(
({ className, ...props }, ref) => /* @__PURE__ */ jsx2("h3", { ref, className: cn("text-2xl font-semibold leading-none tracking-tight", className), ...props })
);
CardTitle.displayName = "CardTitle";
var CardDescription = React2.forwardRef(
({ className, ...props }, ref) => /* @__PURE__ */ jsx2("p", { ref, className: cn("text-sm text-muted-foreground", className), ...props })
);
CardDescription.displayName = "CardDescription";
var CardContent = React2.forwardRef(
({ className, ...props }, ref) => /* @__PURE__ */ jsx2("div", { ref, className: cn("p-6 pt-0", className), ...props })
);
CardContent.displayName = "CardContent";
var CardFooter = React2.forwardRef(
({ className, ...props }, ref) => /* @__PURE__ */ jsx2("div", { ref, className: cn("flex items-center p-6 pt-0", className), ...props })
);
CardFooter.displayName = "CardFooter";
// src/components/ui/input.tsx
import * as React3 from "react";
import { jsx as jsx3 } from "react/jsx-runtime";
var Input = React3.forwardRef(
({ className, type, ...props }, ref) => {
return /* @__PURE__ */ jsx3(
"input",
{
type,
className: cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
),
ref,
...props
}
);
}
);
Input.displayName = "Input";
// src/components/ui/label.tsx
import * as React4 from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva as cva2 } from "class-variance-authority";
import { jsx as jsx4 } from "react/jsx-runtime";
var labelVariants = cva2("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
var Label = React4.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx4(LabelPrimitive.Root, { ref, className: cn(labelVariants(), className), ...props }));
Label.displayName = LabelPrimitive.Root.displayName;
// src/components/ui/badge.tsx
import { cva as cva3 } from "class-variance-authority";
import { jsx as jsx5 } from "react/jsx-runtime";
var badgeVariants = cva3(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
success: "border-transparent bg-success/10 text-success hover:bg-success/20",
warning: "border-transparent bg-warning/10 text-warning hover:bg-warning/20",
error: "border-transparent bg-destructive/10 text-destructive hover:bg-destructive/20"
}
},
defaultVariants: {
variant: "default"
}
}
);
function Badge({ className, variant, ...props }) {
return /* @__PURE__ */ jsx5("div", { className: cn(badgeVariants({ variant }), className), ...props });
}
// src/components/ui/dialog.tsx
import * as React5 from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { jsx as jsx6, jsxs } from "react/jsx-runtime";
var Dialog = DialogPrimitive.Root;
var DialogPortal = DialogPrimitive.Portal;
var DialogOverlay = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx6(
DialogPrimitive.Overlay,
{
ref,
className: cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
),
...props
}
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
var DialogContent = React5.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(DialogPortal, { children: [
/* @__PURE__ */ jsx6(DialogOverlay, {}),
/* @__PURE__ */ jsxs(
DialogPrimitive.Content,
{
ref,
className: cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
),
...props,
children: [
children,
/* @__PURE__ */ jsxs(DialogPrimitive.Close, { className: "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none", children: [
/* @__PURE__ */ jsx6(X, { className: "h-4 w-4" }),
/* @__PURE__ */ jsx6("span", { className: "sr-only", children: "Close" })
] })
]
}
)
] }));
DialogContent.displayName = DialogPrimitive.Content.displayName;
var DialogHeader = ({ className, ...props }) => /* @__PURE__ */ jsx6("div", { className: cn("flex flex-col space-y-1.5 text-center sm:text-left", className), ...props });
DialogHeader.displayName = "DialogHeader";
var DialogFooter = ({ className, ...props }) => /* @__PURE__ */ jsx6("div", { className: cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className), ...props });
DialogFooter.displayName = "DialogFooter";
var DialogTitle = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx6(
DialogPrimitive.Title,
{
ref,
className: cn("text-lg font-semibold leading-none tracking-tight", className),
...props
}
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
var DialogDescription = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx6(DialogPrimitive.Description, { ref, className: cn("text-sm text-muted-foreground", className), ...props }));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
// src/components/ui/drawer.tsx
import * as React6 from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
var Drawer = ({ shouldScaleBackground = true, ...props }) => /* @__PURE__ */ jsx7(DrawerPrimitive.Root, { shouldScaleBackground, ...props });
Drawer.displayName = "Drawer";
var DrawerTrigger = DrawerPrimitive.Trigger;
var DrawerPortal = DrawerPrimitive.Portal;
var DrawerClose = DrawerPrimitive.Close;
var DrawerOverlay = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(DrawerPrimitive.Overlay, { ref, className: cn("fixed inset-0 z-50 bg-black/80", className), ...props }));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
var DrawerContent = React6.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs2(DrawerPortal, { children: [
/* @__PURE__ */ jsx7(DrawerOverlay, {}),
/* @__PURE__ */ jsxs2(
DrawerPrimitive.Content,
{
ref,
className: cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
),
...props,
children: [
/* @__PURE__ */ jsx7("div", { className: "mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" }),
children
]
}
)
] }));
DrawerContent.displayName = "DrawerContent";
var DrawerHeader = ({ className, ...props }) => /* @__PURE__ */ jsx7("div", { className: cn("grid gap-1.5 p-4 text-center sm:text-left", className), ...props });
DrawerHeader.displayName = "DrawerHeader";
var DrawerFooter = ({ className, ...props }) => /* @__PURE__ */ jsx7("div", { className: cn("mt-auto flex flex-col gap-2 p-4", className), ...props });
DrawerFooter.displayName = "DrawerFooter";
var DrawerTitle = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(
DrawerPrimitive.Title,
{
ref,
className: cn("text-lg font-semibold leading-none tracking-tight", className),
...props
}
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
var DrawerDescription = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx7(DrawerPrimitive.Description, { ref, className: cn("text-sm text-muted-foreground", className), ...props }));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
// src/hooks/use-toast.ts
import * as React7 from "react";
var TOAST_LIMIT = 1;
var TOAST_REMOVE_DELAY = 1e6;
var count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
var toastTimeouts = /* @__PURE__ */ new Map();
var addToRemoveQueue = (toastId) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
var reducer = (state, action) => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t)
};
case "DISMISS_TOAST": {
const { toastId } = action;
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast2) => {
addToRemoveQueue(toast2.id);
});
}
return {
...state,
toasts: state.toasts.map(
(t) => t.id === toastId || toastId === void 0 ? {
...t,
open: false
} : t
)
};
}
case "REMOVE_TOAST":
if (action.toastId === void 0) {
return {
...state,
toasts: []
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId)
};
}
};
var listeners = [];
var memoryState = { toasts: [] };
function dispatch(action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
function toast({ ...props }) {
const id = genId();
const update = (props2) => dispatch({
type: "UPDATE_TOAST",
toast: { ...props2, id }
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
}
}
});
return {
id,
dismiss,
update
};
}
function useToast() {
const [state, setState] = React7.useState(memoryState);
React7.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId })
};
}
// src/integrations/supabase/client.ts
import { createClient } from "@supabase/supabase-js";
var SUPABASE_URL = "https://mhskwcysroougxpjacdz.supabase.co";
var SUPABASE_PUBLISHABLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oc2t3Y3lzcm9vdWd4cGphY2R6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTg1OTQ4NjMsImV4cCI6MjA3NDE3MDg2M30.7R5jmf1okIVMQo10w-H0pfqkukfq90vXLJEkN7IvzBY";
var supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
auth: {
storage: localStorage,
persistSession: true,
autoRefreshToken: true
}
});
// src/sdk/SplitModal.tsx
import QRCode from "qrcode";
import { Fragment, jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
function SplitModalContent({ token, baseUrl }) {
const { toast: toast2 } = useToast();
const [paymentIntent, setPaymentIntent] = useState2(null);
const [recipients, setRecipients] = useState2([]);
const [loading, setLoading] = useState2(true);
const [paymentForm, setPaymentForm] = useState2({
name: "",
email: "",
amount: 0
});
const [selectedAmount, setSelectedAmount] = useState2(null);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState2("stripe");
const [inviteForm, setInviteForm] = useState2({
name: "",
email: ""
});
const [isInviting, setIsInviting] = useState2(false);
const [showInviteForm, setShowInviteForm] = useState2(false);
const [isGeneratingQR, setIsGeneratingQR] = useState2(false);
const [showQRInvitation, setShowQRInvitation] = useState2(false);
const [qrCodeData, setQrCodeData] = useState2(null);
const [invitationUrl, setInvitationUrl] = useState2(null);
const [isProcessingPayment, setIsProcessingPayment] = useState2(false);
useEffect2(() => {
if (token) {
fetchPaymentDetails();
}
}, [token]);
useEffect2(() => {
if (recipients.length > 0 && !paymentForm.name) {
const urlParams = new URLSearchParams(window.location.search);
const inviteeEmail = urlParams.get("invitee");
if (inviteeEmail) {
const matchingRecipient = recipients.find(
(r) => r.email.toLowerCase() === decodeURIComponent(inviteeEmail).toLowerCase()
);
if (matchingRecipient) {
setPaymentForm((prev) => ({
...prev,
name: matchingRecipient.name,
email: matchingRecipient.email
}));
toast2({
title: "Welcome!",
description: `Hi ${matchingRecipient.name}, we've pre-filled your details.`
});
}
}
}
}, [recipients]);
const fetchPaymentDetails = async () => {
try {
const response = await fetch(`${baseUrl}/functions/v1/get-splitting-link`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ token })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
const data = await response.json();
if (!data) {
throw new Error("No data received");
}
setPaymentIntent(data);
setRecipients(data.recipients || []);
} catch (error) {
console.error("Error fetching payment details:", error);
toast2({
title: "Error",
description: error instanceof Error ? error.message : "Failed to load payment details",
variant: "destructive"
});
} finally {
setLoading(false);
}
};
const processPayment = async () => {
if (!paymentForm.name || !paymentForm.email || !selectedAmount) {
toast2({
title: "Error",
description: "Please fill in all required fields",
variant: "destructive"
});
return;
}
setIsProcessingPayment(true);
try {
if (selectedPaymentMethod === "stripe") {
const { data, error } = await supabase.functions.invoke("process-connected-payment", {
body: {
payment_intent_id: paymentIntent?.id,
payer_name: paymentForm.name,
payer_email: paymentForm.email,
amount_cents: selectedAmount
}
});
if (error) throw error;
if (data?.checkout_url) {
toast2({
title: "Redirecting to Payment",
description: "You'll be redirected to complete your payment securely."
});
await new Promise((resolve) => setTimeout(resolve, 500));
window.open(data.checkout_url, "_blank");
}
} else if (selectedPaymentMethod === "paypal") {
const { data, error } = await supabase.functions.invoke("create-paypal-payment", {
body: {
payment_intent_id: paymentIntent?.id,
payer_name: paymentForm.name,
payer_email: paymentForm.email,
amount_cents: selectedAmount
}
});
if (error) throw error;
if (data?.approval_url) {
toast2({
title: "Redirecting to PayPal",
description: "You'll be redirected to PayPal to complete your payment."
});
await new Promise((resolve) => setTimeout(resolve, 500));
window.open(data.approval_url, "_blank");
}
}
} catch (error) {
toast2({
title: "Payment Error",
description: error.message || "Failed to process payment",
variant: "destructive"
});
} finally {
setIsProcessingPayment(false);
}
};
const generateQRInvitation = async () => {
if (showQRInvitation) {
setShowQRInvitation(false);
return;
}
setIsGeneratingQR(true);
try {
const url = `${window.location.origin}/split/${token}`;
const qrData = await QRCode.toDataURL(url, {
width: 300,
margin: 2,
color: {
dark: "#000000",
light: "#FFFFFF"
}
});
setQrCodeData(qrData);
setInvitationUrl(url);
setShowQRInvitation(true);
} catch (error) {
toast2({
title: "Error",
description: "Failed to generate QR code",
variant: "destructive"
});
} finally {
setIsGeneratingQR(false);
}
};
const copyInvitationUrl = () => {
if (invitationUrl) {
navigator.clipboard.writeText(invitationUrl);
toast2({
title: "Copied!",
description: "Invitation link copied to clipboard"
});
}
};
const shareInvitation = async () => {
if (navigator.share && invitationUrl) {
try {
await navigator.share({
title: "Split Payment Invitation",
text: "Join me in splitting this payment",
url: invitationUrl
});
} catch (error) {
copyInvitationUrl();
}
} else {
copyInvitationUrl();
}
};
const sendInvitation = async () => {
if (!inviteForm.name || !inviteForm.email) {
toast2({
title: "Error",
description: "Please fill in all invitation fields",
variant: "destructive"
});
return;
}
if (!paymentIntent || !token) return;
setIsInviting(true);
try {
const { data, error } = await supabase.functions.invoke("send-invitation", {
body: {
paymentIntentId: paymentIntent.id,
inviteeEmail: inviteForm.email,
inviteeName: inviteForm.name,
inviterName: "Anonymous",
inviterEmail: "noreply@splito.com",
splittingToken: token
}
});
if (error) throw error;
const newRecipient = {
id: crypto.randomUUID(),
name: inviteForm.name,
email: inviteForm.email,
amount_cents: 0,
status: "pending",
payment_completed_at: null
};
setRecipients((prev) => [newRecipient, ...prev]);
toast2({
title: "Invitation Sent!",
description: `${inviteForm.name} has been invited and added to participants.`
});
setInviteForm({ name: "", email: "" });
setShowInviteForm(false);
fetchPaymentDetails();
} catch (error) {
console.error("Error sending invitation:", error);
toast2({
title: "Error",
description: error.message || "Failed to send invitation",
variant: "destructive"
});
} finally {
setIsInviting(false);
}
};
if (loading) {
return /* @__PURE__ */ jsx8("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsx8(Loader2, { className: "w-8 h-8 animate-spin text-primary" }) });
}
if (!paymentIntent) {
return /* @__PURE__ */ jsxs3("div", { className: "p-6 text-center", children: [
/* @__PURE__ */ jsx8(AlertTriangle, { className: "w-12 h-12 text-warning mx-auto mb-4" }),
/* @__PURE__ */ jsx8("h2", { className: "text-xl font-semibold mb-2", children: "Splitting Link Not Found" }),
/* @__PURE__ */ jsx8("p", { className: "text-muted-foreground", children: "This splitting link is invalid or has expired." })
] });
}
const totalPaid = recipients.filter((r) => r.status === "completed").reduce((sum, r) => sum + r.amount_cents, 0);
const remainingAmount = paymentIntent.amount_cents - totalPaid;
const progressPercentage = totalPaid / paymentIntent.amount_cents * 100;
const quickAmounts = [
{ label: "Equal Split", value: Math.ceil(remainingAmount / Math.max(1, (paymentIntent.max_participants || 10) - recipients.length)) },
{ label: "25%", value: Math.ceil(paymentIntent.amount_cents * 0.25) },
{ label: "50%", value: Math.ceil(paymentIntent.amount_cents * 0.5) },
{ label: "Full Amount", value: remainingAmount }
].filter((amount) => amount.value <= remainingAmount && amount.value > 0);
return /* @__PURE__ */ jsxs3("div", { className: "space-y-6 max-h-[80vh] overflow-y-auto px-1", children: [
/* @__PURE__ */ jsxs3("div", { className: "text-center", children: [
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-center gap-4 mb-4", children: [
/* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center", children: [
/* @__PURE__ */ jsx8("div", { className: "w-12 h-12 rounded-xl flex items-center justify-center mb-1 overflow-hidden", children: paymentIntent?.business?.logo_url ? /* @__PURE__ */ jsx8(
"img",
{
src: paymentIntent.business.logo_url,
alt: `${paymentIntent.business.name} logo`,
className: "w-full h-full object-cover rounded-xl"
}
) : /* @__PURE__ */ jsx8("div", { className: "w-12 h-12 bg-gradient-to-br from-primary to-primary-hover rounded-xl flex items-center justify-center", children: /* @__PURE__ */ jsx8(Building2, { className: "w-6 h-6 text-white" }) }) }),
/* @__PURE__ */ jsx8("span", { className: "text-xs font-medium text-muted-foreground", children: paymentIntent?.business?.name || "Business" })
] }),
/* @__PURE__ */ jsx8(RefreshCw, { className: "w-4 h-4 text-muted-foreground" }),
/* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center", children: [
/* @__PURE__ */ jsx8("div", { className: "w-12 h-12 rounded-xl flex items-center justify-center mb-1 overflow-hidden", children: /* @__PURE__ */ jsx8(
"img",
{
src: "/logo.png",
alt: "Splito",
className: "w-10 h-10 rounded object-cover"
}
) }),
/* @__PURE__ */ jsx8("span", { className: "text-xs font-medium", children: /* @__PURE__ */ jsx8("span", { className: "bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent font-bold", children: "Splito" }) })
] })
] }),
/* @__PURE__ */ jsx8("h2", { className: "text-xl font-bold mb-1", children: "Split Payment Request" }),
/* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground", children: "Join others in splitting this payment" })
] }),
/* @__PURE__ */ jsxs3(Card, { children: [
/* @__PURE__ */ jsx8(CardHeader, { className: "pb-3", children: /* @__PURE__ */ jsxs3(CardTitle, { className: "flex items-center gap-2 text-lg", children: [
/* @__PURE__ */ jsx8(DollarSign, { className: "w-5 h-5" }),
paymentIntent.products?.name || "Payment Request"
] }) }),
/* @__PURE__ */ jsxs3(CardContent, { className: "space-y-3", children: [
paymentIntent.products?.description && /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground", children: paymentIntent.products.description }),
/* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-2 gap-3 text-sm", children: [
/* @__PURE__ */ jsxs3("div", { children: [
/* @__PURE__ */ jsx8("p", { className: "font-medium", children: "Total Amount" }),
/* @__PURE__ */ jsxs3("p", { className: "text-lg font-bold text-primary", children: [
"$",
(paymentIntent.amount_cents / 100).toFixed(2),
" ",
paymentIntent.currency
] })
] }),
/* @__PURE__ */ jsxs3("div", { children: [
/* @__PURE__ */ jsx8("p", { className: "font-medium", children: "Remaining" }),
/* @__PURE__ */ jsxs3("p", { className: "text-lg font-bold text-success", children: [
"$",
(remainingAmount / 100).toFixed(2)
] })
] })
] }),
/* @__PURE__ */ jsxs3("div", { className: "space-y-1", children: [
/* @__PURE__ */ jsxs3("div", { className: "flex justify-between text-xs", children: [
/* @__PURE__ */ jsx8("span", { className: "text-muted-foreground", children: "Progress" }),
/* @__PURE__ */ jsxs3("span", { className: "font-medium", children: [
progressPercentage.toFixed(0),
"%"
] })
] }),
/* @__PURE__ */ jsx8("div", { className: "h-2 bg-secondary rounded-full overflow-hidden", children: /* @__PURE__ */ jsx8(
"div",
{
className: "h-full bg-gradient-to-r from-primary to-primary/80 transition-all duration-500",
style: { width: `${progressPercentage}%` }
}
) })
] })
] })
] }),
/* @__PURE__ */ jsxs3(Card, { children: [
/* @__PURE__ */ jsx8(CardHeader, { className: "pb-3", children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
/* @__PURE__ */ jsxs3(CardTitle, { className: "flex items-center gap-2 text-base", children: [
/* @__PURE__ */ jsx8(Users, { className: "w-4 h-4" }),
"Participants (",
recipients.length,
"/",
paymentIntent.max_participants || 10,
")"
] }),
/* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsx8(
Button,
{
variant: showQRInvitation ? "default" : "outline",
size: "sm",
onClick: generateQRInvitation,
disabled: isGeneratingQR,
children: /* @__PURE__ */ jsx8(QrCode, { className: "w-3 h-3" })
}
),
/* @__PURE__ */ jsx8(
Button,
{
variant: showInviteForm ? "default" : "outline",
size: "sm",
onClick: () => setShowInviteForm(!showInviteForm),
children: /* @__PURE__ */ jsx8(UserPlus, { className: "w-3 h-3" })
}
)
] })
] }) }),
/* @__PURE__ */ jsxs3(CardContent, { className: "space-y-3", children: [
showQRInvitation && qrCodeData && /* @__PURE__ */ jsx8(Card, { className: "bg-gradient-to-r from-primary/5 to-secondary/5", children: /* @__PURE__ */ jsxs3(CardContent, { className: "pt-4 text-center space-y-3", children: [
/* @__PURE__ */ jsx8("img", { src: qrCodeData, alt: "QR Code", className: "mx-auto w-48 h-48" }),
/* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: "Scan to join the split" }),
/* @__PURE__ */ jsxs3("div", { className: "flex gap-2 justify-center", children: [
/* @__PURE__ */ jsx8(Button, { variant: "outline", size: "sm", onClick: copyInvitationUrl, children: /* @__PURE__ */ jsx8(Copy, { className: "w-3 h-3" }) }),
/* @__PURE__ */ jsx8(Button, { variant: "outline", size: "sm", onClick: shareInvitation, children: /* @__PURE__ */ jsx8(Share2, { className: "w-3 h-3" }) })
] })
] }) }),
showInviteForm && /* @__PURE__ */ jsx8(Card, { className: "bg-gradient-to-r from-primary/5 to-secondary/5 border-primary/20", children: /* @__PURE__ */ jsxs3(CardContent, { className: "pt-3 space-y-2", children: [
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 mb-1", children: [
/* @__PURE__ */ jsx8(Mail, { className: "w-3 h-3 text-primary" }),
/* @__PURE__ */ jsx8("h4", { className: "text-sm font-medium", children: "Invite someone to contribute" })
] }),
/* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-1 gap-2", children: [
/* @__PURE__ */ jsxs3("div", { children: [
/* @__PURE__ */ jsx8(Label, { htmlFor: "inviteName", className: "text-xs", children: "Name" }),
/* @__PURE__ */ jsx8(
Input,
{
id: "inviteName",
placeholder: "Friend's name",
value: inviteForm.name,
onChange: (e) => setInviteForm((prev) => ({ ...prev, name: e.target.value })),
className: "h-8 text-sm"
}
)
] }),
/* @__PURE__ */ jsxs3("div", { children: [
/* @__PURE__ */ jsx8(Label, { htmlFor: "inviteEmail", className: "text-xs", children: "Email" }),
/* @__PURE__ */ jsx8(
Input,
{
id: "inviteEmail",
type: "email",
placeholder: "friend@email.com",
value: inviteForm.email,
onChange: (e) => setInviteForm((prev) => ({ ...prev, email: e.target.value })),
className: "h-8 text-sm"
}
)
] })
] }),
/* @__PURE__ */ jsxs3("div", { className: "flex gap-2 pt-1", children: [
/* @__PURE__ */ jsx8(
Button,
{
onClick: sendInvitation,
disabled: isInviting || !inviteForm.name || !inviteForm.email,
size: "sm",
className: "text-xs h-8",
children: isInviting ? /* @__PURE__ */ jsxs3(Fragment, { children: [
/* @__PURE__ */ jsx8(Clock, { className: "w-3 h-3 animate-spin" }),
"Sending..."
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
/* @__PURE__ */ jsx8(Send, { className: "w-3 h-3" }),
"Send Invitation"
] })
}
),
/* @__PURE__ */ jsx8(
Button,
{
variant: "outline",
size: "sm",
className: "text-xs h-8",
onClick: () => {
setShowInviteForm(false);
setInviteForm({ name: "", email: "" });
},
children: "Cancel"
}
)
] })
] }) }),
recipients.length > 0 && /* @__PURE__ */ jsx8("div", { className: "space-y-2", children: recipients.map((recipient) => /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between p-2 border rounded-lg text-sm", children: [
/* @__PURE__ */ jsxs3("div", { children: [
/* @__PURE__ */ jsx8("p", { className: "font-medium text-sm", children: recipient.name }),
/* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: recipient.email })
] }),
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsxs3(Badge, { variant: recipient.status === "completed" ? "success" : "secondary", className: "text-xs", children: [
"$",
(recipient.amount_cents / 100).toFixed(2)
] }),
recipient.status === "completed" && /* @__PURE__ */ jsx8(CheckCircle, { className: "w-3 h-3 text-success" })
] })
] }, recipient.id)) })
] })
] }),
remainingAmount > 0 && /* @__PURE__ */ jsxs3(Card, { children: [
/* @__PURE__ */ jsx8(CardHeader, { className: "pb-3", children: /* @__PURE__ */ jsxs3(CardTitle, { className: "flex items-center gap-2 text-base", children: [
/* @__PURE__ */ jsx8(CreditCard, { className: "w-4 h-4" }),
"Choose Your Contribution"
] }) }),
/* @__PURE__ */ jsxs3(CardContent, { className: "space-y-3", children: [
/* @__PURE__ */ jsx8("div", { className: "grid grid-cols-2 gap-2", children: quickAmounts.map((amount) => /* @__PURE__ */ jsx8(
Button,
{
variant: selectedAmount === amount.value ? "default" : "outline",
onClick: () => setSelectedAmount(amount.value),
className: "text-xs h-9",
children: /* @__PURE__ */ jsxs3("div", { className: "text-center", children: [
/* @__PURE__ */ jsx8("div", { className: "font-semibold", children: amount.label }),
/* @__PURE__ */ jsxs3("div", { className: "text-xs opacity-80", children: [
"$",
(amount.value / 100).toFixed(2)
] })
] })
},
amount.label
)) }),
/* @__PURE__ */ jsxs3("div", { children: [
/* @__PURE__ */ jsx8(Label, { htmlFor: "customAmount", className: "text-xs", children: "Custom Amount" }),
/* @__PURE__ */ jsx8(
Input,
{
id: "customAmount",
type: "number",
placeholder: "Enter custom amount",
value: selectedAmount ? (selectedAmount / 100).toString() : "",
onChange: (e) => setSelectedAmount(Math.round(parseFloat(e.target.value || "0") * 100)),
min: "0.01",
max: (remainingAmount / 100).toFixed(2),
step: "0.01",
className: "h-8 text-sm"
}
)
] }),
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsx8(Label, { className: "text-xs", children: "Your Details" }),
/* @__PURE__ */ jsx8(
Input,
{
placeholder: "Your name",
value: paymentForm.name,
onChange: (e) => setPaymentForm({ ...paymentForm, name: e.target.value }),
className: "h-8 text-sm"
}
),
/* @__PURE__ */ jsx8(
Input,
{
type: "email",
placeholder: "your.email@example.com",
value: paymentForm.email,
onChange: (e) => setPaymentForm({ ...paymentForm, email: e.target.value }),
className: "h-8 text-sm"
}
)
] }),
/* @__PURE__ */ jsx8(
Button,
{
onClick: processPayment,
disabled: !selectedAmount || !paymentForm.name || !paymentForm.email || isProcessingPayment,
className: "w-full",
size: "sm",
children: isProcessingPayment ? /* @__PURE__ */ jsxs3(Fragment, { children: [
/* @__PURE__ */ jsx8(Loader2, { className: "w-4 h-4 mr-2 animate-spin" }),
"Processing..."
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
/* @__PURE__ */ jsx8(CreditCard, { className: "w-4 h-4 mr-2" }),
"Pay $",
selectedAmount ? (selectedAmount / 100).toFixed(2) : "0.00"
] })
}
)
] })
] })
] });
}
function SplitModal({ open, onClose, token, baseUrl, isMobile }) {
if (isMobile) {
return /* @__PURE__ */ jsx8(Drawer, { open, onOpenChange: onClose, children: /* @__PURE__ */ jsxs3(DrawerContent, { className: "max-h-[95vh]", children: [
/* @__PURE__ */ jsx8(DrawerHeader, { children: /* @__PURE__ */ jsx8(DrawerTitle, { children: "Split Payment" }) }),
/* @__PURE__ */ jsx8("div", { className: "px-4 pb-6 overflow-y-auto", children: /* @__PURE__ */ jsx8(SplitModalContent, { token, baseUrl }) })
] }) });
}
return /* @__PURE__ */ jsx8(Dialog, { open, onOpenChange: onClose, children: /* @__PURE__ */ jsxs3(DialogContent, { className: "max-w-2xl max-h-[90vh] overflow-y-auto", children: [
/* @__PURE__ */ jsx8(DialogHeader, { children: /* @__PURE__ */ jsx8(DialogTitle, { children: "Split Payment" }) }),
/* @__PURE__ */ jsx8(SplitModalContent, { token, baseUrl })
] }) });
}
// src/sdk/modal-initializer.tsx
import { jsx as jsx9 } from "react/jsx-runtime";
function openSplitModal(token, baseUrl) {
const isMobile = window.innerWidth < 768;
const apiBaseUrl = baseUrl || window.location.origin;
const container = document.createElement("div");
container.id = "splito-modal-root";
container.style.position = "fixed";
container.style.top = "0";
container.style.left = "0";
container.style.width = "100%";
container.style.height = "100%";
container.style.zIndex = "9999";
document.body.appendChild(container);
const root = createRoot(container);
const handleClose = () => {
root.unmount();
document.body.removeChild(container);
};
root.render(
/* @__PURE__ */ jsx9(
SplitModal,
{
open: true,
onClose: handleClose,
token,
baseUrl: apiBaseUrl,
isMobile
}
)
);
}
export {
openSplitModal
};