Payments
Hoikka uses a provider pattern for payments. Stripe is integrated with real Stripe Elements for secure card collection.
Payment Flow
Stripe
1. Customer reaches checkout
2. Select Stripe as payment method
3. Create payment → Stripe PaymentIntent created
4. Stripe Elements renders card form on frontend
5. Client-side confirmation via stripe.confirmPayment()
6. Server confirms payment status
7. Order is completed → redirect to thank-youMock (development)
1. Customer reaches checkout
2. Select mock payment method
3. Click "Place Order" → payment created and order completed in one step
4. Redirect to thank-youStripe Integration
Environment Variables
STRIPE_SECRET_KEY="sk_test_..."
PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."Server-Side (PaymentIntent)
const { payment, paymentInfo } = await paymentService.createPayment(order, stripeMethodId);
// paymentInfo.clientSecret → used by Stripe Elements
// paymentInfo.providerTransactionId → Stripe PaymentIntent IDFrontend (Stripe Elements)
The StripePayment component handles loading Stripe.js, mounting the Payment Element, and exposing refs for confirmation:
<script lang="ts">
import StripePayment from "$lib/components/storefront/StripePayment.svelte";
let stripeRef, elementsRef;
</script>
{#if paymentInfo?.methodCode === "stripe" && paymentInfo?.clientSecret}
<StripePayment
clientSecret={paymentInfo.clientSecret}
onready={(stripe, elements) => {
stripeRef = stripe;
elementsRef = elements;
}}
/>
{/if}On form submission, confirm the payment client-side before the server action:
const { error } = await stripeRef.confirmPayment({
elements: elementsRef,
redirect: "if_required"
});Webhook Handler
src/routes/api/webhooks/stripe/+server.ts
export const POST = async ({ request }) => {
const sig = request.headers.get("stripe-signature");
const body = await request.text();
const event = stripe.webhooks.constructEvent(body, sig, WEBHOOK_SECRET);
if (event.type === "payment_intent.succeeded") {
const paymentIntent = event.data.object;
await paymentService.confirmPayment(paymentIntent.id);
}
return new Response("OK");
};Payment States
| State | Description |
|---|---|
pending | Payment created, awaiting completion |
authorized | Payment authorized, not captured |
settled | Payment captured successfully |
declined | Payment failed |
refunded | Payment refunded |
Refunds
// Full refund
await paymentService.refundPayment(paymentId);
// Partial refund (amount in cents)
await paymentService.refundPayment(paymentId, 1500);Adding Payment Providers
Implement the PaymentProvider interface:
import type { PaymentProvider } from "$lib/server/services/payments/types";
class MyProvider implements PaymentProvider {
code = "my-provider";
async createPayment(order) {
// Create payment with external provider
}
async confirmPayment(transactionId) {
// Check payment status
}
async refundPayment(transactionId, amount?) {
// Process refund
}
}Register in src/lib/server/services/payments/index.ts:
const PROVIDERS = new Map([
["stripe", new StripeProvider()],
["my-provider", new MyProvider()]
]);Last updated on