mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Add wizard for creating provider
This commit is contained in:
committed by
Torsten Dittmann
parent
08d67f7318
commit
f825b396fd
@@ -287,5 +287,8 @@ export enum Submit {
|
||||
SmsResetTemplate = 'submit_sms_reset_template',
|
||||
SmsUpdateInviteTemplate = 'submit_sms_update_invite_template',
|
||||
SmsUpdateLoginTemplate = 'submit_sms_update_login_template',
|
||||
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template'
|
||||
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template',
|
||||
MessagingProviderCreate = 'submit_messaging_provider_create',
|
||||
MessagingProviderDelete = 'submit_messaging_provider_delete',
|
||||
MessagingProviderUpdate = 'submit_messaging_provider_update'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export let name: string;
|
||||
export let group: string;
|
||||
@@ -7,6 +9,7 @@
|
||||
export let disabled = false;
|
||||
export let padding = 1;
|
||||
export let icon: string = null;
|
||||
export let imageIcon: string = null;
|
||||
export let fullHeight = true;
|
||||
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
|
||||
export let backgroundColor: string = null;
|
||||
@@ -58,5 +61,12 @@
|
||||
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if imageIcon}
|
||||
<img
|
||||
class="u-margin-inline-start-auto"
|
||||
style:--p-text-size="1.25rem"
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${imageIcon}.svg`}
|
||||
alt={imageIcon} />
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -21,25 +21,24 @@
|
||||
Id,
|
||||
ViewSelector
|
||||
} from '$lib/components';
|
||||
import Create from './create.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Container } from '$lib/layout';
|
||||
import { base } from '$app/paths';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { PageData } from './$types';
|
||||
import { showCreate, columns } from './store';
|
||||
import { columns } from './store';
|
||||
import { Pill } from '$lib/elements';
|
||||
import Provider from '../provider.svelte';
|
||||
import ProviderType from '../providerType.svelte';
|
||||
import Filters from '$lib/components/filters/filters.svelte';
|
||||
import CreateProviderDropdown from './createProviderDropdown.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let showCreateDropdownMobile = false;
|
||||
let showCreateDropdownDesktop = false;
|
||||
let showCreateDropdownEmpty = false;
|
||||
let selected: string[] = [];
|
||||
|
||||
const project = $page.params.project;
|
||||
const topicCreated = async (event: CustomEvent<Models.Team<Record<string, unknown>>>) => {
|
||||
await goto(`${base}/console/project-${project}/messaging/topics/topic-${event.detail.$id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -47,10 +46,7 @@
|
||||
<div class="u-flex u-main-space-between">
|
||||
<Heading tag="h2" size="5">Providers</Heading>
|
||||
<div class="is-only-mobile">
|
||||
<Button on:click={() => ($showCreate = true)} event="create_provider">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create provider</span>
|
||||
</Button>
|
||||
<CreateProviderDropdown bind:showCreateDropdown={showCreateDropdownMobile} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: fix width of search input in mobile -->
|
||||
@@ -63,10 +59,7 @@
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<Button on:click={() => ($showCreate = true)} event="create_provider">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create provider</span>
|
||||
</Button>
|
||||
<CreateProviderDropdown bind:showCreateDropdown={showCreateDropdownDesktop} />
|
||||
</div>
|
||||
</SearchQuery>
|
||||
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
|
||||
@@ -147,7 +140,7 @@
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.providers.total} />
|
||||
{:else if data.search}
|
||||
{:else if data.search && data.search != 'empty'}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
@@ -159,11 +152,33 @@
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<!-- TODO: Update docs links -->
|
||||
<Empty
|
||||
single
|
||||
href="https://appwrite.io/docs/references/cloud/client-web/providers"
|
||||
target="provider" />
|
||||
<Empty single target="provider">
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2" trimmed={false}>
|
||||
Create your first provider to get started.
|
||||
</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-flex-wrap u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/references/cloud/client-web/providers"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create provider`}>
|
||||
Documentation
|
||||
</Button>
|
||||
<CreateProviderDropdown bind:showCreateDropdown={showCreateDropdownEmpty}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => (showCreateDropdownEmpty = !showCreateDropdownEmpty)}
|
||||
event="create_provider">
|
||||
<span class="text">Create provider</span>
|
||||
</Button>
|
||||
</CreateProviderDropdown>
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<Create bind:showCreate={$showCreate} on:created={topicCreated} />
|
||||
|
||||
@@ -70,6 +70,6 @@ export const load = async ({ url, route }) => {
|
||||
query,
|
||||
page,
|
||||
view,
|
||||
providers: providers
|
||||
providers
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,68 +1,274 @@
|
||||
<script lang="ts">
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal, CustomId } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { InputText, Button, FormList } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
|
||||
import Provider from './wizard/provider.svelte';
|
||||
import Configure from './wizard/configure.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { project } from '../../store';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { provider, providerParams } from './wizard/store';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { Providers } from '../provider.svelte';
|
||||
|
||||
export let showCreate = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let name: string, id: string, error: string;
|
||||
let showCustomId = false;
|
||||
|
||||
const create = async () => {
|
||||
async function create() {
|
||||
try {
|
||||
const team = await sdk.forProject.teams.create(id ?? ID.unique(), name);
|
||||
name = '';
|
||||
showCreate = false;
|
||||
showCustomId = false;
|
||||
let response = { $id: '', name: '' };
|
||||
const providerId = $providerParams[$provider].providerId || ID.unique();
|
||||
switch ($provider) {
|
||||
case Providers.Twilio:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/twilio'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
accountSid: $providerParams[$provider].accountSid,
|
||||
authToken: $providerParams[$provider].authToken,
|
||||
from: $providerParams[$provider].from
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Msg91:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/msg91'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
senderId: $providerParams[$provider].senderId,
|
||||
authKey: $providerParams[$provider].authKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Telesign:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/telesign'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
username: $providerParams[$provider].username,
|
||||
password: $providerParams[$provider].password
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Textmagic:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/textmagic'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
username: $providerParams[$provider].username,
|
||||
apiKey: $providerParams[$provider].apiKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Vonage:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/vonage'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
apiKey: $providerParams[$provider].apiKey,
|
||||
apiSecret: $providerParams[$provider].apiSecret
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Mailgun:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/mailgun'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
isEuRegion: $providerParams[$provider].isEuRegion,
|
||||
from: $providerParams[$provider].from,
|
||||
apiKey: $providerParams[$provider].apiKey,
|
||||
domain: $providerParams[$provider].domain
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Sendgrid:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/sendgrid'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
apiKey: $providerParams[$provider].apiKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.FCM:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(sdk.forProject.client.config.endpoint + '/messaging/providers/fcm'),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
serverKey: $providerParams[$provider].serverKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.APNS:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/apns'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
authKey: $providerParams[$provider].authKey,
|
||||
authKeyId: $providerParams[$provider].authKeyId,
|
||||
teamId: $providerParams[$provider].teamId,
|
||||
bundleId: $providerParams[$provider].bundleId
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.MQTT:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/mqtt'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
default: $providerParams[$provider].default,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
serverKey: $providerParams[$provider].serverKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
wizard.hide();
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${team.name} has been created`
|
||||
message: `${response.name} has been created`
|
||||
});
|
||||
trackEvent(Submit.TeamCreate, {
|
||||
customId: !!id
|
||||
trackEvent(Submit.MessagingProviderCreate, {
|
||||
provider: $provider
|
||||
});
|
||||
dispatch('created', team);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, Submit.TeamCreate);
|
||||
await goto(
|
||||
`${base}/console/project-${$project.$id}/messaging/providers/provider-${response.$id}`
|
||||
);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingProviderCreate);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (!showCreate) {
|
||||
showCustomId = false;
|
||||
error = null;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
console.log('destroy');
|
||||
});
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Proivder',
|
||||
component: Provider
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
label: 'Configure',
|
||||
component: Configure
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal title="Create team" {error} size="big" bind:show={showCreate} onSubmit={create}>
|
||||
<FormList>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter name"
|
||||
autofocus={true}
|
||||
required
|
||||
bind:value={name} />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" />
|
||||
<span class="text"> Team ID </span>
|
||||
</Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId bind:show={showCustomId} name="Team" bind:id />
|
||||
{/if}
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
|
||||
<Button submit>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
<Wizard title="Create provider" steps={stepsComponents} on:finish={create} />
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { DropList, DropListItem } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { providers } from './store';
|
||||
import Create from './create.svelte';
|
||||
import { providerType, provider } from './wizard/store';
|
||||
import { ProviderTypes } from '../providerType.svelte';
|
||||
import { Providers } from '../provider.svelte';
|
||||
|
||||
export let showCreateDropdown = false;
|
||||
|
||||
const isValueOfStringEnum = <T extends Record<string, string>>(
|
||||
enumType: T,
|
||||
value: string
|
||||
): value is T[keyof T] => Object.values<string>(enumType).includes(value);
|
||||
</script>
|
||||
|
||||
<DropList bind:show={showCreateDropdown} scrollable>
|
||||
<slot>
|
||||
<Button on:click={() => (showCreateDropdown = !showCreateDropdown)} event="create_provider">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create provider</span>
|
||||
</Button>
|
||||
</slot>
|
||||
<svelte:fragment slot="list">
|
||||
{#each Object.entries(providers) as [type, option]}
|
||||
<DropListItem
|
||||
icon={option.icon}
|
||||
on:click={() => {
|
||||
if (
|
||||
type !== ProviderTypes.Email &&
|
||||
type !== ProviderTypes.Sms &&
|
||||
type !== ProviderTypes.Push
|
||||
)
|
||||
return;
|
||||
$providerType = type;
|
||||
const p = Object.keys(providers[type].providers).shift();
|
||||
if (p && isValueOfStringEnum(Providers, p)) {
|
||||
$provider = p;
|
||||
}
|
||||
showCreateDropdown = false;
|
||||
wizard.start(Create);
|
||||
}}>
|
||||
{option.name}
|
||||
</DropListItem>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
@@ -1,5 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { Providers } from '../provider.svelte';
|
||||
import { ProviderTypes } from '../providerType.svelte';
|
||||
|
||||
export let showCreate = writable(false);
|
||||
|
||||
@@ -10,3 +12,370 @@ export const columns = writable<Column[]>([
|
||||
{ id: 'type', title: 'Type', type: 'string', show: true },
|
||||
{ id: 'enabled', title: 'Status', type: 'boolean', show: true }
|
||||
]);
|
||||
|
||||
type ProvidersMap = {
|
||||
[key in ProviderTypes]: {
|
||||
name: string;
|
||||
text: string;
|
||||
icon: string;
|
||||
providers: {
|
||||
[key in Providers]?: {
|
||||
imageIcon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
configure: {
|
||||
label: string;
|
||||
name: string;
|
||||
type: 'text' | 'phone' | 'email' | 'domain' | 'file' | 'switch';
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
popover?: string[];
|
||||
allowedFileExtensions?: string[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const providers: ProvidersMap = {
|
||||
[ProviderTypes.Sms]: {
|
||||
name: 'SMS',
|
||||
text: 'SMS',
|
||||
icon: 'annotation',
|
||||
providers: {
|
||||
[Providers.Twilio]: {
|
||||
imageIcon: 'twilio',
|
||||
title: 'Twilio',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'Account SID',
|
||||
name: 'accountSid',
|
||||
type: 'text',
|
||||
placeholder: 'Enter Account SID',
|
||||
popover: [
|
||||
'<b>How to get the Account SID?</b>',
|
||||
'Head to <b>Twilio console -> Account info -> Account SID.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Auth token',
|
||||
name: 'authToken',
|
||||
type: 'text',
|
||||
placeholder: 'Enter Auth token',
|
||||
popover: [
|
||||
'<b>How to get the Auth token?</b>',
|
||||
'Head to <b>Twilio console -> Account info -> Auth Token.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone',
|
||||
popover: [
|
||||
'<b>How to get sender number?</b>',
|
||||
'Head to <b>Twilio console -> Account info -> My Twilio phone number.</b>',
|
||||
'If you have multiple Twilio phone numbers, you can select one as the default number.'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Msg91]: {
|
||||
imageIcon: 'msg91',
|
||||
title: 'MSG91',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'Auth key',
|
||||
name: 'authKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter auth key',
|
||||
popover: [
|
||||
'<b>How to get the Auth key?</b>',
|
||||
'Create an account in MSG91.',
|
||||
'Click to open the <b>Username dropdown -> Authkey -> Verify your mobile number -> Create Authkey.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender ID',
|
||||
name: 'senderId',
|
||||
type: 'text',
|
||||
placeholder: 'Enter sender ID',
|
||||
popover: [
|
||||
'<b>How to create a Sender ID?</b>',
|
||||
'Head to <b>MSG91 dashboard -> SMS -> Sender ID -> Create sender ID.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone'
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Telesign]: {
|
||||
imageIcon: 'telesign',
|
||||
title: 'Telesign',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'Username',
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
placeholder: 'Enter username'
|
||||
},
|
||||
{
|
||||
label: 'Password',
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
placeholder: 'Enter password'
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone'
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Textmagic]: {
|
||||
imageIcon: 'textmagic',
|
||||
title: 'Textmagic',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'API key',
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API key',
|
||||
popover: [
|
||||
'<b>How to get the API key?</b>',
|
||||
'Create an account in Textmagic.',
|
||||
'Head to <b>TextMagic dashboard -> API Settings -> Add new API key.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Username',
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
placeholder: 'Enter username'
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone'
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Vonage]: {
|
||||
imageIcon: 'vonage',
|
||||
title: 'Vonage',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'API key',
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API key',
|
||||
popover: [
|
||||
'<b>How to get the API key?</b>',
|
||||
'Create an account in Vonage.',
|
||||
'Head to <b>Vonage dashboard and copy the API key.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'API secret',
|
||||
name: 'apiSecret',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API secret',
|
||||
popover: [
|
||||
'<b>How to get the API secret?</b>',
|
||||
'Head to <b>Vonage dashboard and copy the API secret.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
[ProviderTypes.Email]: {
|
||||
name: 'Email',
|
||||
text: 'emails',
|
||||
icon: 'mail',
|
||||
providers: {
|
||||
[Providers.Mailgun]: {
|
||||
imageIcon: 'mailgun',
|
||||
title: 'Mailgun',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'API key',
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API key',
|
||||
popover: [
|
||||
'<b>How to get the API key?</b>',
|
||||
'Create an account in Mailgun.',
|
||||
'Head to <b>Profile -> API Security -> Add new key.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Domain',
|
||||
name: 'domain',
|
||||
type: 'domain',
|
||||
placeholder: 'Enter domain',
|
||||
popover: [
|
||||
'<b>How to create a domain?</b>',
|
||||
'Head to <b>Sending -> Domains -> Add new domain.</b>',
|
||||
'Follow <b>Mailgun instructions</b> to verify the domain name.'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'EU region',
|
||||
name: 'isEuRegion',
|
||||
type: 'switch',
|
||||
description:
|
||||
'Enable the EU region setting if your domain is within the European Union.'
|
||||
},
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'from',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Sendgrid]: {
|
||||
imageIcon: 'sendgrid',
|
||||
title: 'Sendgrid',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'API key',
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API key',
|
||||
popover: [
|
||||
'<b>How to get the API key?</b>',
|
||||
'Create an account in Mailgun.',
|
||||
'Head to <b>Profile -> API Security -> Add new key.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'from',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
[ProviderTypes.Push]: {
|
||||
name: 'Push notification',
|
||||
text: 'notifications',
|
||||
icon: 'device-mobile',
|
||||
providers: {
|
||||
[Providers.FCM]: {
|
||||
imageIcon: 'firebase',
|
||||
title: 'FCM',
|
||||
description: 'Firebase Cloud Messaging',
|
||||
configure: [
|
||||
{
|
||||
label: 'Server key (.json file)',
|
||||
name: 'serverKey',
|
||||
type: 'file',
|
||||
allowedFileExtensions: ['json'],
|
||||
placeholder: 'Enter server key',
|
||||
popover: [
|
||||
'<b>How to get the FCM server key?</b>',
|
||||
'Head to <b>Project settings -> Service accounts -> Generate new private key.</b>',
|
||||
'Generating the new key will result in the download of a JSON file.'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.APNS]: {
|
||||
imageIcon: 'apple',
|
||||
title: 'APNS',
|
||||
description: 'Apple Push Notification Service',
|
||||
configure: [
|
||||
{
|
||||
label: 'Team ID',
|
||||
name: 'teamId',
|
||||
type: 'text',
|
||||
placeholder: 'Enter team ID',
|
||||
popover: [
|
||||
'<b>How to get the team ID?</b>',
|
||||
'Head to <b>Apple Developer Member Center -> Membership details -> Team ID.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Bundle ID',
|
||||
name: 'bundleId',
|
||||
type: 'text',
|
||||
placeholder: 'Enter bundle ID',
|
||||
popover: [
|
||||
'<b>How to get the bundle ID?</b>',
|
||||
'Head to <b>Apple Developer Member Center -> Certificates, Identifiers & Profiles -> Identifiers.</b>',
|
||||
`<a
|
||||
href="/images/apns-bundle-id.png"
|
||||
class="file-preview is-with-image"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="open file in new window">
|
||||
<div class="file-preview-image">
|
||||
<img
|
||||
width="205"
|
||||
height="125"
|
||||
src="/images/apns-bundle-id.png"
|
||||
alt="Screenshot of Bundle ID in Apple" />
|
||||
</div>
|
||||
<div class="file-preview-content">
|
||||
<div class="avatar">
|
||||
<span class="icon-external-link" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</a>`
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Authentication key ID',
|
||||
name: 'authKeyId',
|
||||
type: 'text',
|
||||
placeholder: 'Enter key ID',
|
||||
popover: [
|
||||
'<b>How to get the auth key ID?</b>',
|
||||
'Head to <b>Apple Developer Member Center -> Certificates, Identifiers & Profiles -> Keys.</b>',
|
||||
'Click on your key to view details.'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Auth key (.p8 file)',
|
||||
name: 'authKey',
|
||||
type: 'file',
|
||||
allowedFileExtensions: ['p8'],
|
||||
popover: [
|
||||
'<b>How to get the authentication key?</b>',
|
||||
'Head to <b>Apple Developer Member Center</b> (under Program resources) <b>-> Certificates, Identifiers & Profiles -> Keys.</b>',
|
||||
'Create a key and give it a name. Enable the Apple Push Notifications service (APNS), and register your key.'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
// [Providers.MQTT]: {
|
||||
// imageIcon: 'mqtt',
|
||||
// title: 'MQTT',
|
||||
// description: 'Message Queuing Telemtry Transport'
|
||||
// }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
FormList,
|
||||
InputDomain,
|
||||
InputEmail,
|
||||
InputFile,
|
||||
InputSwitch,
|
||||
InputText
|
||||
} from '$lib/elements/forms';
|
||||
import InputPhone from '$lib/elements/forms/inputPhone.svelte';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { providers } from '../store';
|
||||
import { providerType, provider, providerParams } from './store';
|
||||
|
||||
let files: Record<string, FileList> = {};
|
||||
const inputs = providers[$providerType].providers[$provider].configure;
|
||||
|
||||
async function beforeSubmit() {
|
||||
const promises = [];
|
||||
for (const [key, value] of Object.entries(files)) {
|
||||
const promise = value[0].text().then((text) => {
|
||||
$providerParams[$provider][key] = text;
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
console.log($providerParams[$provider]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Configure</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Set up the credentials below to enable {providers[$providerType].providers[$provider].title}
|
||||
for sending
|
||||
{providers[$providerType].text}.
|
||||
</svelte:fragment>
|
||||
<FormList>
|
||||
{#each inputs as input}
|
||||
{#if input.type === 'text'}
|
||||
<InputText
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
bind:value={$providerParams[$provider][input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
{@html input.popover?.join('<br/><br/>')}
|
||||
</svelte:fragment>
|
||||
</InputText>
|
||||
{:else if input.type === 'email'}
|
||||
<InputEmail
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
bind:value={$providerParams[$provider][input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<p class="body-text-2 u-margin-block-end-16">
|
||||
{@html input.popover?.join('<br/><br/>')}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</InputEmail>
|
||||
{:else if input.type === 'domain'}
|
||||
<InputDomain
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
bind:value={$providerParams[$provider][input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<p class="body-text-2 u-margin-block-end-16">
|
||||
{@html input.popover?.join('<br/><br/>')}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</InputDomain>
|
||||
{:else if input.type === 'phone'}
|
||||
<InputPhone
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
bind:value={$providerParams[$provider][input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<p class="body-text-2 u-margin-block-end-16">
|
||||
{@html input.popover?.join('<br/><br/>')}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</InputPhone>
|
||||
{:else if input.type === 'file'}
|
||||
<InputFile
|
||||
label={input.label}
|
||||
allowedFileExtensions={input.allowedFileExtensions}
|
||||
bind:files={files[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<p class="body-text-2 u-margin-block-end-16">
|
||||
{@html input.popover?.join('<br/><br/>')}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</InputFile>
|
||||
{:else if input.type === 'switch'}
|
||||
<InputSwitch
|
||||
label={input.label}
|
||||
id={input.name}
|
||||
bind:value={$providerParams[$provider][input.name]}>
|
||||
<svelte:fragment slot="description">
|
||||
{input.description}
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
{/if}
|
||||
{/each}
|
||||
</FormList>
|
||||
|
||||
<p class="body-text-2 u-bold u-margin-block-start-48">Need a hand?</p>
|
||||
|
||||
<div
|
||||
class="u-flex u-cross-center u-main-space-between u-padding-block-16"
|
||||
style="border-block-end: solid .0625rem hsl(var(--color-border))">
|
||||
<div class="u-flex u-cross-center u-gap-16">
|
||||
<div class="avatar is-size-small">
|
||||
<span class="icon-book-open" style:--p-text-size="1.25rem" aria-hidden="true" />
|
||||
</div>
|
||||
Read the full guide in the documentation
|
||||
</div>
|
||||
<span class="icon-arrow-right" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-cross-center u-main-space-between u-padding-block-16">
|
||||
<div class="u-flex u-cross-center u-gap-16">
|
||||
<div class="avatar is-size-small">
|
||||
<span class="icon-user-group" style:--p-text-size="1.25rem" aria-hidden="true" />
|
||||
</div>
|
||||
Invite a team member to complete this step
|
||||
</div>
|
||||
<span class="icon-arrow-right" aria-hidden="true" />
|
||||
</div>
|
||||
</WizardStep>
|
||||
@@ -0,0 +1,161 @@
|
||||
<script lang="ts">
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { providerType, provider, providerParams } from './store';
|
||||
import { CustomId, LabelCard } from '$lib/components';
|
||||
import { providers } from '../store';
|
||||
import { FormList, InputText } from '$lib/elements/forms';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Providers } from '../../provider.svelte';
|
||||
|
||||
let name = '';
|
||||
let showCustomId = false;
|
||||
let id: string = null;
|
||||
|
||||
async function beforeSubmit() {
|
||||
console.log($provider);
|
||||
|
||||
switch ($provider) {
|
||||
case Providers.Twilio:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
default: false,
|
||||
enabled: false,
|
||||
accountSid: '',
|
||||
authToken: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Msg91:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
default: false,
|
||||
enabled: false,
|
||||
from: '',
|
||||
senderId: '',
|
||||
authKey: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Telesign:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
default: false,
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Textmagic:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
default: false,
|
||||
enabled: false,
|
||||
username: '',
|
||||
apiKey: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Vonage:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
default: false,
|
||||
enabled: false,
|
||||
apiKey: '',
|
||||
apiSecret: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Mailgun:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
default: false,
|
||||
enabled: false,
|
||||
isEuRegion: false,
|
||||
from: '',
|
||||
apiKey: '',
|
||||
domain: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Sendgrid:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
default: false,
|
||||
enabled: false,
|
||||
apiKey: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
case Providers.FCM:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
default: false,
|
||||
enabled: false,
|
||||
serverKey: ''
|
||||
};
|
||||
break;
|
||||
case Providers.APNS:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
default: false,
|
||||
enabled: false,
|
||||
authKey: '',
|
||||
authKeyId: '',
|
||||
teamId: '',
|
||||
bundleId: ''
|
||||
};
|
||||
break;
|
||||
case Providers.MQTT:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Provider</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Provider name"
|
||||
bind:value={name}
|
||||
autofocus
|
||||
required />
|
||||
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Provider ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId bind:show={showCustomId} name="Provider" bind:id autofocus={false} />
|
||||
{/if}
|
||||
<p class="u-margin-block-start-24">
|
||||
Select a provider you would like to enable for sending {providers[$providerType].text}.
|
||||
</p>
|
||||
<div class="grid-box">
|
||||
{#each Object.entries(providers[$providerType].providers) as [value, option]}
|
||||
<LabelCard
|
||||
name="provider"
|
||||
{value}
|
||||
bind:group={$provider}
|
||||
imageIcon={option.imageIcon}>
|
||||
<svelte:fragment slot="title">{option.title}</svelte:fragment>
|
||||
{#if option.description}
|
||||
{option.description}
|
||||
{/if}
|
||||
</LabelCard>
|
||||
{/each}
|
||||
</div>
|
||||
</FormList>
|
||||
</WizardStep>
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { Providers } from '../../provider.svelte';
|
||||
import type { ProviderTypes } from '../../providerType.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
type ProviderParams = {
|
||||
providerId: string;
|
||||
name: string;
|
||||
default: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* SMS providers
|
||||
*/
|
||||
|
||||
export type TwilioProviderParams = ProviderParams & {
|
||||
accountSid: string;
|
||||
authToken: string;
|
||||
from: string;
|
||||
};
|
||||
|
||||
export type Msg91ProviderParams = ProviderParams & {
|
||||
from: string;
|
||||
senderId: string;
|
||||
authKey: string;
|
||||
};
|
||||
|
||||
export type TelesignProviderParams = ProviderParams & {
|
||||
from: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TextmagicProviderParams = ProviderParams & {
|
||||
from: string;
|
||||
username: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export type VonageProviderParams = ProviderParams & {
|
||||
from: string;
|
||||
apiKey: string;
|
||||
apiSecret: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Email providers
|
||||
*/
|
||||
|
||||
export type MailgunProviderParams = ProviderParams & {
|
||||
isEuRegion: boolean;
|
||||
from: string;
|
||||
apiKey: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export type SendgridProviderParams = ProviderParams & {
|
||||
from: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Push providers
|
||||
*/
|
||||
|
||||
export type FCMProviderParams = ProviderParams & {
|
||||
serverKey: string;
|
||||
};
|
||||
|
||||
export type APNSProviderParams = ProviderParams & {
|
||||
authKey: string;
|
||||
authKeyId: string;
|
||||
teamId: string;
|
||||
bundleId: string;
|
||||
};
|
||||
|
||||
export type MQTTProviderParams = ProviderParams & {
|
||||
serverKey: string;
|
||||
};
|
||||
|
||||
export const providerType = writable<ProviderTypes>(null);
|
||||
export const provider = writable<Providers>(null);
|
||||
export const providerParams = writable<{
|
||||
twilio: Partial<TwilioProviderParams>;
|
||||
msg91: Partial<Msg91ProviderParams>;
|
||||
telesign: Partial<TelesignProviderParams>;
|
||||
textmagic: Partial<TextmagicProviderParams>;
|
||||
vonage: Partial<VonageProviderParams>;
|
||||
mailgun: Partial<MailgunProviderParams>;
|
||||
sendgrid: Partial<SendgridProviderParams>;
|
||||
fcm: Partial<FCMProviderParams>;
|
||||
apns: Partial<APNSProviderParams>;
|
||||
}>({
|
||||
twilio: null,
|
||||
msg91: null,
|
||||
telesign: null,
|
||||
textmagic: null,
|
||||
vonage: null,
|
||||
mailgun: null,
|
||||
sendgrid: null,
|
||||
fcm: null,
|
||||
apns: null
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Reference in New Issue
Block a user