Skip to content

Commit 3c19d57

Browse files
German Zuccoliclaude
andcommitted
feat: group invite flow — show invite link + send via template
After creating a group: - Show invite link with copy button - Form to send invite to a contact via WhatsApp template (number + template name → calls groups/invite endpoint) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e332139 commit 3c19d57

2 files changed

Lines changed: 120 additions & 14 deletions

File tree

public/locales/en.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,14 @@
367367
"Seleccionar número": "Select number",
368368
"Creando...": "Creating...",
369369
"Crear grupo": "Create group",
370-
"Grupo creado. Comparte el link de invitación con los participantes.": "Group created. Share the invite link with participants.",
370+
"Grupo creado. Comparte el link de invitación con los participantes (máx. 8).": "Group created. Share the invite link with participants (max 8).",
371+
"Link de invitación": "Invite link",
372+
"Enviar invitación por WhatsApp": "Send invite via WhatsApp",
373+
"Número del contacto": "Contact number",
374+
"Nombre del template": "Template name",
375+
"Invitación enviada": "Invitation sent",
376+
"Enviando...": "Sending...",
377+
"Enviar invitación": "Send invitation",
371378
"Los grupos de WhatsApp no están disponibles en modo Coexistence. Para usar grupos, conecta un número con Cloud API.": "WhatsApp groups are not available in Coexistence mode. To use groups, connect a number with Cloud API.",
372379
"No se pudo procesar la invitación": "Could not process the invitation",
373380
"¿No tienes cuenta?": "Don't have an account?",

src/routes/_auth/conversations/new.tsx

