feat: add Stripe checkout, order persistence, and webhook handling
Stripe-hosted Checkout integration with full order lifecycle: - stripity_stripe ~> 3.2 with sandbox/prod config via env vars - Order and OrderItem schemas with price snapshots at purchase time - CheckoutController creates pending order then redirects to Stripe - StripeWebhookController verifies signatures and confirms payment - Success page with real-time PubSub updates from webhook - Shop flash messages for checkout error feedback - Cart cleared after successful payment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
<div
|
||||
id="shop-container"
|
||||
phx-hook="CartPersist"
|
||||
class="shop-container min-h-screen pb-20 md:pb-0"
|
||||
style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);"
|
||||
>
|
||||
<.skip_link />
|
||||
|
||||
<%= if @theme_settings.announcement_bar do %>
|
||||
<.announcement_bar theme_settings={@theme_settings} />
|
||||
<% end %>
|
||||
|
||||
<.shop_header
|
||||
theme_settings={@theme_settings}
|
||||
logo_image={@logo_image}
|
||||
header_image={@header_image}
|
||||
active_page="checkout"
|
||||
mode={@mode}
|
||||
cart_count={@cart_count}
|
||||
/>
|
||||
|
||||
<main id="main-content" class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<%= if @order && @order.payment_status == "paid" do %>
|
||||
<div class="text-center mb-12">
|
||||
<div
|
||||
class="inline-flex items-center justify-center w-16 h-16 rounded-full mb-6"
|
||||
style="background-color: var(--t-accent); color: var(--t-accent-contrast);"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1
|
||||
class="text-3xl font-bold mb-3"
|
||||
style="font-family: var(--t-font-heading); color: var(--t-text-primary);"
|
||||
>
|
||||
Thank you for your order
|
||||
</h1>
|
||||
|
||||
<p class="text-lg mb-2" style="color: var(--t-text-secondary);">
|
||||
Order <strong style="color: var(--t-text-primary);">{@order.order_number}</strong>
|
||||
</p>
|
||||
|
||||
<%= if @order.customer_email do %>
|
||||
<p style="color: var(--t-text-secondary);">
|
||||
A confirmation will be sent to <strong>{@order.customer_email}</strong>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<.shop_card class="p-6 mb-8">
|
||||
<h2
|
||||
class="text-lg font-semibold mb-4"
|
||||
style="font-family: var(--t-font-heading); color: var(--t-text-primary);"
|
||||
>
|
||||
Order details
|
||||
</h2>
|
||||
|
||||
<ul class="flex flex-col gap-4 mb-6" style="list-style: none; margin: 0; padding: 0;">
|
||||
<%= for item <- @order.items do %>
|
||||
<li
|
||||
class="flex justify-between items-start pb-4 border-b last:border-b-0 last:pb-0"
|
||||
style="border-color: var(--t-border-default);"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--t-text-primary);">
|
||||
{item.product_name}
|
||||
</p>
|
||||
<%= if item.variant_title do %>
|
||||
<p class="text-sm" style="color: var(--t-text-secondary);">
|
||||
{item.variant_title}
|
||||
</p>
|
||||
<% end %>
|
||||
<p class="text-sm" style="color: var(--t-text-secondary);">
|
||||
Qty: {item.quantity}
|
||||
</p>
|
||||
</div>
|
||||
<span class="font-medium" style="color: var(--t-text-primary);">
|
||||
{SimpleshopTheme.Cart.format_price(item.unit_price * item.quantity)}
|
||||
</span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<div class="border-t pt-4" style="border-color: var(--t-border-default);">
|
||||
<div class="flex justify-between text-lg">
|
||||
<span class="font-semibold" style="color: var(--t-text-primary);">Total</span>
|
||||
<span class="font-bold" style="color: var(--t-text-primary);">
|
||||
{SimpleshopTheme.Cart.format_price(@order.total)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</.shop_card>
|
||||
|
||||
<%= if @order.shipping_address != %{} do %>
|
||||
<.shop_card class="p-6 mb-8">
|
||||
<h2
|
||||
class="text-lg font-semibold mb-3"
|
||||
style="font-family: var(--t-font-heading); color: var(--t-text-primary);"
|
||||
>
|
||||
Shipping to
|
||||
</h2>
|
||||
<div style="color: var(--t-text-secondary);">
|
||||
<p>{@order.shipping_address["name"]}</p>
|
||||
<p>{@order.shipping_address["line1"]}</p>
|
||||
<%= if @order.shipping_address["line2"] do %>
|
||||
<p>{@order.shipping_address["line2"]}</p>
|
||||
<% end %>
|
||||
<p>
|
||||
{@order.shipping_address["city"]}, {@order.shipping_address["postal_code"]}
|
||||
</p>
|
||||
<p>{@order.shipping_address["country"]}</p>
|
||||
</div>
|
||||
</.shop_card>
|
||||
<% end %>
|
||||
|
||||
<div class="text-center">
|
||||
<.shop_link_button href="/collections/all" class="px-8 py-3">
|
||||
Continue shopping
|
||||
</.shop_link_button>
|
||||
</div>
|
||||
<% else %>
|
||||
<%!-- Payment pending or order not found --%>
|
||||
<div class="text-center py-16">
|
||||
<div
|
||||
class="inline-flex items-center justify-center w-16 h-16 rounded-full mb-6 animate-pulse"
|
||||
style="background-color: var(--t-surface-sunken);"
|
||||
>
|
||||
<span style="color: var(--t-text-secondary);">
|
||||
<svg
|
||||
class="w-8 h-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1
|
||||
class="text-3xl font-bold mb-3"
|
||||
style="font-family: var(--t-font-heading); color: var(--t-text-primary);"
|
||||
>
|
||||
Processing your payment
|
||||
</h1>
|
||||
|
||||
<p class="text-lg mb-8" style="color: var(--t-text-secondary);">
|
||||
Please wait while we confirm your payment. This usually takes a few seconds.
|
||||
</p>
|
||||
|
||||
<p class="text-sm" style="color: var(--t-text-tertiary);">
|
||||
If this page doesn't update, please <a
|
||||
href="/contact"
|
||||
class="underline"
|
||||
style="color: var(--t-accent);"
|
||||
>contact us</a>.
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</main>
|
||||
|
||||
<.shop_footer theme_settings={@theme_settings} mode={@mode} />
|
||||
|
||||
<.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
subtotal={@cart_subtotal}
|
||||
cart_count={@cart_count}
|
||||
mode={@mode}
|
||||
open={assigns[:cart_drawer_open] || false}
|
||||
cart_status={assigns[:cart_status]}
|
||||
/>
|
||||
|
||||
<.search_modal hint_text={~s(Try a search – e.g. "mountain" or "notebook")} />
|
||||
|
||||
<.mobile_bottom_nav active_page="checkout" mode={@mode} />
|
||||
</div>
|
||||
Reference in New Issue
Block a user