stripe 1/2

This commit is contained in:
lovebird 2026-02-21 10:12:48 +01:00
parent 5fbffdee5e
commit 8731ea7cf9
8 changed files with 718 additions and 622 deletions

View File

@ -26,5 +26,7 @@ export interface EcommerceBundleDependencies {
stripeReturnUrl?: string;
/** Currency code for Stripe (default: "eur"). */
currency?: string;
/** Returns the current auth token for authenticated API calls. */
getAuthToken?: () => Promise<string | null>;
}
export declare const EcommerceBundle: React.FC<EcommerceBundleDependencies>;

View File

@ -36,5 +36,7 @@ export interface CheckoutFlowProps {
stripeReturnUrl?: string;
/** Currency code for Stripe payments (default: "eur"). */
currency?: string;
/** Returns the current auth token for authenticated API calls. */
getAuthToken?: () => Promise<string | null>;
}
export declare function CheckoutFlow({ userId, userDisplayName, userEmail, onFetchAddresses, onSaveAddress, onPlaceOrder, onBackToCart, onOrderSuccess, toast, stripePublishableKey, apiBaseUrl, stripeReturnUrl, currency, }: CheckoutFlowProps): import("react/jsx-runtime").JSX.Element;
export declare function CheckoutFlow({ userId, userDisplayName, userEmail, onFetchAddresses, onSaveAddress, onPlaceOrder, onBackToCart, onOrderSuccess, toast, stripePublishableKey, apiBaseUrl, stripeReturnUrl, currency, getAuthToken, }: CheckoutFlowProps): import("react/jsx-runtime").JSX.Element;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,9 @@ export interface Transaction {
currency: string;
product_info: any[];
shipping_info: any;
payment_provider?: string;
external_order_id?: string;
metadata?: Record<string, any>;
}
export interface PurchasesListProps {
/** Async function to fetch user transactions. */

View File

@ -36,6 +36,8 @@ export interface EcommerceBundleDependencies {
stripeReturnUrl?: string;
/** Currency code for Stripe (default: "eur"). */
currency?: string;
/** Returns the current auth token for authenticated API calls. */
getAuthToken?: () => Promise<string | null>;
}
export const EcommerceBundle: React.FC<EcommerceBundleDependencies> = (props) => {
@ -61,6 +63,7 @@ export const EcommerceBundle: React.FC<EcommerceBundleDependencies> = (props) =>
apiBaseUrl={props.apiBaseUrl}
stripeReturnUrl={props.stripeReturnUrl}
currency={props.currency}
getAuthToken={props.getAuthToken}
/>
);
}

View File

@ -40,6 +40,8 @@ export interface CheckoutFlowProps {
stripeReturnUrl?: string;
/** Currency code for Stripe payments (default: "eur"). */
currency?: string;
/** Returns the current auth token for authenticated API calls. */
getAuthToken?: () => Promise<string | null>;
}
export function CheckoutFlow({
@ -56,6 +58,7 @@ export function CheckoutFlow({
apiBaseUrl = "",
stripeReturnUrl,
currency = "eur",
getAuthToken,
}: CheckoutFlowProps) {
const [savedAddresses, setSavedAddresses] = useState<SavedAddress[]>([]);
const [stripePromise, setStripePromise] = useState<Promise<StripeJS | null> | null>(null);
@ -88,12 +91,22 @@ export function CheckoutFlow({
// Stripe expects amount in smallest currency unit (cents for USD)
const amountInCents = Math.round(subtotal * 100);
fetch(`${apiBaseUrl}/api/stripe/create-payment-intent`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: amountInCents, currency }),
})
.then((r) => r.json())
// Get auth token for the API call (needed for Stripe Customer creation)
const makeRequest = async () => {
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (getAuthToken) {
const token = await getAuthToken();
if (token) headers["Authorization"] = `Bearer ${token}`;
}
const r = await fetch(`${apiBaseUrl}/api/stripe/create-payment-intent`, {
method: "POST",
headers,
body: JSON.stringify({ amount: amountInCents, currency }),
});
return r.json();
};
makeRequest()
.then((data) => {
if (mounted && data.clientSecret) {
setClientSecret(data.clientSecret);
@ -161,6 +174,9 @@ export function CheckoutFlow({
},
});
toast?.success("Payment successful!");
// Brief delay to let the Stripe webhook update the transaction
// with receipt URL and status before navigating
await new Promise((r) => setTimeout(r, 2000));
onOrderSuccess();
} catch (err) {
console.error("Failed to record Stripe transaction:", err);

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Package, Clock, CheckCircle, XCircle, ArrowLeft, ExternalLink, RefreshCw, Store } from "lucide-react";
import { Package, Clock, CheckCircle, XCircle, ArrowLeft, ExternalLink, RefreshCw, Store, Receipt, FileText } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@ -18,6 +18,9 @@ export interface Transaction {
currency: string;
product_info: any[];
shipping_info: any;
payment_provider?: string;
external_order_id?: string;
metadata?: Record<string, any>;
}
export interface PurchasesListProps {
@ -34,6 +37,7 @@ export interface PurchasesListProps {
const statusConfig: Record<string, { label: string; variant: "default" | "secondary" | "destructive" | "outline"; icon: React.ReactNode }> = {
pending: { label: "Pending", variant: "secondary", icon: <Clock className="h-3.5 w-3.5" /> },
processing: { label: "Processing", variant: "default", icon: <RefreshCw className="h-3.5 w-3.5" /> },
paid: { label: "Paid", variant: "default", icon: <CheckCircle className="h-3.5 w-3.5" /> },
completed: { label: "Completed", variant: "default", icon: <CheckCircle className="h-3.5 w-3.5" /> },
failed: { label: "Failed", variant: "destructive", icon: <XCircle className="h-3.5 w-3.5" /> },
refunded: { label: "Refunded", variant: "outline", icon: <RefreshCw className="h-3.5 w-3.5" /> },
@ -182,6 +186,34 @@ export function PurchasesList({ onFetchTransactions, onNavigate, toast }: Purcha
Ships to: {(tx.shipping_info as any).fullName}, {(tx.shipping_info as any).city}, {(tx.shipping_info as any).country}
</div>
)}
{/* Receipt / Invoice links */}
{tx.metadata && (tx.metadata.stripe_receipt_url || tx.metadata.stripe_invoice_url) && (
<div className="mt-3 pt-3 border-t flex items-center gap-4">
{tx.metadata.stripe_receipt_url && (
<a
href={tx.metadata.stripe_receipt_url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline flex items-center gap-1"
>
<Receipt className="h-3.5 w-3.5" />
View Receipt
</a>
)}
{tx.metadata.stripe_invoice_url && (
<a
href={tx.metadata.stripe_invoice_url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline flex items-center gap-1"
>
<FileText className="h-3.5 w-3.5" />
View Invoice
</a>
)}
</div>
)}
</CardContent>
</Card>
);