Lines changed: 112 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createFileRoute, useNavigate } from "@tanstack/react-router";
22
import useBoundStore from "@/stores/useBoundStore";
3-
import { Search, X, MessageSquarePlus, MessageCircle, Phone } from "lucide-react";
3+
import { Search, X, MessageSquarePlus, MessageCircle, Phone, Copy, Check, Send } from "lucide-react";
44
import { useTranslation } from "@/hooks/useTranslation";
55
import { startConversation } from "@/utils/ConversationUtils";
66
import { useState } from "react";
@@ -35,8 +35,11 @@ function NewChat() {
3535
const [phoneNumber, setPhoneNumber] = useState("");
3636
const [groupName, setGroupName] = useState("");
3737
const [groupDescription, setGroupDescription] = useState("");
38-
const [groupResult, setGroupResult] = useState<{ invite_link?: string } | null>(null);
38+
const [groupResult, setGroupResult] = useState<{ invite_link?: string; group_id?: string } | null>(null);
3939
const [selectedGroupAddress, setSelectedGroupAddress] = useState("");
40+
const [copied, setCopied] = useState(false);
41+
const [invitePhone, setInvitePhone] = useState("");
42+
const [inviteTemplateName, setInviteTemplateName] = useState("");
4043

4144
// Cloud API addresses support groups; Coexistence does not
4245
const cloudApiAddresses = whatsappAddresses?.filter(
@@ -62,6 +65,29 @@ function NewChat() {
6265
onSuccess: (data) => setGroupResult(data),
6366
});
6467

68+
const sendInviteMutation = useMutation({
69+
mutationFn: async () => {
70+
if (!groupResult?.group_id || !groupResult?.invite_link || !invitePhone || !inviteTemplateName) return;
71+
const addr = cloudApiAddresses!.length === 1
72+
? cloudApiAddresses![0].address
73+
: selectedGroupAddress;
74+
const { data, error } = await supabase.functions.invoke("whatsapp-management/groups/invite", {
75+
method: "POST",
76+
body: {
77+
organization_id: activeOrgId,
78+
organization_address: addr,
79+
group_id: groupResult.group_id,
80+
invite_link: groupResult.invite_link,
81+
recipient_phone: invitePhone.replace(/\D/g, ""),
82+
template_name: inviteTemplateName,
83+
},
84+
});
85+
if (error) throw new Error(error.message);
86+
if (data?.error) throw new Error(data.error);
87+
return data;
88+
},
89+
});
90+
6591
function sanitizePhoneNumber(phone: string): string {
6692
// Remove all non-digit characters, keep as-is
6793
return phone.replace(/\D/g, "");
@@ -211,20 +237,93 @@ function NewChat() {
211237
</div>
212238
)}
213239

214-
{groupResult && (
240+
{!groupResult && (
241+
<button
242+
type="submit"
243+
className="primary w-full"
244+
disabled={!groupName.trim() || createGroupMutation.isPending}
245+
>
246+
{createGroupMutation.isPending ? t("Creando...") : t("Crear grupo")}
247+
</button>
248+
)}
249+
</form>
250+
251+
{groupResult && (
252+
<div className="flex flex-col gap-[12px] mt-[12px]">
215253
<div className="bg-primary/10 text-primary text-[13px] p-3 rounded-lg">
216-
{t("Grupo creado. Comparte el link de invitación con los participantes.")}
254+
{t("Grupo creado. Comparte el link de invitación con los participantes (máx. 8).")}
217255
</div>
218-
)}
219256

220-
<button
221-
type="submit"
222-
className="primary w-full"
223-
disabled={!groupName.trim() || createGroupMutation.isPending}
224-
>
225-
{createGroupMutation.isPending ? t("Creando...") : t("Crear grupo")}
226-
</button>
227-
</form>
257+
{/* Invite link with copy button */}
258+
<label>
259+
<div className="label">{t("Link de invitación")}</div>
260+
<div className="flex gap-[8px]">
261+
<input
262+
type="text"
263+
className="text flex-1"
264+
value={groupResult.invite_link || ""}
265+
readOnly
266+
/>
267+
<button
268+
type="button"
269+
className="p-[8px] rounded-lg border border-border hover:bg-accent"
270+
onClick={() => {
271+
navigator.clipboard.writeText(groupResult.invite_link || "");
272+
setCopied(true);
273+
setTimeout(() => setCopied(false), 2000);
274+
}}
275+
>
276+
{copied
277+
? <Check className="w-[16px] h-[16px] text-primary" />
278+
: <Copy className="w-[16px] h-[16px] text-muted-foreground" />}
279+
</button>
280+
</div>
281+
</label>
282+
283+
{/* Send invite via template */}
284+
<div className="border-t border-border pt-[12px]">
285+
<div className="label mb-[8px]">{t("Enviar invitación por WhatsApp")}</div>
286+
<div className="flex flex-col gap-[8px]">
287+
<input
288+
type="text"
289+
className="text"
290+
placeholder={t("Número del contacto")}
291+
value={invitePhone}
292+
onChange={(e) => setInvitePhone(e.target.value)}
293+
/>
294+
<input
295+
type="text"
296+
className="text"
297+
placeholder={t("Nombre del template")}
298+
value={inviteTemplateName}
299+
onChange={(e) => setInviteTemplateName(e.target.value)}
300+
/>
301+
302+
{sendInviteMutation.error && (
303+
<div className="text-[13px] text-destructive">
304+
{(sendInviteMutation.error as Error).message}
305+
</div>
306+
)}
307+
308+
{sendInviteMutation.isSuccess && (
309+
<div className="text-[13px] text-primary">
310+
{t("Invitación enviada")}
311+
</div>
312+
)}
313+
314+
<button
315+
type="button"
316+
className="primary w-full flex items-center justify-center gap-[6px]"
317+
disabled={!invitePhone.trim() || !inviteTemplateName.trim() || sendInviteMutation.isPending}
318+
onClick={() => sendInviteMutation.mutate()}
319+
>
320+
<Send className="w-[14px] h-[14px]" />
321+
{sendInviteMutation.isPending ? t("Enviando...") : t("Enviar invitación")}
322+
</button>
323+
</div>
324+
</div>
325+
</div>
326+
)}
228327
</SectionBody>
229328
)}
230329

0 commit comments

Comments
 (0)