mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-08-29 11:56:04 +08:00
Provider Improvements (#123)
* Fixes push notification provider Improves provider schema form building * Minor tweak
This commit is contained in:
parent
db83ff9ea6
commit
6adc68215b
16 changed files with 119 additions and 34 deletions
|
@ -38,7 +38,10 @@ export default class MailgunEmailProvider extends EmailProvider {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['api_key', 'domain'],
|
required: ['api_key', 'domain'],
|
||||||
properties: {
|
properties: {
|
||||||
api_key: { type: 'string' },
|
api_key: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'API Key',
|
||||||
|
},
|
||||||
domain: { type: 'string' },
|
domain: { type: 'string' },
|
||||||
webhook_signing_key: {
|
webhook_signing_key: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
|
@ -43,8 +43,14 @@ export default class SESEmailProvider extends EmailProvider {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['accessKeyId', 'secretAccessKey'],
|
required: ['accessKeyId', 'secretAccessKey'],
|
||||||
properties: {
|
properties: {
|
||||||
accessKeyId: { type: 'string' },
|
accessKeyId: {
|
||||||
secretAccessKey: { type: 'string' },
|
type: 'string',
|
||||||
|
title: 'Access Key ID',
|
||||||
|
},
|
||||||
|
secretAccessKey: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Secret Access Key',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface APNParams {
|
||||||
teamId: string
|
teamId: string
|
||||||
}
|
}
|
||||||
production: boolean
|
production: boolean
|
||||||
|
bundleId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FCMParams {
|
interface FCMParams {
|
||||||
|
@ -46,27 +47,49 @@ export default class LocalPushProvider extends PushProvider {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
required: ['production', 'token'],
|
required: ['production', 'token'],
|
||||||
|
title: 'APN',
|
||||||
|
description: 'Settings for Apple Push Notifications to send messages to iOS devices.',
|
||||||
properties: {
|
properties: {
|
||||||
production: {
|
production: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
description: 'Leave unchecked if you are wanting to send to sandbox devices only.',
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['key', 'keyId', 'teamId'],
|
required: ['key', 'keyId', 'teamId'],
|
||||||
properties: {
|
properties: {
|
||||||
key: { type: 'string' },
|
key: {
|
||||||
keyId: { type: 'string' },
|
type: 'string',
|
||||||
teamId: { type: 'string' },
|
minLength: 80,
|
||||||
|
},
|
||||||
|
keyId: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Key ID',
|
||||||
|
},
|
||||||
|
teamId: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Team ID',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bundleId: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Bundle ID',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fcm: {
|
fcm: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
required: ['id'],
|
required: ['id'],
|
||||||
|
title: 'FCM',
|
||||||
|
description: 'Settings for Firebase Cloud Messaging to send messages to Android devices.',
|
||||||
properties: {
|
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> {
|
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, {
|
const response = await this.transport.send(typeof tokens === 'string' ? [tokens] : tokens, {
|
||||||
title,
|
title,
|
||||||
topic,
|
topic: this.apn?.bundleId,
|
||||||
body,
|
body,
|
||||||
custom,
|
custom,
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,6 +7,7 @@ export default class PushChannel {
|
||||||
constructor(provider?: PushProvider) {
|
constructor(provider?: PushProvider) {
|
||||||
if (provider) {
|
if (provider) {
|
||||||
this.provider = provider
|
this.provider = provider
|
||||||
|
this.provider.boot?.()
|
||||||
} else {
|
} else {
|
||||||
throw new Error('A valid push notification provider must be defined!')
|
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
|
// Find tokens from active devices with push enabled
|
||||||
const tokens = variables.user.pushEnabledDevices.map(device => device.token)
|
const tokens = variables.user.pushEnabledDevices.map(device => device.token)
|
||||||
|
|
||||||
|
// If no tokens, don't send
|
||||||
|
if (tokens?.length <= 0) return
|
||||||
|
|
||||||
const push = {
|
const push = {
|
||||||
tokens,
|
tokens,
|
||||||
...template.compile(variables),
|
...template.compile(variables),
|
||||||
|
|
|
@ -30,8 +30,14 @@ export default class NexmoTextProvider extends TextProvider {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['api_key', 'api_secret', 'phone_number'],
|
required: ['api_key', 'api_secret', 'phone_number'],
|
||||||
properties: {
|
properties: {
|
||||||
api_key: { type: 'string' },
|
api_key: {
|
||||||
api_secret: { type: 'string' },
|
type: 'string',
|
||||||
|
title: 'API Key',
|
||||||
|
},
|
||||||
|
api_secret: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'API Secret',
|
||||||
|
},
|
||||||
phone_number: { type: 'string' },
|
phone_number: { type: 'string' },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,7 +29,10 @@ export default class PlivoTextProvider extends TextProvider {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['auth_id', 'auth_token', 'phone_number'],
|
required: ['auth_id', 'auth_token', 'phone_number'],
|
||||||
properties: {
|
properties: {
|
||||||
auth_id: { type: 'string' },
|
auth_id: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Auth ID',
|
||||||
|
},
|
||||||
auth_token: { type: 'string' },
|
auth_token: { type: 'string' },
|
||||||
phone_number: { type: 'string' },
|
phone_number: { type: 'string' },
|
||||||
},
|
},
|
||||||
|
|
|
@ -37,7 +37,10 @@ export default class TwilioTextProvider extends TextProvider {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['account_sid', 'auth_token', 'phone_number'],
|
required: ['account_sid', 'auth_token', 'phone_number'],
|
||||||
properties: {
|
properties: {
|
||||||
account_sid: { type: 'string' },
|
account_sid: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Account SID',
|
||||||
|
},
|
||||||
auth_token: { type: 'string' },
|
auth_token: { type: 'string' },
|
||||||
phone_number: { type: 'string' },
|
phone_number: { type: 'string' },
|
||||||
},
|
},
|
||||||
|
|
|
@ -167,7 +167,7 @@ export class PushTemplate extends Template {
|
||||||
this.title = json?.data.title
|
this.title = json?.data.title
|
||||||
this.topic = json?.data.topic
|
this.topic = json?.data.topic
|
||||||
this.body = json?.data.body
|
this.body = json?.data.body
|
||||||
this.custom = json?.data.custom
|
this.custom = json?.data.custom ?? {}
|
||||||
}
|
}
|
||||||
|
|
||||||
compile(variables: Variables): CompiledPush {
|
compile(variables: Variables): CompiledPush {
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { UserEvent } from '../users/UserEvent'
|
||||||
import { loadTextChannel } from '../providers/text'
|
import { loadTextChannel } from '../providers/text'
|
||||||
import { RequestError } from '../core/errors'
|
import { RequestError } from '../core/errors'
|
||||||
import CampaignError from '../campaigns/CampaignError'
|
import CampaignError from '../campaigns/CampaignError'
|
||||||
|
import { loadPushChannel } from '../providers/push'
|
||||||
|
import { getUserFromEmail, getUserFromPhone } from '../users/UserRepository'
|
||||||
|
|
||||||
export const pagedTemplates = async (params: SearchParams, projectId: number) => {
|
export const pagedTemplates = async (params: SearchParams, projectId: number) => {
|
||||||
return await Template.searchParams(
|
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)
|
const campaign = await getCampaign(template.campaign_id, template.project_id)
|
||||||
if (!campaign) throw new RequestError(CampaignError.CampaignDoesNotExist)
|
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') {
|
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, {
|
await channel?.send(template, {
|
||||||
user: User.fromJson({ ...variables.user, data: variables.user, email: recipient }),
|
user: User.fromJson({ ...variables.user, data: variables.user, email: recipient }),
|
||||||
event: UserEvent.fromJson(variables.event || {}),
|
event,
|
||||||
context: variables.context || {},
|
context,
|
||||||
})
|
})
|
||||||
} else if (template.type === 'text') {
|
} 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, {
|
await channel?.send(template, {
|
||||||
user: User.fromJson({ ...variables.user, data: variables.user, phone: recipient }),
|
user: User.fromJson({ ...variables.user, data: variables.user, phone: recipient }),
|
||||||
event: UserEvent.fromJson(variables.event || {}),
|
event,
|
||||||
context: variables.context || {},
|
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 {
|
} else {
|
||||||
throw new RequestError('Sending template proofs is only supported for email and text message types as this time.')
|
throw new RequestError('Sending template proofs is only supported for email and text message types as this time.')
|
||||||
|
|
|
@ -255,7 +255,7 @@ export const subscriptionCreateSchema: JSONSchemaType<SubscriptionParams> = {
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['email', 'text', 'webhook'],
|
enum: ['email', 'text', 'push', 'webhook'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
|
|
|
@ -26,7 +26,6 @@ export default function FormWrapper<T extends FieldValues>({
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async data => {
|
const handleSubmit = form.handleSubmit(async data => {
|
||||||
console.log('submitting', data)
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
onSubmit(data, navigate).finally(() => {
|
onSubmit(data, navigate).finally(() => {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
border: 1px solid var(--color-grey);
|
border: 1px solid var(--color-grey);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: 10px;
|
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;
|
||||||
}
|
}
|
|
@ -5,18 +5,22 @@ import SwitchField from './SwitchField'
|
||||||
|
|
||||||
interface Schema {
|
interface Schema {
|
||||||
type: 'string' | 'number' | 'boolean' | 'object'
|
type: 'string' | 'number' | 'boolean' | 'object'
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
properties?: Record<string, Schema>
|
properties?: Record<string, Schema>
|
||||||
required?: string[]
|
required?: string[]
|
||||||
|
minLength?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SchemaProps {
|
interface SchemaProps {
|
||||||
title?: string
|
title?: string
|
||||||
|
description?: string
|
||||||
parent: string
|
parent: string
|
||||||
schema: Schema
|
schema: Schema
|
||||||
form: any
|
form: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SchemaFields({ title, parent, form, schema }: SchemaProps) {
|
export default function SchemaFields({ title, description, parent, form, schema }: SchemaProps) {
|
||||||
if (!schema?.properties) {
|
if (!schema?.properties) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
@ -25,28 +29,40 @@ export default function SchemaFields({ title, parent, form, schema }: SchemaProp
|
||||||
const keys = Object.keys(schema.properties)
|
const keys = Object.keys(schema.properties)
|
||||||
return <div className="ui-schema-form">
|
return <div className="ui-schema-form">
|
||||||
{title && <h5>{snakeToTitle(title)}</h5>}
|
{title && <h5>{snakeToTitle(title)}</h5>}
|
||||||
|
{description && <p>{description}</p> }
|
||||||
<div className="ui-schema-fields">
|
<div className="ui-schema-fields">
|
||||||
{keys.map(key => {
|
{keys.map(key => {
|
||||||
const item = props[key]
|
const item = props[key]
|
||||||
const required = schema.required?.includes(key)
|
const required = schema.required?.includes(key)
|
||||||
|
const title = item.title ?? snakeToTitle(key)
|
||||||
if (item.type === 'string' || item.type === 'number') {
|
if (item.type === 'string' || item.type === 'number') {
|
||||||
return <TextInput.Field
|
return <TextInput.Field
|
||||||
key={key}
|
key={key}
|
||||||
form={form}
|
form={form}
|
||||||
name={`${parent}.${key}`}
|
name={`${parent}.${key}`}
|
||||||
label={snakeToTitle(key)}
|
label={title}
|
||||||
|
subtitle={item.description}
|
||||||
required={required}
|
required={required}
|
||||||
|
textarea={(item.minLength ?? 0) >= 80}
|
||||||
/>
|
/>
|
||||||
} else if (item.type === 'boolean') {
|
} else if (item.type === 'boolean') {
|
||||||
return <SwitchField
|
return <SwitchField
|
||||||
key={key}
|
key={key}
|
||||||
form={form}
|
form={form}
|
||||||
name={`${parent}.${key}`}
|
name={`${parent}.${key}`}
|
||||||
label={snakeToTitle(key)}
|
label={title}
|
||||||
|
subtitle={item.description}
|
||||||
required={required}
|
required={required}
|
||||||
/>
|
/>
|
||||||
} else if (item.type === 'object') {
|
} 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'
|
return 'no key'
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default function SwitchField<X extends FieldValues, P extends FieldPath<X
|
||||||
id={id}
|
id={id}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(event) => onChange?.(event.target.checked)}
|
onChange={(event) => onChange?.(event.target.checked)}
|
||||||
{...form?.register(name, { disabled, required })}
|
{...form?.register(name, { disabled })}
|
||||||
/>
|
/>
|
||||||
<div className="slider round"></div>
|
<div className="slider round"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -66,7 +66,7 @@ const SendProof = ({ open, onClose, onSubmit, type }: SendProofProps) => {
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title="Send Proof"
|
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>
|
<FormWrapper<TemplateProofParams>
|
||||||
onSubmit={async ({ recipient }) => await onSubmit(recipient)}>
|
onSubmit={async ({ recipient }) => await onSubmit(recipient)}>
|
||||||
{form => (
|
{form => (
|
||||||
|
|
|
@ -64,7 +64,6 @@ const TextForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> })
|
||||||
const PushTable = ({ data }: { data: PushTemplateData }) => <InfoTable rows={{
|
const PushTable = ({ data }: { data: PushTemplateData }) => <InfoTable rows={{
|
||||||
Title: data.title,
|
Title: data.title,
|
||||||
Body: data.body,
|
Body: data.body,
|
||||||
Topic: data.topic,
|
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
const PushForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> }) => <>
|
const PushForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> }) => <>
|
||||||
|
@ -79,11 +78,6 @@ const PushForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> })
|
||||||
label="Body"
|
label="Body"
|
||||||
textarea
|
textarea
|
||||||
required />
|
required />
|
||||||
<TextInput.Field
|
|
||||||
form={form}
|
|
||||||
name="data.topic"
|
|
||||||
label="Topic"
|
|
||||||
required />
|
|
||||||
</>
|
</>
|
||||||
|
|
||||||
interface TemplateDetailProps {
|
interface TemplateDetailProps {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue