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:
@@ -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 så 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>
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user