12 implement twilio sms (#37)

* add twilio sdk

* add twilio config

use app version in config

* remove default config

* phone regex

* bun update

* create client

* fix env var

* create Textarea component

* move TextInput

* allow snippets on labels

* update with label and error

* move button

* make button children snippet

* add form props

* allow region code

* add twilio FROM number

* rename to twilioClient

* implement simple messaging

* add twilio phone number as empty var

* format

* move twilio client to local on requests

* fix story

* on second thought, dont use locals since we are only using twilio in one place

Don't want to init a twilio client on every request when its only used
in on a single page

* use i18n for page text

* validate env with a script

* remove Zod validation when loading env vars
This commit is contained in:
Baobeld 2025-01-02 20:11:27 -05:00 committed by GitHub
parent a4d998665b
commit 1c5b37b24b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 418 additions and 119 deletions

View file

@ -1,2 +1,4 @@
<script lang="ts">
</script>
<a href="/app/sms" class="btn btn-ghost">SMS</a>

View file

@ -0,0 +1,49 @@
import { TWILIO_PHONE_NUMBER } from '$env/static/private';
import { PhoneRegex } from '$lib/regex';
import { logger } from '$lib/server/logger';
import { TwilioClient } from '$lib/server/twilio';
import { fail, type Actions } from '@sveltejs/kit';
import zod from 'zod';
export const actions = {
push: async (event) => {
const form = await event.request.formData();
if (!form.has('phone')) {
return fail(400, { error: 'phone_missing' });
}
if (!form.get('message')) {
return fail(400, { error: 'message_missing' });
}
const {
success: phoneSuccess,
data: phone,
error: phoneError,
} = zod.string().regex(PhoneRegex).safeParse(form.get('phone'));
if (!phoneSuccess) {
logger.error(phoneError);
return fail(400, { error: 'invalid_phone' });
}
const message = form.get('message');
if (typeof message !== 'string') {
return fail(400, { error: 'invalid_message' });
}
try {
const result = await TwilioClient.messages.create({
to: phone,
body: message,
from: TWILIO_PHONE_NUMBER,
});
logger.debug(result);
} catch (e) {
logger.error(e);
fail(500, { success: false });
}
return {
success: true,
};
},
} satisfies Actions;

View file

@ -0,0 +1,68 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { Button } from '$lib/components/Actions';
import { Textarea, TextInput } from '$lib/components/DataInput';
import { fade } from 'svelte/transition';
import type { ActionData } from './$types';
import { messages } from '$lib/i18n';
type Props = {
form: ActionData;
};
let { form }: Props = $props();
</script>
{#snippet PhoneLabel()}
<i class="fi fi-sr-phone-flip"></i> {messages.sms_label_phone()}
{/snippet}
{#snippet MessageLabel()}
<i class="fi fi-sr-comment-alt"></i> {messages.sms_label_message()}
{/snippet}
{#snippet alert(message: string)}
<div role="alert" class="alert alert-error absolute -top-20" transition:fade>
<i class="fi fi-bs-octagon-xmark h-6 w-6 shrink-0"></i>
<span>{message}</span>
</div>
{/snippet}
<div class="page" transition:fade>
<div class="card bg-base-200 px-4 pt-4 shadow-xl">
<div class="card-title justify-center">
<h2 class="text-2xl font-semibold">{messages.sms_prompt()}</h2>
{#if form?.error}
{@render alert(form.error)}
{/if}
</div>
<form id="sms" method="POST" action="?/push" use:enhance>
<div class="card-body">
<TextInput
type="tel"
name="phone"
label={PhoneLabel}
placeholder="XXX-XXX-XXXX"
bordered
fade
/>
<Textarea
label={MessageLabel}
size="lg"
error={form?.error}
name="message"
placeholder="..."
form="sms"
/>
</div>
<div class="card-actions justify-center px-8 pb-4">
<Button outline type="submit" full>{messages.sms_button_submit()}</Button>
</div>
</form>
</div>
</div>
<style>
.page {
@apply flex flex-col items-center justify-around gap-24 py-[10%];
}
</style>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import Button from '$lib/components/common/Button';
import TextInput from '$lib/components/common/TextInput';
import { TextInput } from '$lib/components/DataInput';
import { Button } from '$lib/components/Actions';
import Tabs from '$lib/components/Navigation/Tabs';
import { messages } from '$lib/i18n/index.js';
import { fade } from 'svelte/transition';
@ -54,7 +54,7 @@
{/if}
</div>
<div class="card-actions px-4">
<Button block type="submit" label={messages.login_button_submit()} outline />
<Button block type="submit" outline>{messages.login_button_submit()}</Button>
</div>
</form>
{/snippet}