| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- "use client";
- import { useState, useTransition } from "react";
- import { useTranslations } from "next-intl";
- import { useRouter } from "next/navigation";
- import { toast } from "sonner";
- import { Input } from "@/components/ui/input";
- import { Button } from "@/components/ui/button";
- import { Label } from "@/components/ui/label";
- import { Switch } from "@/components/ui/switch";
- import { AppCard } from "@/components/app-card";
- import { Loader2, Send, Info, Copy, Check } from "lucide-react";
- import { ORG_SMS_KEYS } from "../Schema/smsSettingsSchema";
- import {
- setSmsSettings,
- testSmsSend,
- } from "../Actions/smsSettingsActions";
- import {
- ReadOnlyBanner,
- SaveButton,
- ReadOnlyWrapper,
- } from "@/app/(authenticated)/settings/read-only-guard";
- type SmsProviderType = "twilio" | "vonage" | "telnyx";
- export function SmsSettingsForm({
- initial,
- appUrl,
- }: {
- initial: Record<string, string>;
- appUrl: string;
- }) {
- const t = useTranslations("settings");
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
- const [isTesting, setIsTesting] = useState(false);
- const [copied, setCopied] = useState(false);
- const [testPhone, setTestPhone] = useState("");
- const hasProvider = !!initial[ORG_SMS_KEYS.SMS_PROVIDER];
- const [enabled, setEnabled] = useState(hasProvider);
- const [smsProvider, setSmsProvider] = useState<SmsProviderType>(
- (initial[ORG_SMS_KEYS.SMS_PROVIDER] as SmsProviderType) || "twilio",
- );
- // Shared
- const [phoneNumber, setPhoneNumber] = useState(
- initial[ORG_SMS_KEYS.SMS_PHONE_NUMBER] || "",
- );
- // Twilio
- const [twilioAccountSid, setTwilioAccountSid] = useState(
- initial[ORG_SMS_KEYS.SMS_TWILIO_ACCOUNT_SID] || "",
- );
- const [twilioAuthToken, setTwilioAuthToken] = useState(
- initial[ORG_SMS_KEYS.SMS_TWILIO_AUTH_TOKEN] || "",
- );
- // Vonage
- const [vonageApiKey, setVonageApiKey] = useState(
- initial[ORG_SMS_KEYS.SMS_VONAGE_API_KEY] || "",
- );
- const [vonageApiSecret, setVonageApiSecret] = useState(
- initial[ORG_SMS_KEYS.SMS_VONAGE_API_SECRET] || "",
- );
- // Telnyx
- const [telnyxApiKey, setTelnyxApiKey] = useState(
- initial[ORG_SMS_KEYS.SMS_TELNYX_API_KEY] || "",
- );
- const webhookSecret = initial[ORG_SMS_KEYS.SMS_WEBHOOK_SECRET] || "";
- const webhookUrl = webhookSecret
- ? `${appUrl}/api/webhooks/sms/${smsProvider}?org_secret=${webhookSecret}`
- : "";
- const handleCopyWebhook = () => {
- if (!webhookUrl) return;
- navigator.clipboard.writeText(webhookUrl);
- setCopied(true);
- toast.success(t("sms.webhookCopied"));
- setTimeout(() => setCopied(false), 2000);
- };
- const handleSave = () => {
- startTransition(async () => {
- if (!enabled) {
- const result = await setSmsSettings({
- [ORG_SMS_KEYS.SMS_PROVIDER]: "",
- });
- if (result.success) {
- toast.success(t("sms.savedDisabled"));
- router.refresh();
- } else {
- toast.error(result.error ?? t("sms.failedSave"));
- }
- return;
- }
- const data: Record<string, string> = {
- [ORG_SMS_KEYS.SMS_PROVIDER]: smsProvider,
- [ORG_SMS_KEYS.SMS_PHONE_NUMBER]: phoneNumber,
- // Twilio
- [ORG_SMS_KEYS.SMS_TWILIO_ACCOUNT_SID]: twilioAccountSid,
- [ORG_SMS_KEYS.SMS_TWILIO_AUTH_TOKEN]: twilioAuthToken,
- // Vonage
- [ORG_SMS_KEYS.SMS_VONAGE_API_KEY]: vonageApiKey,
- [ORG_SMS_KEYS.SMS_VONAGE_API_SECRET]: vonageApiSecret,
- // Telnyx
- [ORG_SMS_KEYS.SMS_TELNYX_API_KEY]: telnyxApiKey,
- };
- const result = await setSmsSettings(data);
- if (result.success) {
- toast.success(t("sms.saved"));
- router.refresh();
- } else {
- toast.error(result.error ?? t("sms.failedSave"));
- }
- });
- };
- const handleTestSms = async () => {
- if (!testPhone.trim()) {
- toast.error(t("sms.enterTestPhone"));
- return;
- }
- setIsTesting(true);
- try {
- const result = await testSmsSend(testPhone.trim());
- if (result.success) {
- toast.success(t("sms.testSentTo", { phone: result.data?.sentTo ?? '' }));
- } else {
- toast.error(result.error ?? t("sms.testFailed"));
- }
- } finally {
- setIsTesting(false);
- }
- };
- const isTestDisabled =
- isTesting ||
- !enabled ||
- !phoneNumber ||
- !testPhone.trim() ||
- (smsProvider === "twilio" && (!twilioAccountSid || !twilioAuthToken)) ||
- (smsProvider === "vonage" && (!vonageApiKey || !vonageApiSecret)) ||
- (smsProvider === "telnyx" && !telnyxApiKey);
- return (
- <div className="space-y-6">
- <ReadOnlyBanner />
- <ReadOnlyWrapper>
- <AppCard
- title={t("sms.title")}
- description={t("sms.description")}
- contentClassName="space-y-6"
- >
- <div className="flex items-center justify-between">
- <div className="space-y-0.5">
- <Label htmlFor="enable-sms">{t("sms.enableLabel")}</Label>
- <p className="text-xs text-muted-foreground">
- {t("sms.enableHint")}
- </p>
- </div>
- <Switch
- id="enable-sms"
- checked={enabled}
- onCheckedChange={setEnabled}
- />
- </div>
- {!enabled && (
- <div className="flex items-start gap-3 rounded-lg border bg-muted/50 p-4">
- <Info className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
- <p className="text-sm text-muted-foreground">
- {t("sms.disabledInfo")}
- </p>
- </div>
- )}
- {enabled && (
- <>
- {/* Provider selection */}
- <div className="flex flex-wrap gap-2">
- <Button
- type="button"
- variant={smsProvider === "twilio" ? "default" : "outline"}
- onClick={() => setSmsProvider("twilio")}
- className="flex-1"
- >
- Twilio
- </Button>
- <Button
- type="button"
- variant={smsProvider === "vonage" ? "default" : "outline"}
- onClick={() => setSmsProvider("vonage")}
- className="flex-1"
- >
- Vonage
- </Button>
- <Button
- type="button"
- variant={smsProvider === "telnyx" ? "default" : "outline"}
- onClick={() => setSmsProvider("telnyx")}
- className="flex-1"
- >
- Telnyx
- </Button>
- </div>
- {/* Phone number (shared) */}
- <div className="space-y-2">
- <Label htmlFor="sms-phone-number">
- {t("sms.phoneNumber")}
- </Label>
- <Input
- id="sms-phone-number"
- placeholder={t("sms.phoneNumberPlaceholder")}
- value={phoneNumber}
- onChange={(e) => setPhoneNumber(e.target.value)}
- />
- <p className="text-xs text-muted-foreground">
- {t("sms.phoneNumberHint")}
- </p>
- </div>
- {/* Twilio fields */}
- {smsProvider === "twilio" && (
- <div className="grid gap-4 sm:grid-cols-2">
- <div className="space-y-2">
- <Label htmlFor="twilio-sid">
- {t("sms.accountSid")}
- </Label>
- <Input
- id="twilio-sid"
- type="password"
- placeholder="AC••••••••••••••••••••••••••••••••"
- value={twilioAccountSid}
- onChange={(e) => setTwilioAccountSid(e.target.value)}
- />
- </div>
- <div className="space-y-2">
- <Label htmlFor="twilio-token">
- {t("sms.authToken")}
- </Label>
- <Input
- id="twilio-token"
- type="password"
- placeholder="••••••••••••••••••••••••••••••••"
- value={twilioAuthToken}
- onChange={(e) => setTwilioAuthToken(e.target.value)}
- />
- </div>
- </div>
- )}
- {/* Vonage fields */}
- {smsProvider === "vonage" && (
- <div className="grid gap-4 sm:grid-cols-2">
- <div className="space-y-2">
- <Label htmlFor="vonage-key">{t("sms.apiKey")}</Label>
- <Input
- id="vonage-key"
- type="password"
- placeholder="••••••••"
- value={vonageApiKey}
- onChange={(e) => setVonageApiKey(e.target.value)}
- />
- </div>
- <div className="space-y-2">
- <Label htmlFor="vonage-secret">
- {t("sms.apiSecret")}
- </Label>
- <Input
- id="vonage-secret"
- type="password"
- placeholder="••••••••••••••••"
- value={vonageApiSecret}
- onChange={(e) => setVonageApiSecret(e.target.value)}
- />
- </div>
- </div>
- )}
- {/* Telnyx fields */}
- {smsProvider === "telnyx" && (
- <div className="space-y-2">
- <Label htmlFor="telnyx-key">{t("sms.apiKey")}</Label>
- <Input
- id="telnyx-key"
- type="password"
- placeholder="KEY••••••••••••••••••••••••••••••••"
- value={telnyxApiKey}
- onChange={(e) => setTelnyxApiKey(e.target.value)}
- />
- </div>
- )}
- {/* Webhook URL */}
- {webhookSecret && (
- <div className="space-y-2">
- <Label>{t("sms.webhookUrl")}</Label>
- <div className="flex items-center gap-2">
- <Input
- readOnly
- value={webhookUrl}
- className="font-mono text-xs"
- />
- <Button
- type="button"
- variant="outline"
- size="icon"
- onClick={handleCopyWebhook}
- aria-label={t("sms.copyWebhookUrl")}
- >
- {copied ? (
- <Check className="h-4 w-4" />
- ) : (
- <Copy className="h-4 w-4" />
- )}
- </Button>
- </div>
- <p className="text-xs text-muted-foreground">
- {t("sms.webhookUrlHint", { provider: smsProvider })}
- </p>
- </div>
- )}
- {/* Test SMS */}
- <div className="space-y-3 pt-2">
- <div className="space-y-2">
- <Label htmlFor="test-phone">
- {t("sms.testPhoneNumber")}
- </Label>
- <div className="flex items-center gap-2">
- <Input
- id="test-phone"
- placeholder={t("sms.testPhonePlaceholder")}
- value={testPhone}
- onChange={(e) => setTestPhone(e.target.value)}
- className="max-w-xs"
- />
- <Button
- type="button"
- variant="outline"
- onClick={handleTestSms}
- disabled={isTestDisabled}
- >
- {isTesting ? (
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
- ) : (
- <Send className="mr-2 h-4 w-4" />
- )}
- {t("sms.sendTestSms")}
- </Button>
- </div>
- <p className="text-xs text-muted-foreground">
- {t("sms.testSmsHint")}
- </p>
- </div>
- </div>
- </>
- )}
- </AppCard>
- <SaveButton>
- <div className="flex justify-end">
- <Button onClick={handleSave} disabled={isPending}>
- {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- {t("sms.saveSettings")}
- </Button>
- </div>
- </SaveButton>
- </ReadOnlyWrapper>
- </div>
- );
- }
|