diff --git a/messages/de.json b/messages/de.json
index 42646baf..ab93bc99 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -232,6 +232,16 @@
"confirmingPayment": "Ihre Zahlung wird bestätigt...",
"selectPaymentMethod": "Bitte wählen Sie eine Zahlungsmethode.",
"manualPaymentInfo": "Sie erhalten Zahlungsanweisungen nach der Bestellung.",
+ "mpesaPhoneLabel": "M-Pesa-Telefonnummer",
+ "mpesaPhonePlaceholder": "07XX XXX XXX",
+ "mpesaPhoneHint": "Geben Sie die Safaricom-Nummer ein, um die M-Pesa-Zahlungsaufforderung zu erhalten.",
+ "mpesaPhoneInvalid": "Geben Sie eine gültige Safaricom-Nummer ein, z. B. 0712 345 678.",
+ "mpesaAwaitTitle": "Prüfen Sie Ihr Telefon",
+ "mpesaAwaitBody": "Wir haben eine M-Pesa-Zahlungsaufforderung an Ihr Telefon gesendet. Geben Sie Ihre M-Pesa-PIN ein, um die Zahlung abzuschließen.",
+ "mpesaTimeoutTitle": "Zahlung noch nicht bestätigt",
+ "mpesaTimeoutBody": "Ihre Bestellung wurde aufgegeben. Wenn Sie Ihre M-Pesa-PIN bereits eingegeben haben, kann die Bestätigung einige Minuten dauern. Sie können diese Seite gefahrlos schließen.",
+ "mpesaContinueShopping": "Weiter einkaufen",
+ "mpesaPaymentFailed": "Die M-Pesa-Zahlung wurde nicht abgeschlossen. Bitte versuchen Sie es erneut.",
"failedToCreatePayment": "Zahlung konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
"placeOrder": "Bestellung aufgeben",
"noPaymentRequired": "Keine Zahlung erforderlich — Ihre Bestellung ist vollständig abgedeckt.",
diff --git a/messages/en.json b/messages/en.json
index 01103cc4..ec650ba1 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -232,6 +232,16 @@
"confirmingPayment": "Confirming your payment...",
"selectPaymentMethod": "Please select a payment method.",
"manualPaymentInfo": "You will receive payment instructions after placing your order.",
+ "mpesaPhoneLabel": "M-Pesa phone number",
+ "mpesaPhonePlaceholder": "07XX XXX XXX",
+ "mpesaPhoneHint": "Enter the Safaricom number to receive the M-Pesa payment prompt.",
+ "mpesaPhoneInvalid": "Enter a valid Safaricom number, e.g. 0712 345 678.",
+ "mpesaAwaitTitle": "Check your phone",
+ "mpesaAwaitBody": "We've sent an M-Pesa payment request to your phone. Enter your M-Pesa PIN to complete the payment.",
+ "mpesaTimeoutTitle": "Payment not yet confirmed",
+ "mpesaTimeoutBody": "Your order has been placed. If you already entered your M-Pesa PIN, it can take a few minutes to confirm. You can close this page safely.",
+ "mpesaContinueShopping": "Continue Shopping",
+ "mpesaPaymentFailed": "The M-Pesa payment was not completed. Please try again.",
"failedToCreatePayment": "Failed to create payment. Please try again.",
"placeOrder": "Place Order",
"noPaymentRequired": "No payment required — your order is fully covered.",
diff --git a/messages/es.json b/messages/es.json
index 2ab5f75f..dcdf2730 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -232,6 +232,16 @@
"confirmingPayment": "Confirmando tu pago...",
"selectPaymentMethod": "Por favor selecciona un método de pago.",
"manualPaymentInfo": "Recibirás instrucciones de pago después de realizar tu pedido.",
+ "mpesaPhoneLabel": "Número de teléfono M-Pesa",
+ "mpesaPhonePlaceholder": "07XX XXX XXX",
+ "mpesaPhoneHint": "Ingresa el número de Safaricom para recibir la solicitud de pago de M-Pesa.",
+ "mpesaPhoneInvalid": "Ingresa un número de Safaricom válido, p. ej. 0712 345 678.",
+ "mpesaAwaitTitle": "Revisa tu teléfono",
+ "mpesaAwaitBody": "Hemos enviado una solicitud de pago M-Pesa a tu teléfono. Ingresa tu PIN de M-Pesa para completar el pago.",
+ "mpesaTimeoutTitle": "Pago aún no confirmado",
+ "mpesaTimeoutBody": "Tu pedido se ha realizado. Si ya ingresaste tu PIN de M-Pesa, la confirmación puede tardar unos minutos. Puedes cerrar esta página de forma segura.",
+ "mpesaContinueShopping": "Seguir comprando",
+ "mpesaPaymentFailed": "El pago de M-Pesa no se completó. Inténtalo de nuevo.",
"failedToCreatePayment": "No se pudo crear el pago. Inténtalo de nuevo.",
"placeOrder": "Realizar pedido",
"noPaymentRequired": "No se requiere pago — tu pedido está completamente cubierto.",
diff --git a/messages/fr.json b/messages/fr.json
index a07f1713..32164352 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -232,6 +232,16 @@
"confirmingPayment": "Confirmation de votre paiement...",
"selectPaymentMethod": "Veuillez sélectionner un mode de paiement.",
"manualPaymentInfo": "Vous recevrez les instructions de paiement après avoir passé votre commande.",
+ "mpesaPhoneLabel": "Numéro de téléphone M-Pesa",
+ "mpesaPhonePlaceholder": "07XX XXX XXX",
+ "mpesaPhoneHint": "Saisissez le numéro Safaricom pour recevoir la demande de paiement M-Pesa.",
+ "mpesaPhoneInvalid": "Saisissez un numéro Safaricom valide, par ex. 0712 345 678.",
+ "mpesaAwaitTitle": "Vérifiez votre téléphone",
+ "mpesaAwaitBody": "Nous avons envoyé une demande de paiement M-Pesa sur votre téléphone. Saisissez votre code PIN M-Pesa pour finaliser le paiement.",
+ "mpesaTimeoutTitle": "Paiement pas encore confirmé",
+ "mpesaTimeoutBody": "Votre commande a été passée. Si vous avez déjà saisi votre code PIN M-Pesa, la confirmation peut prendre quelques minutes. Vous pouvez fermer cette page en toute sécurité.",
+ "mpesaContinueShopping": "Continuer mes achats",
+ "mpesaPaymentFailed": "Le paiement M-Pesa n'a pas abouti. Veuillez réessayer.",
"failedToCreatePayment": "Échec de la création du paiement. Veuillez réessayer.",
"placeOrder": "Passer la commande",
"noPaymentRequired": "Aucun paiement requis — votre commande est entièrement couverte.",
diff --git a/messages/pl.json b/messages/pl.json
index 388315c8..77532d85 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -232,6 +232,16 @@
"confirmingPayment": "Potwierdzanie płatności...",
"selectPaymentMethod": "Proszę wybrać metodę płatności.",
"manualPaymentInfo": "Otrzymasz instrukcje płatności po złożeniu zamówienia.",
+ "mpesaPhoneLabel": "Numer telefonu M-Pesa",
+ "mpesaPhonePlaceholder": "07XX XXX XXX",
+ "mpesaPhoneHint": "Wprowadź numer Safaricom, aby otrzymać monit o płatność M-Pesa.",
+ "mpesaPhoneInvalid": "Wprowadź prawidłowy numer Safaricom, np. 0712 345 678.",
+ "mpesaAwaitTitle": "Sprawdź telefon",
+ "mpesaAwaitBody": "Wysłaliśmy prośbę o płatność M-Pesa na Twój telefon. Wprowadź swój PIN M-Pesa, aby dokończyć płatność.",
+ "mpesaTimeoutTitle": "Płatność jeszcze niepotwierdzona",
+ "mpesaTimeoutBody": "Twoje zamówienie zostało złożone. Jeśli wprowadziłeś już PIN M-Pesa, potwierdzenie może potrwać kilka minut. Możesz bezpiecznie zamknąć tę stronę.",
+ "mpesaContinueShopping": "Kontynuuj zakupy",
+ "mpesaPaymentFailed": "Płatność M-Pesa nie została zrealizowana. Spróbuj ponownie.",
"failedToCreatePayment": "Nie udało się utworzyć płatności. Spróbuj ponownie.",
"placeOrder": "Złóż zamówienie",
"noPaymentRequired": "Płatność nie jest wymagana — zamówienie jest w pełni pokryte.",
diff --git a/next.config.ts b/next.config.ts
index 5360decd..68c27a6a 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -33,6 +33,8 @@ const nextConfig: NextConfig = {
},
images: {
qualities: [25, 50, 75, 85, 100],
+ dangerouslyAllowSVG: true,
+ contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
dangerouslyAllowLocalIP: true, // Allow localhost images in development
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
@@ -42,6 +44,12 @@ const nextConfig: NextConfig = {
hostname: "localhost",
pathname: "/rails/active_storage/**",
},
+ {
+ protocol: "http",
+ hostname: "127.0.0.1",
+ port: "9000",
+ pathname: "/media/**",
+ },
{
protocol: "https",
hostname: "**.vendo.dev",
@@ -67,6 +75,16 @@ const nextConfig: NextConfig = {
hostname: "**.olitt.store",
pathname: "/rails/active_storage/**",
},
+ {
+ protocol: "https",
+ hostname: "**.olitt.com",
+ pathname: "/media/**",
+ },
+ {
+ protocol: "https",
+ hostname: "**.olitt.net",
+ pathname: "/media/**",
+ },
{
protocol: "https",
hostname: "images.unsplash.com",
diff --git a/public/olitt-logo.svg b/public/olitt-logo.svg
new file mode 100644
index 00000000..0e8c2c4e
--- /dev/null
+++ b/public/olitt-logo.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/public/payment-icons/mpesa.svg b/public/payment-icons/mpesa.svg
new file mode 100644
index 00000000..84110897
--- /dev/null
+++ b/public/payment-icons/mpesa.svg
@@ -0,0 +1,444 @@
+
\ No newline at end of file
diff --git a/src/app/[country]/[locale]/(checkout)/awaiting-payment/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/awaiting-payment/[id]/page.tsx
new file mode 100644
index 00000000..3005fdfb
--- /dev/null
+++ b/src/app/[country]/[locale]/(checkout)/awaiting-payment/[id]/page.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import { CircleAlert, Loader2, Smartphone } from "lucide-react";
+import { usePathname, useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { use, useEffect, useRef, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { getOrderPaymentStatus } from "@/lib/data/payment";
+import { extractBasePath } from "@/lib/utils/path";
+
+const POLL_INTERVAL_MS = 3000;
+const POLL_TIMEOUT_MS = 120000;
+
+interface AwaitingPaymentPageProps {
+ params: Promise<{
+ id: string;
+ country: string;
+ locale: string;
+ }>;
+}
+
+export default function AwaitingPaymentPage({
+ params,
+}: AwaitingPaymentPageProps) {
+ const { id: cartId } = use(params);
+ const t = useTranslations("checkout");
+ const router = useRouter();
+ const pathname = usePathname();
+ const basePath = extractBasePath(pathname);
+ const [timedOut, setTimedOut] = useState(false);
+ const startedRef = useRef(false);
+
+ useEffect(() => {
+ if (startedRef.current) return;
+ startedRef.current = true;
+
+ let active = true;
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
+
+ async function poll() {
+ while (active) {
+ let state: "pending" | "completed" | "failed" = "pending";
+ try {
+ ({ state } = await getOrderPaymentStatus(cartId));
+ } catch {
+ state = "pending";
+ }
+ if (!active) return;
+
+ if (state === "completed") {
+ router.replace(`${basePath}/order-placed/${cartId}`);
+ return;
+ }
+ if (state === "failed") {
+ const message = encodeURIComponent(t("mpesaPaymentFailed"));
+ router.replace(
+ `${basePath}/checkout/${cartId}?payment_error=${message}`,
+ );
+ return;
+ }
+ if (Date.now() >= deadline) {
+ setTimedOut(true);
+ return;
+ }
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
+ }
+ }
+
+ poll();
+
+ return () => {
+ active = false;
+ };
+ }, [cartId, basePath, router, t]);
+
+ if (timedOut) {
+ return (
+