La till lösenordsval vid checkout

X-Lovable-Edit-ID: edt-622ecb9c-6ab7-49a8-b6fc-d6cb1506675b
Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-07-12 14:03:42 +00:00
3 changed files with 186 additions and 1 deletions
+92 -1
View File
@@ -1,5 +1,5 @@
import { useState, useMemo, useEffect } from "react"; import { useState, useMemo, useEffect } from "react";
import { Loader2, CheckCircle2, CreditCard, FileText, ChevronsUpDown, Check } from "lucide-react"; import { Loader2, CheckCircle2, CreditCard, FileText, ChevronsUpDown, Check, Lock, Eye, EyeOff, Sparkles } from "lucide-react";
import { supabase } from "@/integrations/supabase/client"; import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -86,6 +86,7 @@ const formSchema = z.object({
.refine(isStockholmPostalCode, "Vi levererar endast inom Storstockholm (100 00 199 99)"), .refine(isStockholmPostalCode, "Vi levererar endast inom Storstockholm (100 00 199 99)"),
stad: z.string().trim().min(1, "Fyll i stad").max(100), stad: z.string().trim().min(1, "Fyll i stad").max(100),
kommentarer: z.string().trim().max(1000).optional(), kommentarer: z.string().trim().max(1000).optional(),
password: z.string().max(100).optional(),
}); });
type Method = "card" | "invoice"; type Method = "card" | "invoice";
@@ -100,6 +101,7 @@ const PlansSection = () => {
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const [clientSecret, setClientSecret] = useState<string | null>(null); const [clientSecret, setClientSecret] = useState<string | null>(null);
const [cityOpen, setCityOpen] = useState(false); const [cityOpen, setCityOpen] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [form, setForm] = useState({ const [form, setForm] = useState({
företag: "", företag: "",
epost: "", epost: "",
@@ -108,6 +110,7 @@ const PlansSection = () => {
postnummer: "", postnummer: "",
stad: "", stad: "",
kommentarer: "", kommentarer: "",
password: "",
}); });
const filteredCities = useMemo(() => { const filteredCities = useMemo(() => {
@@ -187,6 +190,15 @@ const PlansSection = () => {
return; return;
} }
// Password required for guests (auto-create account)
if (!user) {
const pw = form.password || "";
if (pw.length < 8) {
setErrors((prev) => ({ ...prev, password: "Lösenordet måste vara minst 8 tecken" }));
return;
}
}
if (!hasPaymentsConfigured()) { if (!hasPaymentsConfigured()) {
toast.error("Betalningar är inte konfigurerade ännu."); toast.error("Betalningar är inte konfigurerade ännu.");
return; return;
@@ -194,6 +206,33 @@ const PlansSection = () => {
setSubmitting(true); setSubmitting(true);
try { try {
// Auto-create account for guests, then sign in
if (!user && form.password) {
const { data: accData, error: accErr } = await supabase.functions.invoke("create-account", {
body: {
email: result.data.epost,
password: form.password,
company: result.data.företag,
phone: result.data.telefon ?? "",
},
});
if (accErr) throw accErr;
if ((accData as any)?.error) throw new Error((accData as any).error);
const { error: signInErr } = await supabase.auth.signInWithPassword({
email: result.data.epost,
password: form.password,
});
if (signInErr) {
if ((accData as any)?.existed) {
throw new Error(
"Ett konto finns redan för denna e-post. Fel lösenord — logga in eller använd 'Glömt lösenord'.",
);
}
throw signInErr;
}
}
const body = { const body = {
method, method,
employees, employees,
@@ -453,6 +492,58 @@ const PlansSection = () => {
</div> </div>
</div> </div>
{!user && (
<div className="relative overflow-hidden rounded-sm border border-primary/40 bg-gradient-to-br from-primary/[0.06] via-background to-primary/[0.03] p-5 md:p-6 shadow-[0_2px_20px_-8px_rgba(0,0,0,0.15)]">
<div className="absolute -top-10 -right-10 w-32 h-32 rounded-full bg-primary/10 blur-2xl pointer-events-none" />
<div className="relative flex items-start gap-3 mb-4">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-primary/15 flex items-center justify-center border border-primary/30">
<Sparkles className="w-5 h-5 text-primary" />
</div>
<div>
<div className="font-display text-lg md:text-xl font-semibold text-foreground leading-tight">
Ditt konto skapas automatiskt
</div>
<p className="text-muted-foreground text-xs md:text-sm mt-1 leading-relaxed">
Välj ett lösenord loggar vi in dig direkt. Hantera ditt abonnemang, ändra leveranser och se fakturor i "Mitt konto" när som helst.
</p>
</div>
</div>
<label className="block mb-1.5 text-sm font-semibold text-foreground">
Välj lösenord <span className="text-primary">*</span>
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
<input
name="password"
type={showPassword ? "text" : "password"}
value={form.password}
onChange={handleChange}
placeholder="Minst 8 tecken"
autoComplete="new-password"
aria-invalid={!!errors.password}
className="w-full pl-10 pr-11 py-3 rounded-sm bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/40 focus:border-primary/60 transition-colors"
/>
<button
type="button"
onClick={() => setShowPassword((s) => !s)}
aria-label={showPassword ? "Dölj lösenord" : "Visa lösenord"}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
{errors.password ? (
<p className="text-destructive text-xs mt-1.5">{errors.password}</p>
) : (
<p className="text-muted-foreground text-xs mt-1.5">
Använd minst 8 tecken. Vi rekommenderar en blandning av bokstäver och siffror.
</p>
)}
</div>
)}
<div> <div>
<label className="block mb-1.5 text-sm font-semibold"> <label className="block mb-1.5 text-sm font-semibold">
Leveransadress <span className="text-primary">*</span> Leveransadress <span className="text-primary">*</span>
+3
View File
@@ -12,6 +12,9 @@ verify_jwt = false
[functions.submit-order] [functions.submit-order]
verify_jwt = false verify_jwt = false
[functions.create-account]
verify_jwt = false
[functions.create-checkout] [functions.create-checkout]
verify_jwt = false verify_jwt = false
@@ -0,0 +1,91 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0";
import { z } from "npm:zod@3.23.8";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, x-supabase-client-platform, x-supabase-api-version, apikey, content-type",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
const BodySchema = z.object({
email: z.string().trim().email().max(255),
password: z.string().min(8).max(100),
company: z.string().trim().max(200).optional().default(""),
phone: z.string().trim().max(30).optional().default(""),
});
const admin = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
if (req.method !== "POST") {
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
try {
const parsed = BodySchema.safeParse(await req.json());
if (!parsed.success) {
return new Response(
JSON.stringify({ error: "Ogiltiga fält", details: parsed.error.flatten().fieldErrors }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
const { email, password, company, phone } = parsed.data;
// Try to create user with email_confirm=true so they can sign in immediately
const { data: created, error: createErr } = await admin.auth.admin.createUser({
email,
password,
email_confirm: true,
user_metadata: { company, phone },
});
if (createErr) {
const msg = createErr.message || "";
const alreadyExists =
msg.toLowerCase().includes("already") ||
msg.toLowerCase().includes("registered") ||
msg.toLowerCase().includes("exists");
if (alreadyExists) {
return new Response(JSON.stringify({ ok: true, existed: true }), {
status: 200,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
console.error("createUser error:", createErr);
return new Response(JSON.stringify({ error: msg }), {
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Best-effort update profile
if (created?.user?.id) {
await admin
.from("profiles")
.update({
company_name: company || null,
phone: phone || null,
})
.eq("user_id", created.user.id);
}
return new Response(JSON.stringify({ ok: true, existed: false }), {
status: 200,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
} catch (err: any) {
console.error("create-account error:", err);
return new Response(JSON.stringify({ error: err?.message ?? "Något gick fel" }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
});