Provider Improvements (#123)

* Fixes push notification provider

Improves provider schema form building

* Minor tweak
This commit is contained in:
Chris Anderson 2023-04-12 22:36:04 -05:00 committed by GitHub
parent db83ff9ea6
commit 6adc68215b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 119 additions and 34 deletions

View file

@ -38,7 +38,10 @@ export default class MailgunEmailProvider extends EmailProvider {
type: 'object',
required: ['api_key', 'domain'],
properties: {
api_key: { type: 'string' },
api_key: {
type: 'string',
title: 'API Key',
},
domain: { type: 'string' },
webhook_signing_key: {
type: 'string',

View file

@ -43,8 +43,14 @@ export default class SESEmailProvider extends EmailProvider {
type: 'object',
required: ['accessKeyId', 'secretAccessKey'],
properties: {
accessKeyId: { type: 'string' },
secretAccessKey: { type: 'string' },
accessKeyId: {
type: 'string',
title: 'Access Key ID',
},
secretAccessKey: {
type: 'string',
title: 'Secret Access Key',
},
},
},
},

View file

@ -12,6 +12,7 @@ interface APNParams {
teamId: string
}
production: boolean
bundleId: string
}
interface FCMParams {
@ -46,27 +47,49 @@ export default class LocalPushProvider extends PushProvider {
type: 'object',
nullable: true,
required: ['production', 'token'],
title: 'APN',
description: 'Settings for Apple Push Notifications to send messages to iOS devices.',
properties: {
production: {
type: 'boolean',
description: 'Leave unchecked if you are wanting to send to sandbox devices only.',
},
token: {
type: 'object',
required: ['key', 'keyId', 'teamId'],
properties: {
key: { type: 'string' },
keyId: { type: 'string' },
teamId: { type: 'string' },
key: {
type: 'string',
minLength: 80,
},
keyId: {
type: 'string',
title: 'Key ID',
},
teamId: {
type: 'string',
title: 'Team ID',
},
},
},
bundleId: {
type: 'string',
title: 'Bundle ID',
},
},
},
fcm: {
type: 'object',
nullable: true,
required: ['id'],
title: 'FCM',
description: 'Settings for Firebase Cloud Messaging to send messages to Android devices.',
properties: {
id: { type: 'string' },
id: {
type: 'string',
title: 'Server Key',
minLength: 80,
},
},
},
},
@ -81,10 +104,11 @@ export default class LocalPushProvider extends PushProvider {
}
async send(push: Push): Promise<PushResponse> {
const { tokens, title, topic, body, custom } = push
// TODO: Need a better way of bubbling up errors
const { tokens, title, body, custom } = push
const response = await this.transport.send(typeof tokens === 'string' ? [tokens] : tokens, {
title,
topic,
topic: this.apn?.bundleId,
body,
custom,
})

View file

@ -7,6 +7,7 @@ export default class PushChannel {
constructor(provider?: PushProvider) {
if (provider) {
this.provider = provider
this.provider.boot?.()
} else {
throw new Error('A valid push notification provider must be defined!')
}
@ -17,6 +18,9 @@ export default class PushChannel {
// Find tokens from active devices with push enabled
const tokens = variables.user.pushEnabledDevices.map(device => device.token)
// If no tokens, don't send
if (tokens?.length <= 0) return
const push = {
tokens,
...template.compile(variables),

View file

@ -30,8 +30,14 @@ export default class NexmoTextProvider extends TextProvider {
type: 'object',
required: ['api_key', 'api_secret', 'phone_number'],
properties: {
api_key: { type: 'string' },
api_secret: { type: 'string' },
api_key: {
type: 'string',
title: 'API Key',
},
api_secret: {
type: 'string',
title: 'API Secret',
},
phone_number: { type: 'string' },
},
})

View file

@ -29,7 +29,10 @@ export default class PlivoTextProvider extends TextProvider {
type: 'object',
required: ['auth_id', 'auth_token', 'phone_number'],
properties: {
auth_id: { type: 'string' },
auth_id: {
type: 'string',
title: 'Auth ID',
},
auth_token: { type: 'string' },
phone_number: { type: 'string' },
},

View file

@ -37,7 +37,10 @@ export default class TwilioTextProvider extends TextProvider {
type: 'object',
required: ['account_sid', 'auth_token', 'phone_number'],
properties: {
account_sid: { type: 'string' },
account_sid: {
type: 'string',
title: 'Account SID',
},
auth_token: { type: 'string' },
phone_number: { type: 'string' },
},

View file

@ -167,7 +167,7 @@ export class PushTemplate extends Template {
this.title = json?.data.title
this.topic = json?.data.topic
this.body = json?.data.body
this.custom = json?.data.custom
this.custom = json?.data.custom ?? {}
}
compile(variables: Variables): CompiledPush {

View file

@ -9,6 +9,8 @@ import { UserEvent } from '../users/UserEvent'
import { loadTextChannel } from '../providers/text'
import { RequestError } from '../core/errors'
import CampaignError from '../campaigns/CampaignError'
import { loadPushChannel } from '../providers/push'
import { getUserFromEmail, getUserFromPhone } from '../users/UserRepository'
export const pagedTemplates = async (params: SearchParams, projectId: number) => {
return await Template.searchParams(
@ -65,20 +67,33 @@ export const sendProof = async (template: TemplateType, variables: Variables, re
const campaign = await getCampaign(template.campaign_id, template.project_id)
if (!campaign) throw new RequestError(CampaignError.CampaignDoesNotExist)
const event = UserEvent.fromJson(variables.event || {})
const context = variables.context || {}
const projectId = template.project_id
if (template.type === 'email') {
const channel = await loadEmailChannel(campaign.provider_id, template.project_id)
const channel = await loadEmailChannel(campaign.provider_id, projectId)
await channel?.send(template, {
user: User.fromJson({ ...variables.user, data: variables.user, email: recipient }),
event: UserEvent.fromJson(variables.event || {}),
context: variables.context || {},
event,
context,
})
} else if (template.type === 'text') {
const channel = await loadTextChannel(campaign.provider_id, template.project_id)
const channel = await loadTextChannel(campaign.provider_id, projectId)
await channel?.send(template, {
user: User.fromJson({ ...variables.user, data: variables.user, phone: recipient }),
event: UserEvent.fromJson(variables.event || {}),
context: variables.context || {},
event,
context,
})
} else if (template.type === 'push') {
const channel = await loadPushChannel(campaign.provider_id, projectId)
const user = await getUserFromEmail(projectId, recipient) ?? await getUserFromPhone(projectId, recipient)
if (!user) throw new RequestError('Unable to find a user matching the criteria.')
user.data = { ...variables.user, ...user.data }
await channel?.send(template, {
user,
event,
context,
})
} else {
throw new RequestError('Sending template proofs is only supported for email and text message types as this time.')

View file

@ -255,7 +255,7 @@ export const subscriptionCreateSchema: JSONSchemaType<SubscriptionParams> = {
},
channel: {
type: 'string',
enum: ['email', 'text', 'webhook'],
enum: ['email', 'text', 'push', 'webhook'],
},
},
additionalProperties: false,

View file

@ -26,7 +26,6 @@ export default function FormWrapper<T extends FieldValues>({
})
const handleSubmit = form.handleSubmit(async data => {
console.log('submitting', data)
setIsLoading(true)
onSubmit(data, navigate).finally(() => {
setIsLoading(false)

View file

@ -2,4 +2,16 @@
border: 1px solid var(--color-grey);
border-radius: var(--border-radius);
padding: 10px;
}
.ui-schema-form > h5 {
margin-top: 15px;
margin-bottom: 2px;
}
.ui-schema-form > p {
margin: 2px 0 10px;
color: var(--color-primary-soft);
font-weight: 400;
font-size: 14px;
}

View file

@ -5,18 +5,22 @@ import SwitchField from './SwitchField'
interface Schema {
type: 'string' | 'number' | 'boolean' | 'object'
title?: string
description?: string
properties?: Record<string, Schema>
required?: string[]
minLength?: number
}
interface SchemaProps {
title?: string
description?: string
parent: string
schema: Schema
form: any
}
export default function SchemaFields({ title, parent, form, schema }: SchemaProps) {
export default function SchemaFields({ title, description, parent, form, schema }: SchemaProps) {
if (!schema?.properties) {
return <></>
}
@ -25,28 +29,40 @@ export default function SchemaFields({ title, parent, form, schema }: SchemaProp
const keys = Object.keys(schema.properties)
return <div className="ui-schema-form">
{title && <h5>{snakeToTitle(title)}</h5>}
{description && <p>{description}</p> }
<div className="ui-schema-fields">
{keys.map(key => {
const item = props[key]
const required = schema.required?.includes(key)
const title = item.title ?? snakeToTitle(key)
if (item.type === 'string' || item.type === 'number') {
return <TextInput.Field
key={key}
form={form}
name={`${parent}.${key}`}
label={snakeToTitle(key)}
label={title}
subtitle={item.description}
required={required}
textarea={(item.minLength ?? 0) >= 80}
/>
} else if (item.type === 'boolean') {
return <SwitchField
key={key}
form={form}
name={`${parent}.${key}`}
label={snakeToTitle(key)}
label={title}
subtitle={item.description}
required={required}
/>
} else if (item.type === 'object') {
return SchemaFields({ title: key, form, parent: `${parent}.${key}`, schema: item })
return <SchemaFields
key={key}
form={form}
title={title}
description={item.description}
parent={`${parent}.${key}`}
schema={item}
/>
}
return 'no key'
})}

View file

@ -32,7 +32,7 @@ export default function SwitchField<X extends FieldValues, P extends FieldPath<X
id={id}
checked={checked}
onChange={(event) => onChange?.(event.target.checked)}
{...form?.register(name, { disabled, required })}
{...form?.register(name, { disabled })}
/>
<div className="slider round"></div>
</div>

View file

@ -66,7 +66,7 @@ const SendProof = ({ open, onClose, onSubmit, type }: SendProofProps) => {
open={open}
onClose={onClose}
title="Send Proof"
description={`Enter the ${type === 'email' ? 'email address' : 'phone number'} of the recipient you want to receive the proof of this template.`}>
description={`Enter the ${type === 'email' ? 'email address' : 'email or phone number'} of the recipient you want to receive the proof of this template.`}>
<FormWrapper<TemplateProofParams>
onSubmit={async ({ recipient }) => await onSubmit(recipient)}>
{form => (

View file

@ -64,7 +64,6 @@ const TextForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> })
const PushTable = ({ data }: { data: PushTemplateData }) => <InfoTable rows={{
Title: data.title,
Body: data.body,
Topic: data.topic,
}} />
const PushForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> }) => <>
@ -79,11 +78,6 @@ const PushForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> })
label="Body"
textarea
required />
<TextInput.Field
form={form}
name="data.topic"
label="Topic"
required />
</>
interface TemplateDetailProps {