UNPKG

@splito/sdk

Version:

Official JavaScript/TypeScript SDK for Splito - Split payments across multiple recipients

958 lines (942 loc) 40.5 kB
// 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 };